Skip to content

senior-dev works in a private worktree copy that starts from where you are - #1674

Merged
AbirAbbas merged 22 commits into
devfrom
zeropoint95/senior-dev-git-worktrees
Sep 29, 2026
Merged

AbirAbbas merged 22 commits into
devfrom
zeropoint95/senior-dev-git-worktrees

Conversation

@ZeroPoint95

@ZeroPoint95 ZeroPoint95 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review this first: internal/session/programcopy.go holds the copy, the snapshot and the clean-up. Everything else feeds it.

Try it:

  1. Run make build.
  2. In a repository with uncommitted changes, run /senior-dev <brief>.
  3. Open its task and click ▸ brief.

Why: senior-dev switched your checkout, refused uncommitted work, and failed runs over missing test dependencies.

Changed

  • senior-dev works in a private git worktree copy, not in your checkout. Your branch, index and files never move. The copy lives in ~/Library/Caches/codeaf/worktrees/ and is removed when senior-dev exits. Its branch is always kept.
  • It starts from where you are. Your tracked edits and staged additions become the branch's first commit, written through a private index. Untracked files are copied as local inputs and kept out of automatic commits and submitted snapshots, even if the run edits them; stage a new file before launch when it should ship. A checkout in the middle of a merge is cut from HEAD, and the receipt says why.
  • A run that handed in a change ends 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.
  • The coder thinks at high by default. --variant or a thinking rung overrides it.
  • Shell runs use the profile's worker seat, the same as chat runs.

Added

  • The copy has its own dependencies when the network is on (the default):

    • node_modules is cloned copy-on-write;
    • .venv is left out and rebuilt;
    • .env is copied;
    • the prompt gets a ## Dependencies section.

    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 its bootstrap record.

  • ▸ brief opens the whole brief as a document that scrolls with the page. ctrl+o close brief returns to the steps.

  • program.links in .codeaf/config.json sets which ignored files and folders are carried into the copy.

Fixed

  • Two runs on one repository no longer block each other or overwrite each other's refs. The refs are now refs/worktree/senior-dev/…, one set per copy.
  • A crashed codeaf no longer finishes a copy while senior-dev is still writing to it. The program holds the same lock, passed to it as fd 3.
  • A crashed run's work is committed to its branch. A candidate it submitted is kept on <branch>-submitted.
  • All 16 findings from the adversarial review are fixed (cfcee9762).
  • Landing instructions explicitly put every follow-up check in a worktree for the run's branch.
  • Narrow footers preserve the effort label before shortening the model name.
  • Exclude-file cleanup preserves the original final newline and file existence, plus rules added while a run was active.
  • Copied untracked inputs stay out of eager commits, submission snapshots, final commits and recovery. Snapshot staging excludes them before their contents enter git's object store. New files created by the run remain eligible for commits.
  • The ending's merge instructions stash tracked changes only, leaving local untracked inputs in place.

How it was checked

  • make build passed on final commit 36785a070.
  • Focused regressions passed across session, senior-dev app/utilities, the shell entry point, and the task-page footer. Coverage includes copied inputs absent from git objects and branch history, submitted snapshots, eager commits, crash recovery, retries, and a real stash-and-merge flow that leaves untracked files intact.
  • TMPDIR=/private/tmp GOMAXPROCS=4 GOFLAGS=-p=2 SHARDS=2 make pr-ready BASE=46cc52885 checked all follow-up fixes against the reviewed PR head. Build, vet, formatting, packed/manual gates and repository laws passed. Full cmd/codeaf, internal/seniordev/app, internal/seniordev/util and internal/tui3 suites passed.
  • The full session suite completed with four failures listed below. Each reproduces on the clean pre-fix commit 46cc52885 under the same environment; no test was added to a skip ledger.
  • The default macOS temp path also triggers three existing /var versus /private/var assertions 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
  • TestNewAdmissionReadsSettingsChangedBeforeItsCreation
  • TestHeldBeltRunStopsWithoutPreparingRepository
  • TestHeldRunReopensOnTheSameAdmissionWithoutPreparingFiles
  • TestHeldRecoveryRetainsTheAcceptedCrew

Baseline 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
  • New per-platform files:
    • internal/session/programcopy_{unix,windows}.go
    • programcopy_clone_{darwin,linux,other}.go
    • internal/delegate/hold_{unix,windows}.go
  • StageRecord.Models() reads optional models and effort fields from any stage record. The change is additive within protocol v2.
  • senior-dev's stage data now carries its coder models, at most 12.

Not in this PR

  • senior-dev still adds .senior-dev/ to .git/info/exclude permanently.
  • senior-dev's own ending text still says "failed verification".
  • The footer fallback for runs from before this change.
  • Deleting branches from runs that changed nothing.
  • The missing /resume conversation after an abrupt close.

Checklist

  • Change entry: docs/changes/unreleased/1674-senior-dev-worktree-copy.md
  • Manual updated: senior-dev.md, delegates.md, tasks.md, task-controls.md, keys.md, worker-harness.md
  • No new line in .github/known-red.txt
  • Only my own paths staged

🤖 Generated with Claude Code

ZeroPoint95 and others added 14 commits September 28, 2026 18:48
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>
@AbirAbbas

Copy link
Copy Markdown
Collaborator

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 codeaf and --no-host) and with shell runs, against a throwaway repo with a dirty tree. The person's HEAD, branch, index, files and stash stayed identical before and after. The start receipt, the mid-merge line, stop, recovery after codeaf is closed, side-by-side runs, the footer, ▸ brief, dependency handling and the ending's stash-and-merge all behave as the body says. I proved the merge with the current dev (after #1658) locally with make pr-ready. The only reds were wait-based session tests that fail on dev on this box too, and all of them pass when run alone.

Pushed:

  • b191069f1 The first commit now stages only what git status names. Before, the fresh index dropped the person's index flags: a skip-worktree local config went into the kept branch with its hidden edits, and a skip-worktree or assume-unchanged file missing from disk was recorded as a deletion that the ending's merge would apply.
  • 8956d0e20 With the network on, a node_modules that is itself a symlink is left out. cp -a and clonefile both copied the link, so npm install in the copy wrote into the person's folder.
  • 2b4d86c42 If a swept copy's submitted candidate can't be put on a branch, the copy now stays on disk and the ending says so. Before, it was removed along with the only ref holding the candidate.
  • 338243775 A swept copy no longer gets a <branch>-start branch. refs/worktree/senior-dev/start has the same tree as the base, so every recovered run was leaving that branch, plus a sentence saying it held something.
  • 02465d7f8 manual: ▸ brief doesn't draw the copy-location line codeaf adds at launch, and the page now says so.
  • 46cc52885 A file added to the index and deleted since (AD), or a staged rename whose new name was deleted since (RD), made git add --pathspec-from-file refuse the whole list. The copy then started without ANY of the person's uncommitted work, and the receipt showed could not be read (fatal: pathspec 'gone.txt' did not match any files). The fixed binary carries the work. The old add -A -- . never hit this; the path list in b191069f1 introduced it.

Left for you (not blocking):

  1. After a run, the chat model (deepseek-v4-flash) ran pytest in the person's own checkout rather than on the run's branch. That tests the wrong code, and it left __pycache__ behind, which the next run's first commit then carried. The prompt says to use a worktree, but the landing note could say more firmly where to check, or codeaf could run the checks itself.
  2. The first commit carries every untracked, non-ignored file onto a branch that is always kept, including things like a credentials.json or a 50 MB file. The receipt names only three paths. Worth deciding whether to warn, or to cap by size.
  3. Branches pile up: every run keeps its task/* branch, even when it changed nothing.
  4. Nits:
    • Rewriting .git/info/exclude adds a trailing newline, and leaves an empty file where there was none.
    • At about 20 columns the footer drops the effort ("… · h…").
  5. /resume didn't list a conversation that began with /senior-dev after an abrupt close. This is probably the untitled-conversation problem Fix shell-opened titles and recover task-only conversations #1667 addresses, not this PR.

@AbirAbbas

Copy link
Copy Markdown
Collaborator

Round 2, on 96b9c08a6 and 36785a070. Today's fixes still hold: all 56 programcopy/programfolder tests pass on the head. The exclude-file and narrow-footer nits are fixed, thanks. credentials.json never reaches a branch or git's object store now.

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 parser.py, then /senior-dev Finish parse_numbers in parser.py (I started it) … add tests in test_parser.py.

  1. The start receipt said: "Your untracked files (credentials.json, parser.py) are copied as local inputs, not committed."
  2. senior-dev edited parser.py in its copy.
  3. The run ended: "submitted a change, and the project's own build and tests passed … its work is on the branch …, 1 file".
  4. The branch holds only test_parser.py. A clone of it fails with ModuleNotFoundError: No module named 'parser'.
  5. After the ending's stash-and-merge, all eight new tests fail against the person's original parser.py, which still raises NotImplementedError. The finished function survives only in the run's log.

The local-inputs rule drops edits to untracked files by design: the brief, the finishing commit and TestProgramUntrackedInputsStayOutOfSnapshotsAndEndings all say so. That breaks "finish what I started" whenever what was started is a new file. People rarely stage a new file first, and a hand-off from chat gives them no moment to do it.

Suggestion: treat an input the run changed as the run's work and commit it. Keep only inputs it left unchanged (like credentials.json) out. The ending would then need stash -u for those paths, so the untracked original doesn't block the merge. It should also say when edits to inputs were left out, rather than "passed … 1 file".

Smaller: after the run, the chat followed the new landing guidance and ran git worktree add for its checks. It left that worktree registered, with the run's branch checked out in it.

make test-laws is green on a local merge with dev e7a2bef7c. pr-ready failed on three load-timing tests, and each passes alone.

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>
@ZeroPoint95

ZeroPoint95 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks. The round-2 blocker is fixed in fc7feb003, using your suggestion.

What changed

  • An untracked file the run changed is now its work and goes on its branch. One it left as it was stays off the branch and out of git's object store.
  • codeaf fingerprints each untracked file as it copies it in (SHA-256, outside git). The check lives in internal/gitidentity/inputs.go (Inputs.LeftAlone), and codeaf and senior-dev both use it:
    • codeaf's finishing commit, on a normal ending and on recovery after codeaf went away (ProgramFolder.excludedFromCommit);
    • senior-dev's per-write commit (util.InputLeftAlone);
    • its submitted snapshot, clean-tree check, rollback and DifferentPaths (gitRecorder.ignoredAtStart).
  • The child gets the list through SENIOR_DEV_INPUTS_AT_START, beside SENIOR_DEV_IGNORED_AT_START, on both roads.
  • An input whose fingerprint hasn't been taken yet counts as untouched. That covers a codeaf that dies while copying.
  • The ending names the untracked files the branch holds, and adds git -C <repo> stash push -u -- '<file>' for exactly those, after the plain stash when there is a snapshot. Files it didn't touch are never stashed.
  • The start receipt and the brief now say that any untracked file it changes is committed as its work, and the rest stay off the branch.

Your smaller point: the landing note's check instruction is now one command that adds the worktree, runs the check and always removes it (… worktree add -q --detach "$tmp" <branch> && (cd "$tmp" && <check>); status=$?; … worktree remove --force "$tmp"; exit $status). program-outcome.md says the same.

Checked

  • Your scenario, in the real binary, shell road and chat road, with an untracked half-written hello.txt plus a credentials.json:
    • the branch holds the finished hello.txt;
    • credentials.json isn't on the branch or in the object store;
    • following the ending's stash-and-merge gives the finished file and leaves credentials.json in place;
    • no worktree is left behind.
  • New tests, each confirmed to fail under the old keep-everything-out rule:
    • TestProgramUntrackedInputsTheRunChangedAreItsWork: finished and recovered. It also follows the ending's own stash-and-merge.
    • TestTheProgramIsHandedItsInputsWithTheirFingerprints
    • TestGitRecorderTakesTheInputsTheRunChanged
    • the extended TestEagerCommitSkipsInitialIgnoresAndGeneratedRunPaths
    • TestAnInputIsLeftAloneOnlyWhileItIsAsItWasCopied
  • make test-laws: the only failures are the three job-log /var vs /private/var tests, which fail the same way on dev here.
  • Manual (senior-dev.md, delegates.md) and the change entry are updated.

Still open (from round 1, not blocking): task/* branches pile up, since every run keeps its branch even when it changed nothing. This has been deferred intentionally, it's not a big deal.

AbirAbbas and others added 5 commits September 29, 2026 15:17
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>
@AbirAbbas

Copy link
Copy Markdown
Collaborator

Taking over (round 3): fc7feb003 fixes the blocker. I checked it by hand in the real binary on the shell road, the chat on the engine road, and a closed-pane recovery. The setup was an untracked half-written parser.py, a credentials.json, an unstaged edit and a staged util.py.

  • The branch holds the finished parser.py, and a clone of it passes its tests. The recovered run's branch has the same file.
  • credentials.json is off the branch and out of the object store.
  • The checkout is untouched.
  • The ending's stash, stash push -u and merge work, and the chat's check worktree is removed afterwards.

I pushed five small fixes, all about how file names reach git:

  • 920008aa2 The ending's stash push -u -- <file> also took git glob matches. notes[1].md also stashed an untouched notes1.md, which the manual's stash drop then deleted. Names containing *?[\ now get :(literal), and 'parser.py' reads as before.
  • 546fd4153 senior-dev's per-write commit and codeaf's finishing commit had the same glob problem: an untouched notes1.md got committed, or written into the object store. Both now use --literal-pathspecs.
  • bc2bd56f1 The changed-file list now reads names with -z. Before, a non-ASCII input such as résumé.md was left out of the stash advice, and the person's own copy of it then blocked the merge.
  • 97f085227 A name that starts with : is also written :(literal). Git read ':colon.txt' as magic and stashed colon.txt.
  • 580e84b36 manual: the ending's commands leave two stashes, and git stash pop on the untracked one fails with parser.py already exists, no checkout. A new section, "Bringing senior-dev's branch in", says to drop once per stash, and how to read a later version of a file out of its stash.

The earlier CI red on fc7feb003 was a test race that predates this PR, in TestAParentHandedADivisionBeforeItStartedOpensOnTheReportsAndNotOnTheWait. It passed 85/85 locally and green on re-run. I'll fix it separately.

Small things left for you, not blocking:

  • If an input's fingerprint can't be taken after the copy, edits to that input are dropped.
  • Each candidate snapshot re-hashes large inputs, about 119 ms for a 200 MB file.
  • The ending can say "the project's own build and tests passed" right next to "no build or tests it could find to run".

@AbirAbbas AbirAbbas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed and hand-verified over three rounds; see the comments above.

@AbirAbbas
AbirAbbas merged commit 461a43f into dev Sep 29, 2026
5 of 7 checks passed
@AbirAbbas
AbirAbbas deleted the zeropoint95/senior-dev-git-worktrees branch September 29, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants