senior-dev works in a private worktree copy that starts from where you are - #1674
Conversation
A program that edits code no longer switches the person's checkout onto its branch and holds the repository for the run. In a git repository codeaf cuts the run a git worktree under the system's temporary folder, on a branch of its own from the commit the checkout stands on, and removes it the moment the program exits: what it left is committed on its branch, the branch is always kept and released, and the person's checkout, index and uncommitted changes are never touched. Runs on one repository now work side by side; a plain folder is still worked in itself and held. Ignored dependency and environment folders (node_modules, .venv, .env, ...) are linked into the copy, never build output; a project names its own list as program.links in .codeaf/config.json. A run whose process went away is finished the same way by the next codeaf. A run codeaf sends back after an ending carries on on the earlier run's branch. senior-dev's start and submitted refs are per worktree (refs/worktree/senior-dev/*), so two runs no longer overwrite each other's. The coder's reasoning effort defaults to high; history summaries send none. The chat may set thinking (low..max) on a hand-off, which outranks a rung on the worker seat; --variant takes the one ladder plus none. /task --best and --cheap now reach a program's unpinned worker, and a shell run with no --high works on the profile's worker seat as a chat run does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A copy is never deleted with its program's work unsaved: a stale index.lock a killed git left is cleared before the finishing commit, and a copy git can no longer read, or whose leftovers can be neither committed nor patched, is kept on disk and the ending says where. The hold on a run's folder is handed to the program's own process, so a codeaf that dies leaves its copy alone until the program has stopped. A run found gone credits only models that answered, keeps a submitted candidate on <branch>-submitted, and says when nothing was left to commit. Rescue branches take the first free name. Copies move from $TMPDIR to the account's cache folder (or CODEAF_HOME's worktrees when that moved), refusing a folder that is a link or another account's. Exclude lines for links are fenced, written under a lock by rename, and removed with the last copy. program.links takes a JSON list and refuses the run when it does not apply; a missing git-lfs is named. A sweep rereads a record under the hold, so no run is finished twice. The run after a pass starts on a new branch cut from it. Receipts, the refusal wording, the stop promise, the outcome prompt and the manual now say what the code does; /task --best was never a road to senior-dev and the manual no longer says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ncluded A run in a repository was cut from the checkout's last commit, so "finish what I'm in the middle of" handed senior-dev work that was not there. What the checkout has not committed — edits staged or not, deletions, new files git does not ignore — is now written into one commit on top of HEAD through an index of codeaf's own, the way sealGroundWork carries a task's parent, and the program's branch begins with it. The person's files, index and branch are only read; the changes stay uncommitted there. The notes folder is left out, a checkout mid merge or rebase is not carried (its files hold conflict markers), and a git that will not read it leaves the run starting from HEAD, said on the receipt. The program's work is counted from that commit, so the person's changes are never its files and a run that adds nothing changed nothing; a run carried on counts from the line's snapshot and carries nothing new. The brief says the first commit is the work so far, the receipt says it was carried in, and the ending says to stash -u before the merge that brings in both. Manual, prompt and tests follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…project A fortnight of real runs showed senior-dev's `fail` endings came from its own check of the project — a CI line cut in half, `python3 -m pytest` on a machine with no pytest — and none from a change that broke the project, and each one woke the conversation to fix work that was never broken. - A change senior-dev handed in is finished work whatever its check said: DelegateWorker lands a `fail` terminal with `submitted: true` as done, the check's word riding on as the verdict, and the chat is told that check is a lead, not a verdict, to run the project's own checks itself and act only on what those show. `failed` now means it handed in nothing. The shell's last line says `finished` the same way. senior-dev's protocol is unchanged. - With the network on (the default), senior-dev is told to install what the project's tests need into the project's own environment (.venv, npm ci), never the system's, and never to fake a missing tool; one run had written a pytest.py stand-in. With SENIOR_DEV_NET=off the section is absent. - The check runs with the project's .venv active, and probes for pytest with that interpreter. - With the network on, the copy has dependencies of its own instead of links into the person's folder: .env files copied, node_modules cloned copy-on-write where the disk can (clonefile on APFS, reflink on Linux) and otherwise left for the program to install, a .venv never carried (an editable install in it points at the person's source), and .venv/venv/ node_modules kept out of git in the copy. With the network off, links as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
senior-dev says the models its coder routes on and its effort in its bootstrap record, after its defaults and the crew's leniency; codeaf keeps them on the program record and the task page. A program's organized room shows them on the seam where it said Conversation totals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The brief dropdown used to pin the brief between the head's rules, cut to half the frame. Open, it is now the room's body: the whole brief from its top, under its work order's plain headings, one line per line it kept, list items hung, capital leads in bold, read by the room's own scroll; shut, the steps come back where they were. The key row says ctrl+o close brief while it is open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hanges The test still asserted that a shell run's branch holds none of the person's uncommitted work, which stopped being true when the copy began carrying those changes in as its first commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The snapshot of the person's uncommitted work read the start commit into a private index and ran `git add -A -- .`. A fresh index has none of the person's index flags, so a tracked file they hid with skip-worktree (the "local config I never commit" idiom) was committed into the branch with its local edits, and one hidden and absent from the working tree was recorded as a deletion, which the ending's merge command would then apply to their branch. Neither was named on the receipt. The snapshot now stages exactly the paths `git status` reports, both sides of a staged rename included, as literal pathspecs through a pathspec file beside the private index. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…y as a link With the network on, a copy is meant to have dependencies of its own, so the program's installs never reach the person's folders. carryOne read the source with os.Stat, which follows a link, so a node_modules that is itself a link to a folder elsewhere read as a folder and went to cloneTree; both `cp -a` on Linux and clonefile(CLONE_NOFOLLOW) on a Mac copy the LINK, and the program's `npm install` then wrote through it into the person's folder. A folder that is a link is now left out with the network on (node_modules and a virtual environment are installed fresh in the copy), and linked only when the project listed that name itself in program.links. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e kept A run whose codeaf went away is finished by the next codeaf's sweep, which puts a branch on each of the copy's own refs its branch does not hold (the submitted candidate at refs/worktree/senior-dev/submitted) and then removes the copy. A `git branch` that failed there was ignored, so the copy, and with it the only ref holding the candidate, was deleted anyway. keepOwnRefs now answers what it could not keep, and the copy stays on disk through the existing kept-copy ending, naming the ref, per the file's own law that nothing the program left is deleted unsaved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
senior-dev keeps refs/worktree/senior-dev/start, a commit of the exact tree it started from that is not an ancestor of its branch. The sweep that finishes a copy whose codeaf went away kept every copy ref its branch's tip did not hold, so each recovered run left a `<branch>-start` branch holding nothing new and an ending that said "what it had saved at refs/worktree/senior-dev/start, which its branch does not hold, is kept on the branch <branch>-start". Seen on both the next-run sweep and an engine stop in the real binary. branchHolds now also counts a ref whose tree matches the run's base or any commit between the base and the tip as held. A submitted candidate with a tree of its own is still kept on `<branch>-submitted`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The page said `▸ brief` shows the whole brief senior-dev was handed from its first line. The line codeaf puts at its head when senior-dev works in a copy (where the copy is, its branch, and that the branch begins with the person's uncommitted work) is added at launch and is not drawn there; the page now says so and where the branch is named instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… of the work
The snapshot now hands git add only the paths git status names, and git
refuses the whole list when one of them matches nothing: a file added to
the index and deleted since ("AD"), or the new name of a staged rename
deleted since ("RD"). That one refusal left every other uncommitted change
out of the copy, with git's own line in the receipt. Paths that are on
neither side are dropped before git is asked.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Taking over: I pushed six small fixes. Nothing blocks merging after them. I checked this by hand in the real binary on both roads (plain Pushed:
Left for you (not blocking):
|
|
Round 2, on One blocker, from a hand run in the real binary (shell road and chat road), so I'm holding the merge: Setup: an untracked, half-written
The local-inputs rule drops edits to untracked files by design: the brief, the finishing commit and Suggestion: treat an input the run changed as the run's work and commit it. Keep only inputs it left unchanged (like Smaller: after the run, the chat followed the new landing guidance and ran
|
Every untracked file copied into the run's copy stayed off its branch, even one the run finished, so a half-written parser.py a person asked it to finish was lost with the copy (review round 2 on #1674). Each input is now fingerprinted as it is copied in; codeaf's finishing commit and senior-dev's eager commits, candidate, clean-tree check and restore all take an input the run changed and leave one it did not, which never reaches git's object store. The ending adds git stash push -u for exactly the untracked files the branch holds. The chat's check command now adds and removes its temporary worktree in one line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks. The round-2 blocker is fixed in What changed
Your smaller point: the landing note's check instruction is now one command that adds the worktree, runs the check and always removes it ( Checked
Still open (from round 1, not blocking): |
The ending tells the person to move aside, with `git stash push -u -- <file>`, each untracked file of theirs the run changed. git reads those words as pathspec patterns, so an input named `notes[1].md` also stashed an untouched `notes1.md` (and `star*.txt` took `starZ.txt`); the manual's next step for a person who changed nothing since, `git stash drop`, then deleted files that exist nowhere else. Bracketed names are common (Next.js routes such as `app/[slug]/page.tsx`). A name holding a pattern character now carries git's `:(literal)` magic in the command; an ordinary name such as parser.py reads as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A person's untracked file the run leaves alone must stay off its branch and out of git's object store. But senior-dev's per-write commit (`git add -- <path>`, `git commit --only -- <path>`) and codeaf's finishing commit (`git add -A -- <paths>`) hand paths to git as pathspecs, which git reads as globs: writing `notes[1].md` beside an untouched input `notes1.md` committed `notes1.md` too, and the finishing commit wrote its bytes into the object store before a reset kept it off the branch. Those calls now run with --literal-pathspecs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
changedBetween read `git diff --name-only` without -z, so git's default
core.quotePath turned `résumé.md` into `"r\303\251sum\303\251.md"`. The ending
compares those names with the person's untracked files the run changed, so
such a file was on the branch but missing from the `stash push -u` advice,
and the person's own copy then blocked the merge ("untracked working tree
files would be overwritten"). The paths are read NUL-separated now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erally git reads a pathspec that starts with `:` as magic, so for a changed untracked file named `:colon.txt` the ending's `stash push -u -- ':colon.txt'` stashed an untouched `colon.txt` instead. Such a name now carries `:(literal)` like a name with pattern characters. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ranch in The page said that after the ending's commands `git stash drop` discards what you put aside and `git stash pop` puts later edits back on top. With an untracked file the run changed, the ending's commands make two stashes, so one drop left the tracked one behind; and a pop of the untracked one fails (`parser.py already exists, no checkout`) because the branch now holds that file. Seen in the real binary on a copy of the person's project. The advice has its own section now, says to drop once per stash, and how to read a later version of an untracked file out of its stash before dropping it. It also names the `:(literal)` spelling the ending uses for bracketed names. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Taking over (round 3):
I pushed five small fixes, all about how file names reach git:
The earlier CI red on Small things left for you, not blocking:
|
AbirAbbas
left a comment
There was a problem hiding this comment.
Reviewed and hand-verified over three rounds; see the comments above.
Review this first:
internal/session/programcopy.goholds the copy, the snapshot and the clean-up. Everything else feeds it.Try it:
make build./senior-dev <brief>.▸ brief.Why: senior-dev switched your checkout, refused uncommitted work, and failed runs over missing test dependencies.
Changed
~/Library/Caches/codeaf/worktrees/and is removed when senior-dev exits. Its branch is always kept.finished, even when its own check failed. codeaf treats that check as a lead and runs the project's own checks first. A run that handed in nothing still ends failed.highby default.--variantor athinkingrung overrides it.Added
The copy has its own dependencies when the network is on (the default):
node_modulesis cloned copy-on-write;.venvis left out and rebuilt;.envis copied;## Dependenciessection.With
SENIOR_DEV_NET=off, the folders are linked instead and that section is left out.The task page footer names the models:
senior-dev on deepseek-v4-pro, kimi-k2.6 · high. senior-dev reports them in itsbootstraprecord.▸ briefopens the whole brief as a document that scrolls with the page.ctrl+o close briefreturns to the steps.program.linksin.codeaf/config.jsonsets which ignored files and folders are carried into the copy.Fixed
refs/worktree/senior-dev/…, one set per copy.<branch>-submitted.cfcee9762).How it was checked
make buildpassed on final commit36785a070.TMPDIR=/private/tmp GOMAXPROCS=4 GOFLAGS=-p=2 SHARDS=2 make pr-ready BASE=46cc52885checked all follow-up fixes against the reviewed PR head. Build, vet, formatting, packed/manual gates and repository laws passed. Fullcmd/codeaf,internal/seniordev/app,internal/seniordev/utilandinternal/tui3suites passed.46cc52885under the same environment; no test was added to a skip ledger./varversus/private/varassertions in the law gate. Those were reproduced on the clean pre-fix commit before using the canonical temp path above.Four session failures reproduced before these fixes
TestNewAdmissionReadsSettingsChangedBeforeItsCreationTestHeldBeltRunStopsWithoutPreparingRepositoryTestHeldRunReopensOnTheSameAdmissionWithoutPreparingFilesTestHeldRecoveryRetainsTheAcceptedCrewBaseline command:
TMPDIR=/private/tmp GOMAXPROCS=4 GOFLAGS=-p=2 make test-focus PKGS=./internal/session RUN='^(TestHeldBeltRunStopsWithoutPreparingRepository|TestHeldRecoveryRetainsTheAcceptedCrew|TestHeldRunReopensOnTheSameAdmissionWithoutPreparingFiles|TestNewAdmissionReadsSettingsChangedBeforeItsCreation)$' TEST_FLAGS=-count=1.Earlier real-binary review covered both chat entry paths and shell runs, dirty-checkout preservation, stop/recovery, side-by-side runs, dependencies, the model footer, and scrolling a 10,000-character brief.
Reviewer notes
internal/session/programcopy_{unix,windows}.goprogramcopy_clone_{darwin,linux,other}.gointernal/delegate/hold_{unix,windows}.goStageRecord.Models()reads optionalmodelsandeffortfields from any stage record. The change is additive within protocol v2.Not in this PR
.senior-dev/to.git/info/excludepermanently./resumeconversation after an abrupt close.Checklist
docs/changes/unreleased/1674-senior-dev-worktree-copy.mdsenior-dev.md,delegates.md,tasks.md,task-controls.md,keys.md,worker-harness.md.github/known-red.txt🤖 Generated with Claude Code