Repository navigation
Conversation
The PR description documents sufficient build and real-CLI verification. No commands from the PR were run. |
The base came from whichever driver first sent one to next, with ref always HEAD. A missing ref then made the local driver drop the whole workspace and run the step in the member's checkout.
restoreUneditedConfig staged AGENT_PATHS with one git add. Git exits 128 when any listed path matches no file in the worktree or the index, and then stages nothing, so a copy kept the base agent file in its index after a step that edited none.
next answered wait at the first ready step whose workspace the driver could not take, so a driver without new never got a ready read-only step. Settling now returns every ready leaf in order, next leases the first this driver can take, and it waits only when every ready step is held back, naming each one.
A command such as `npm test` after an implement step inside a `new` copy ran in the member checkout, so it tested code without the change. A read-only step under a copy owner now leases a read-only contract on the branch its need wrote (or the owner's branch), runs in that worktree on the base agent files, commits nothing, and reports the commit it read as `read`. A copy the reader changed is reset to that commit, so a later step there does not commit the change. Needs that wrote two branches fail with workspace_base. Part of Tencent#1246 (F4).
…rkflows A branch can commit .github as a symlink. The nested Copilot paths were then copied and removed through it, deleting files outside the worktree. A root-level hooks file no longer yields '.' as a path to reset.
…the reader changed A reader that detached HEAD changed nothing the snapshot measured, so a later continue step committed off the branch. A reset also dropped a person's uncommitted edits, ignored git failures while reporting the copy as put back, and left initialized submodules where the reader moved them. The state is now kept on refs/teamai/kept/<attempt> (not the stash list, which every worktree shares) before the reset.
…m fails When update-ref or stash create failed, the failure said the copy could NOT be put back and told the user to put it back, but not that the edits exist only in the copy. Read literally, that is a reset that destroys them. The result of leaving a read copy is now a tagged union (untouched, restored, unrestored), so each message follows from one state.
After a failed restore the driver still leased later steps into that copy, so a continue writer could commit on a detached HEAD while reporting the named branch, or commit what the reader left. leaveReadCopy now writes a marker in the worktree's own git dir. stageLease refuses a workspace lease while the copy is not on refs/heads/<branch> at the marker's commit with a clean status, and removes the marker once it is, so the person who puts the copy back only runs teamai workflow retry.
… destroying it git stash create in the superproject never saves uncommitted or untracked files inside a submodule, yet submodule update --force and a foreach git clean discarded them, while the failure said the changes were kept. Before any keep or restore, each initialized submodule is checked recursively. When one has such files, nothing is reset, and the reader fails unrestored naming them, which also blocks later steps in that copy. The foreach clean is gone.
…put back git clean -fd skips a repository the reader created inside the copy, every restore command still exits 0, and the next writer's git add -A committed it as a gitlink while the failure said the copy was put back. After the restore, the copy now counts as put back only when HEAD is refs/heads/<branch> at the commit it read and git status, submodules included, is empty. Otherwise the reader fails unrestored naming what is left, and the nested repository stays.
submodule update without --checkout follows a configured update=merge, rebase or none, so the reader's submodule commit survived the restore and a later writer committed that gitlink. The restore now passes --checkout. --force is dropped: a dirty submodule never reaches this point, and a clean one checks out without it.
symbolic-ref --short prints heads/<branch> when a tag has the branch's name, so a reader that changed nothing looked like it had moved HEAD and failed read_only_dirty.
…d its copy exits non-zero The changes of a failing reader still went to refs/teamai/kept/<attempt> and were removed from the copy, but its report was only the exit status, so nothing said where they went. The command's exit status and output snippet stay first, followed by the same put-back or could NOT be put back sentence a passing reader gets.
…s branch ensureWorktree returned an existing worktree without checking HEAD, so after a driver stopped before it put a read copy back, the next writer committed on a detached HEAD and reported the named branch. An existing worktree whose HEAD is not refs/heads/<branch> now fails workspace_base, naming the directory, where HEAD is, and the branch to check out.
The base_config message said to commit the symlinked parent as a directory on the branch and retry. When the symlink comes from the run base, every run branch starts with it and no step can fix it. That case now says to commit it as a directory on the base branch and start a new run. The branch case names the branch and the worktree to commit in.
local-driver.ts had crossed 1,000 lines. copyLeftOver, dirtySubmodules, the unrestored marker and the keep-and-restore sequence are git-level handling of a worktree and take no lease, so they move to worktree.ts as putBackCopy and unrestoredCopy. leaveReadCopy keeps the snapshot, overlay and lease wiring. No behaviour change.
…der started The postcondition required an empty status, so a submodule an earlier writer left dirty could never pass, and it read the user's status config, so status.showUntrackedFiles=no hid a repository the reader left. The copy's status is now captured at staging, before the overlay, and put back means that status again, read with --untracked-files=all.
…reader changed Every dirty submodule blocked the restore, so a submodule an earlier writer left dirty failed a reader that never touched it. Only repositories among the reader's changed paths are checked now. Inside each, status ignores only dirty nested submodules, so a commit made in vendor/inner alone shows as a change in vendor and the copy is left untouched instead of being reset. A submodule with no checkout left counts as unkept. The reset and checkout no longer recurse into submodules under submodule.recurse=true.
…copy is removed A gitlink on the kept ref does not keep its commit reachable, and removing the copy deletes the submodule's own store. Each submodule commit the kept tree records and the read commit does not, including a repository the reader created, is pushed into the workspace repository under refs/teamai/kept-modules/<attempt>/<path> before any reset. A failed push is a failed keep: nothing is reset and the message says the changes were NOT saved.
…river leaves it refused The marker was written only after a failed put-back, so a driver killed while the reader ran left no marker, and on resume a later writer committed the reader's files. Staging a read-only lease now writes the marker, with the copy's status, before the overlay, and fails workspace_base if it cannot. A copy put back or left untouched clears it; an unrestored one keeps it with the reason. A resumed copy that is back as it was found clears it by itself.
…is not a worktree A copy whose directory was deleted stays registered, so worktree add refused its path; it is now pruned and created again. A plain directory at the copy path was told to check out the branch, which would switch the checkout around it; it is now refused as not a workflow worktree. A marker that exists but cannot be read now blocks the copy instead of counting as absent.
… and where submodule commits are kept Ignored files stay, the copy is marked when the reader starts so a killed driver still blocks later steps, and submodule commits are kept under refs/teamai/kept-modules/<attempt>/.
…pt sentence Keeps local-driver.ts under 1,000 lines: clearing the reader's mark when nothing changed and the kept-ref sentence live beside the keep they describe.
…py untouched At 9d4fff6 this was reported as kept while the submodule's uncommitted files were lost. The changed-repositories check already fails it closed.
…y a marked copy is refused
…ubmodules a reader changed A status line names a path, not its content, so a marker whose copy was dirty when the reader started is no longer cleared by matching status; the person who puts the copy back deletes it. Module pushes ignore push.recurseSubmodules.
…enever a copy's directory is missing
…op, can still run in it
… step's snapshot, keep, and commit
…reporting, or pushing for it
…h or detaches it at the same commit
73a3a8b to
cd90927
Compare
|
No new findings in the scoped diff. All four earlier findings are resolved: cleanup accounts for unexpanded workflows, submodule-ignore settings no longer hide changes, writers validate their leased branch before committing, and independent commands can run beside headless prompts. The PR description documents sufficient build and real-CLI verification at Review was read-only; no PR code was run, built, or installed. |
…e driver's git checks With color.branch=always, git branch --contains printed escape codes around the name, so a retry never pushed a branch whose first push failed. With diff.external and diff.trustExitCode, an external diff that called two blobs equal made commitWork skip a writer's staged change. Also tests that status.showUntrackedFiles=no does not hide a writer's new file.
All four earlier findings are resolved and are not raised again. The PR description documents sufficient build and real-CLI verification, including head Review was read-only; no PR code was run, built, or installed. |
…to finish it push_failed carried only git's stderr. It now names the branch and the worktree that hold the commits, and says to push that branch from there or retry the step. Git's own text comes last.
…ced with a symlink The symlinked-parent check ran only before a step. A step that ran mv .github <outside> && ln -s <outside> .github passed it, and restoreUneditedConfig then followed the new link: it deleted .github/agents files outside the worktree and removed the link. Restoration now skips every agent path whose parent is a symlink, keeping the step's edit. The next step still fails base_config on it.
|
No new findings in the scoped diff. All five earlier findings are resolved, including the restoration guard for step-created parent symlinks, and are not raised again. The PR description documents sufficient build and real-CLI verification at head Review was read-only; no PR code was run, built, or installed. |
Heads-up on an overlap with #1234. Both PRs edit the |
Summary
Closes the workflow workspace gaps listed in #1246. Rules after this PR:
AGENT_PATHSfrom the run's base:AGENTS.md,CLAUDE.md,.agents,.githooks, each defaulttoolPathsproject directory and MCP file, and Copilot's.githubagent paths, not.github/workflows. A symlinked parent failsbase_config. When a step itself replaces such a parent with a symlink, restoring the agent files skips everything under it, so nothing outside the worktree is deleted.workflow startin a git checkout recordsrun.base(commit, plus branch unless detached). A laterbasenever changes it. An unreadable leaseworkspacefailsworkspace_base.waitcomes only when every ready step is held, with each reason.read. The driver puts the copy back, keeping what the step changed onrefs/teamai/kept/<attempt>andrefs/teamai/kept-modules/<attempt>/, or fails itworkspace_baseand leaves the copy marked.--ignore-submodules=none, and each submodule is staged by name, sincegit add -Askipsignore=all. A read-only step fails when HEAD's commit or its branch changes. A writer always stages, sostatus.showUntrackedFiles=nodoes not hide its new file, and--no-ext-diffkeeps a trusteddiff.externalfrom calling its change empty.changes, and pushes only from its leased branch. Off it, the step failsworkspace_base, nothing is committed or pushed, and the worktree stays.reportsetscleanuponly when no step that can still run uses the copy, judged on the run as the nextnextsettles it: loops have re-pended their steps, ready workflows and fan-outs have added theirs, and one not started counts by the copy around it unless it opens its own.max_parallelslot, and steps in other checkouts start beside it. Steps in its checkout wait. An interactive prompt holds every slot.color.branch=always(git branch --no-color).push_failednames the branch and the worktree that hold the commits, then says to push that branch from there or retry the step, and quotes git last.changesorreadthat do not match the lease returninvalid_report. A workspace step uses the worktree's own config for its tool. Run records keepbad_session.Evidence
Head
cd909273:tsc --noEmit, lint,vitest run(476 files, 11453 tests) and build pass. Full e2e: 160 files pass. The two terminal Ctrl-C cases inworkflow-signals.test.tsfail here at the same setup assertion onmain(c6bd1264) too; CI runs them.Real CLI (
dist/index.js,init --self --provider git, Claude, local bare origin,workflow start <name> --driver local). The repo has avendorsubmodule withignore = allin.gitmodules.ship:changecopy,implementwrites ignoreddeps/, nestedverify.testreads ittestfails "deps/installed.txt missing"testswitch: writer runsgit switch -c scratchworkspace_base"left workspace … on refs/heads/scratch, not on branch …"vendor: reader commits invendorread_only_dirty, commit kept underrefs/teamai/kept-modules/<attempt>/Head
ae8b8606adds--no-ext-diffand--no-coloron top ofcd909273.tsc --noEmitand lint pass, and so dovitest run src/__tests__/workflow(18 files, 1125 tests),commands-reference docs-examples, build andtest:e2e -- workflow-workspaces. On the real CLI as above, withstatus.showUntrackedFiles=noanddiff.ignoreSubmodules=allset, three things were checked. A writer whose only change is a new file commits it. A reader's commit invendorfailsread_only_dirtyand stays off the branch. The next writer'svendorcommit lands on it. With the machine's global git config,push.recurseSubmodules=checkthen fails that writer's push withpush_failed, as the user's policy says.Head
3ef4e188adds thepush_failedmessage (ba957ded) and the restore guard for a step-made parent symlink (3ef4e188).tsc --noEmitand lint pass, and so dovitest run src/__tests__/workflow(18 files, 1127 tests), build andtest:e2e -- workflow-workspaces. Both changes were tested first, and both tests failed before the fix. On the real CLI, the run with the global config above now fails with "… but pushing it to origin failed. The commits stay on . Fix what blocked the push, then push from or retry the step. Git said: …". In a second real-CLI run, the base has.github/agents/a.mdand the step runsmv .github <outside> && ln -s <outside> .github. The step succeeds,<outside>/agents/a.mdis kept, and the branch records.githubas a symlink (120000).Table tests: copy removal (7 shapes), submodule ignore (3 settings × 4 observers), writer off branch (4), HEAD moves (2), steps beside a prompt (4). Every row but one guard row in each of the copy and prompt tables failed before its fix.
Left to CI: Codex, CodeBuddy, OpenCode, and the
gitlab/githubproviders; the changed code does not depend on them. The prompt overlap is covered by the driver test only, since a real prompt needs a live agent.Merge Danger
Door: two-way. Run files gain optional fields (
read,run.checkout, an optionalbase.ref). No migration.Blast Radius: workflows. Runs with workspaces, or prompts with
max_parallelabove 1, behave differently. Submoduleignoresettings no longer hide a step's changes.Closes #1246