Skip to content

The post-merge review's nine findings, fixed as directed - #1594

Merged
suleimansh merged 11 commits into
mainfrom
claude/pr-1536-refactor-review-gs46xm
Aug 19, 2026
Merged

The post-merge review's nine findings, fixed as directed#1594
suleimansh merged 11 commits into
mainfrom
claude/pr-1536-refactor-review-gs46xm

Conversation

@brillout

Copy link
Copy Markdown
Member

Implements every direction from the review round on #1536, one commit per instruction, in the order given.

What each commit does

  1. handoff: local holdsremoveProjectWorktree keeps a publish-nothing session's unpushed checkout (the remote-less fallback, no new mechanism) instead of pushing it to make removal possible. The armed handoff is read off the agent's meta (written as its first event), so a boot death keeps the recoverable default, and an already-pushed branch still lets the checkout go. Covers teardown, the sweep, and the dashboard's Remove alike, since they share the one implementation.
  2. Footer — the framework "<what to build>" line D4 orphaned is gone; a test pins the absence.
  3. Spec cleanupreadAgentSpec consumes the mkdtemp directory with the file, and both spawners remove a spec whose child never ran. Only a directory carrying the module's own prefix is removed whole, so a hand-written --agent <path> spec can't take the user's directory with it.
  4. Stale claim comment — the daemon-services.ts line claiming the pinned entry "outlives the agent's local process" is replaced with what holds (the pushed drain lock for tickets), and auto-pm.SPEC.md now owns the ticketless hand-off window as accepted-not-closed, with the rationale on record.
  5. Shutdown gaponStart refuses once stopAgents has run, checked at entry and re-checked right before the spawn (the framework: auto PM has no stopped flag, so quiesce cannot stop an in-flight sweep from spawning an orphan run #983 shape), cleaning up the spec it wrote when the late check refuses. No shutdown reordering.
  6. Dead surface deletedpreset:/event: (keys, resolution, echo, chips — absence pinned beside the other retired spellings), the six transport-less option fields plus the modes event kind and OPEN_LOOP_MODES, the unreachable "not enabled on this server" guards, and the control.SPEC.md sentence that contradicted context.SPEC.md. The surviving option docs stop naming D4's flags.
  7. SweephasRemote asked once per project (no per-checkout probe-and-push on a repo that can never push), and each kept checkout's line said once, not every ten minutes — re-announced only if the checkout reappears after a removal or a daemon restart.

Behaviour changes, called out

  • A handoff: local session's checkout now stays on disk (visible on the Remove list) until its branch reaches the remote by someone's explicit act, or Delete throws it away. Remove refuses for it the same way it refuses on a remote-less repo. The lifecycle e2e story pins the new contract (checkout kept, nothing on origin); the publish story runs push-armed so it keeps pinning the click-vs-teardown ref collision.
  • An unknown preset:/event: value in a committed yml no longer errors — the keys are unknown now, and unknown keys are ignored (the zero-migration rule, pinned in config.test.ts).

Verification

Both typechecks, a full build, 1466 node tests / 771 dashboard tests (81 files), all green on the head. The three files spanning multiple commits are byte-identical between the tested tree and the final commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj


Generated by Claude Code

claude added 7 commits August 18, 2026 21:34
…hed (B5/E5)

removeProjectWorktree pushed any unpushed branch to make removal recoverable, and neither it
nor any caller read the handoff level — so teardown published every `handoff: local` session's
branch to origin at session end, the sweep retried until it landed, and the dashboard's Remove
did the same. That overrode the rung B5 exists to deliver, a Stop's "publish nothing of what I
cut short", and the daemon's own CI-fix agents, which run `handoff: local` precisely so they
push nothing of their own.

The fix is the rule's own fallback rather than a new mechanism: a session armed to publish
nothing keeps its unpushed checkout, exactly as a repo with no remote keeps every checkout.
The armed handoff is read off the agent's meta — written as its first event — so a meta
without one (a boot death) keeps the recoverable default, and a branch already on the remote
still lets the checkout go: removing what the remote holds publishes nothing.

The lifecycle story pins the new contract: a local-handoff session ends with its checkout on
the Remove list and nothing on origin; the publish story runs push-armed, keeping the
click-vs-teardown ref collision it exists to pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
`framework "<what to build>"` printed at every daemon start, and following it answered
`unknown command` with exit 2: D4 deleted the positional intent path and left the line.
The footer test pins the absence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
… it up

writeAgentSpec makes one mkdtemp directory per spec; readAgentSpec removed only session.json,
leaving one empty directory per session behind for the life of the machine. And on the spawn
`error` path nothing ran to consume the spec at all, so the whole prompt stayed on disk —
against the module's own claim that a spec never outlives the session it started.

Consuming now removes the directory, but only one carrying the module's own mkdtemp prefix:
`--agent <path>` accepts any path, and a hand-written spec must not take the directory the
user keeps it in with it. Both spawners remove the spec their dead child never read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
…ent's to deny

The comment beside the drain start still claimed the pinned queue entry "rides along (#1253)"
and outlives the agent's local process — a durable claim E2 deleted. What actually holds: a
ticket-linked entry is claimed by the pushed drain lock (#1420); a ticketless one has only the
sweep's in-memory pin, which releases at a web hand-off (the local meta settles while the
cloud session still works the entry) and dies with the daemon — so past the cooldown the same
open entry can fan out to a second agent.

auto-pm.SPEC.md owns that window now, as accepted rather than closed: the claim it would take
was deleted as a read-time guess, tickets are the queue's normal case and stay covered, and
the queue's planned move onto an eagerly-pushed data branch closes the window structurally. A
second claim shape built now would be deleted then.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
…983)

The HTTP surface closes last in shutdown, and neither sendStart nor /_relay/start checked any
closing state — so a Start arriving during the quiesce, stop and flush span spawned a
detached, unref'd child in its own process group, outside the snapshot stopAgents terminates:
an orphan on ppid 1 holding a worktree and burning quota after "Ctrl-C closes everything".
Auto-pm already re-checks its stop before every spawn for exactly this shape (#983); onStart
now does the same, at entry and again right before the spawn (everything between is awaited),
removing the spec it wrote when the re-check refuses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
…stubs, unwired-host guards

Zero users, so what nothing consumes goes rather than being documented around:

- `preset:` / `event:` were parsed, type-checked, echoed on the `◆ config:` line and rendered
  as launcher chips, while A5 deleted their every consumer — a setting that silently does
  nothing, wearing a confirmation. They are unknown keys now like every retired spelling, and
  the absence is pinned beside the other retirements.
- D4 left six option fields with no transport: `StartAgentOptions.autopilot`/`technical`
  (written and read by nothing, under a header still claiming they map to run flags),
  `AgentOptions.sessionLink` (a live reader no writer could reach, with two doc comments
  instructing embedders to set it), `preset`/`buildEvent` on the flag layer, and
  `todoMaxItems` (read live, settable nowhere — the suite asserted the void). The `modes`
  event kind and OPEN_LOOP_MODES, which nothing emitted, go with them, and the surviving
  option docs stop naming flags that no longer exist.
- The "not enabled on this server" guards were unreachable since D3 made every context field
  required and the accessor throw on a wiring bug; the guards go, and control.SPEC.md stops
  describing the multi-host world F3 removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
…once

A repo that can never push (no remote configured) had every retained checkout probed with a
doomed commit-and-push cycle every ten minutes, each pass printing the same "kept the
worktree" line per checkout for the life of the daemon. The remote's existence is asked once
per project — the answer cannot change between two rows of the same sweep — and each kept
checkout's line is said once: on the pass that first keeps it, again only if it reappears
after a removal or the daemon restarts, which is the boot-time accounting a retained checkout
deserves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
@brillout

Copy link
Copy Markdown
Member Author

Merge conflict is being fixed.

@suleimansh I'll let you merge if you think this is good.

…less-window bullet

The one conflict was auto-pm.SPEC.md's TLDR, where #1592's new bullet (a settled-empty
agent's ticket lock is released by the sweep) and this branch's ticketless hand-off window
bullet extended the same list. Both stand: the release continues the pushed-lock story, the
window continues the ticketless one. daemon-services.ts auto-merged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
@suleimansh suleimansh self-assigned this Aug 18, 2026
@suleimansh
suleimansh self-requested a review August 18, 2026 21:54
@suleimansh

Copy link
Copy Markdown
Member

Reviewed all seven commits — spot-checks plus a high-effort review pass. The nine directions are implemented as directed, and the deletions all trace clean: every removed surface verifiably had no remaining consumer, and the removed guards were unreachable since D3 made the context accessor throw.

Four things worth fixing before this merges, all small, all in the keep-checkout half:

  1. The handoff: local guard fails open on an unreadable meta (worktrees.ts:149). findAgent(...).catch(() => undefined) collapses a read failure into "no meta", and meta?.handoff && !meta.handoff.push then falls through to pushAgentBranch — publishing the exact branch the rung refuses, then deleting the checkout. Distinguish "meta read fine but has no handoff" (boot death — push, as intended) from "meta could not be read" (keep, retry next sweep).
  2. commitPendingWork runs before the publish-nothing refusal (worktrees.ts:134). A kept handoff: local checkout is now a place someone works — and every 10-minute sweep pass commits their WIP as "[The Framework] uncommitted changes" before reaching the refusal. Move the meta check above the commit.
  3. The shutdown re-check leaks the workspace it just allocated (daemon-runtime.ts:748). Between the entry check and the pre-spawn re-check, allocateWorkspace created a worktree + branch; the refusal removes only the spec. The orphan has no agent.json, so the next boot's sweep — with finding 1's fail-open — pushes an empty tf-agent-* junk branch to origin to reclaim it. Move the re-check above allocation, or tear the workspace down on refusal.
  4. A child that spawns and dies before consuming its spec leaks it (cli.ts:1357 + the daemon exit handler). Cleanup fires only on the spawn error event; a boot crash leaves framework-session-*/session.json — full prompt plus any device token — on disk per attempt. removeAgentSpec is idempotent, so also calling it on nonzero exit is a one-liner per site.

Now or as follow-ups, your call:

  • removeAgentSpec trusts the framework-session- name prefix as ownership — a user's own directory with that name gets rm -rf'd whole (agent-spec.ts:66); checking the parent is the configured spec dir closes it.
  • The kept-checkout announce-dedup keys on agentId only, so a changed keep-reason (say you add a remote and pushes start failing on auth) never logs (merged-worktrees.ts:147). Key on id+reason.
  • Kept handoff: local checkouts re-pay the full probe-and-push cycle every tick (merged-worktrees.ts:86), and the one-boolean handoff read goes through findAgent's full archive scan (worktrees.ts:149) — the batching this PR gave the no-remote case fits both.
  • repoHasRemote duplicates the inline probe in agent-handoff.ts:277 — point the latter at the new helper.
  • FEATURES-SPEC.md rows 77/97 don't reflect the permanent-keep behavior; AGENTS.md wants the feature list current.

@suleimansh
suleimansh removed their request for review August 18, 2026 23:31
claude added 3 commits August 18, 2026 23:45
…he way

Two holes in the publish-nothing keep, both from the review. The armed-handoff read collapsed
"the record could not be read" into "no record", and no record falls through to the push — so
exactly the unreadable case published the branch the rung refuses. The read is strict now: the
live agent.json, else the archived record, where ENOENT means a boot death (push, the
recoverable default) and anything else refuses with the error, retried by a later pass. The
store's forgiving list reads stay forgiving; this is the one read where absence and failure
mean opposite things.

And commitPendingWork ran before the refusal, so every ten-minute sweep pass grabbed whatever
someone had half-typed in a kept checkout as "[The Framework] uncommitted changes". The handoff
decides first now, and a publish-nothing checkout goes only from a clean tree on a pushed tip —
read via the new worktreeClean, the status probe without the commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
…spec all go back

The shutdown re-check refused past an allocation it never returned: between onStart's entry
check and the pre-spawn re-check, a worktree and branch were created, and the refusal removed
only the spec. The orphan had no agent.json, so the next boot's sweep would have reclaimed it
by pushing an empty junk branch. The refusal now takes the fresh worktree and branch back too —
a continuation's checkout is the agent's own and stays.

A child that spawned and died before reading its spec left the prompt (and any device token) on
disk: cleanup fired only on the spawn `error` event. Both spawners now also remove the spec on
exit, which is a no-op after a consumed one.

And removeAgentSpec trusted the directory name alone as ownership — a user's own directory that
happened to carry the mkdtemp prefix was removed whole. Ownership is now the name AND sitting
directly in the configured spec home; anything else loses only the file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj
…, and the feature rows tell the truth

Three smaller review items. The announce-dedup keyed on the checkout alone, so a changed keep
reason — a remote added, pushes now failing on auth — never logged; it keys on checkout and
reason now, so a changed state is said again while an unchanged one stays quiet. The handoff
read's inline remote probe becomes a repoHasRemote call, one implementation for one question.
And FEATURES-SPEC rows 77/97 still described the pre-E5 retention story; they now say what
holds — a checkout stays until its work is on the remote, a publish-nothing agent's until you
publish or delete it, and reclaiming never publishes what the agent refused to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148SehcEqApqZTvG5wPzJoj

Copy link
Copy Markdown
Member Author

All four blockers fixed and pushed — three commits — plus four of the five your-call items; one deferred, named below.

1. Fail-open meta read — the read is strict now, and it had to leave findAgent: the store's list reads are forgiving at every level (down to nodeFs.readdir returning [] on any error), so "could not be read" was structurally invisible through them. removeProjectWorktree reads the live agent.json, else the archived record, directly: ENOENT is a boot death and keeps the recoverable default; any other read or parse failure refuses with the error and a later pass retries. Test writes a corrupt agent.json and pins keep-plus-nothing-pushed.

2. Commit before the refusal — the handoff decides first now, and a publish-nothing checkout goes only from a clean tree on a pushed tip (a new worktreeClean — the status probe without the commit). The keep test now also pins that the dirty edit stays uncommitted and the branch gains no commit. This closes the wider hole too: previously a local-handoff checkout with a hand-pushed tip plus new WIP would have been committed and pushed by the old commit-first ordering.

3. Leaked workspace on shutdown refusal — kept the late re-check (it covers the awaited allocation+spec span the entry check can't) and made the refusal take everything back: spec, fresh worktree, and its branch. A continuation's checkout is the agent's own and stays. Test lands the stop through the preflight seam — deterministic, no race — and pins no branches/ entry, no tf-agent-* branch, empty spec home.

4. Spec left by a child that died unread — both spawners also remove the spec on exit now, a no-op after a consumed one. Tested against the dying stub with a scoped spec home.

Your-call items: took the removeAgentSpec ownership check (prefix AND directly-in-the-spec-home; a same-named user directory elsewhere loses only the file — tested), the reason-keyed announce dedup (changed reason logs again — tested), the repoHasRemote reuse in the handoff read, and the FEATURES-SPEC rows (77 also predated E5 — it still said failed/stopped keeps for inspection). Deferred: the batching/perf item — skipping the probe cycle for known-local checkouts wants the sweep to read metas before offering rows, which reshapes removeMergedWorktrees's seams; cheap once #1582's queue work touches that area anyway, and the announce-dedup already removed the visible symptom.

Verified on the head: both typechecks, a full build, 1483 node / 771 dashboard (81 files).


Generated by Claude Code

@suleimansh
suleimansh merged commit 59fa356 into main Aug 19, 2026
2 checks passed
@suleimansh
suleimansh deleted the claude/pr-1536-refactor-review-gs46xm branch August 19, 2026 00:27
suleimansh added a commit that referenced this pull request Aug 19, 2026
…ch funnel (#1582)

Two textual conflicts: worktrees.ts' import line (union — their readFile for the strict meta
read, our sep for the data-root prefix), and daemon-runtime.SPEC.md (their shutdown-refusal
bullet kept; the archive-committer sentence stays deleted — this branch removes that service).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
suleimansh added a commit that referenced this pull request Aug 19, 2026
#1594)

The merge crossed two changes: #1594's lifecycle test keeps the handoff-local checkout and
reads the archived replay right after the worktree shows on the Remove list — which is true
the moment the meta flips done, before teardown runs — while #1582 made the archive a
data-branch git cycle instead of a file copy. The read now waits for the journal the way the
rest of the story waits for its states; the product surface was already fine (the dashboard
polls).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
suleimansh added a commit that referenced this pull request Aug 19, 2026
* The the-framework_data branch: one home for everything TF writes (#1582)

The core module only, consumers follow: DATA_BRANCH checked out as a worktree at
.the-framework/branches/the-framework_data (adopted from origin, or born parentless off the
empty tree), a root tickets symlink into it, and one serialized write cycle — sync, apply,
commit, push — with the op re-run against origin's state when a push loses a race, and
stranded local commits carried out by the next cycle. Eager pull = the same cycle with a
no-op, so it also drains what a network failure left behind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* The queue and the tickets move to the data branch; the daemon is their one local writer (#1582)

Readers: findTodoBacklog/nextQueuedTicket/readDocs' backlog read off the branch via
readDataFile (checkout, local ref, then origin — so a project checkout, an agent worktree,
and a cloud clone all read the same queue); cli's (fix #N) ticket read does too. Writers:
appendTodoEntry/appendFlatTodoEntry are funneled withDataBranch cycles (an agent worktree
resolves its project root via the git common dir); ticket locks acquire/release are funneled
ops — the old commit/push/default-branch machinery in ticket-locks.ts goes, and a re-run op
recognizes its own claims. The backlog loop's check-off is the framework's own funneled
write now, not an agent file edit (the loop stalls only when the write cannot land, retried
inline). queue-promote.ts is deleted whole: a pinned drain's entry is checked off by the
daemon at settle, once the run's epilogue reports the work published — the same reported
ending the #1583 lock release keys off, so auto-pm now holds any entry-carrying agent
through the end-before-handoff gap. The daemon clock gains an eager 'data sync' pull, so
every machine converges on what others pushed (#1577/#1397 die structurally when the
remaining writers move in the follow-up commits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Session archives live on the data branch, committed and pushed at settle (#1582)

The lasting copy moves from main's .the-framework/<user>/agents/ to agents/<user>/ at the
data branch's root: teardown funnels archiveWorktreeAgent through withDataBranch, so every
archive is a pushed commit the moment the session settles — no debounce, no flush pass, no
human. That deletes agent-commit.ts whole (the committer service, its clock job, and the
shutdown flushAgents phase existed to commit archives a human would otherwise have to
notice). Readers (listAgents/findArchive and friends) look at the data checkout first, then
the transient .the-framework/agents/ a worktree-less agent still writes; the pre-#1582
<user>/agents location on main is not read — hand-run migration moves the existing history.
Delete removes a data-branch record through the funnel too, so the deletion is itself a
pushed commit. .the-framework/.gitignore collapses to 'ignore it all' (nothing under it is
committed on main any more), and ensureArchiveIgnored goes with the rules it repaired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Prompts learn the data branch: one protocol section, every data write through it (#1582)

New 'The data branch' section rides the system channel beside the two format specs: where
tickets/ and TODO_AGENTS.md live, how to read them from any checkout (git show), and the
fetch → detached worktree → commit → push → rebase-on-reject write cycle that works the same
from a local worktree and a cloud clone. The presets that touch framework data (drain,
plan-tickets, both triages, both suggests, update-tickets, maintenance, market-research), the
two in-code pins in auto-pm.ts, and the system/on-before-mergeable TODO_FILE macros all point
at it. The pinned drain contract changes shape: the agent no longer checks its entry off (the
daemon retires it at settle) and the ticket/plan/lock cleanup is a data-branch commit instead
of riding the PR; a plan is now a data write, not a PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* SPECs, feature list, and changeset catch up with the data branch (#1582)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* A drain claim survives a missing tickets/ dir, and a failed lock cycle says why (#1582)

Live smoke found the pair: retiring the last ticket removes tickets/ itself (git keeps no
empty dirs, and the branch is born without it), and acquireTicketLocks' default write was a
bare writeFile — so the next ticket-linked drain claim died on ENOENT inside the funnel,
acquire resolved [], and the sweep stood the batch down with 'every entry in this batch
links a ticket another agent already claimed', which was wrong on every word. The default
write now creates parents, the way data-branch.ts' own default already does, and a cycle
that fails before any lock lands is logged with its real error instead of only when part of
the batch had already been claimed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* The archived-replay read waits for teardown's data-branch commit (#1582/#1594)

The merge crossed two changes: #1594's lifecycle test keeps the handoff-local checkout and
reads the archived replay right after the worktree shows on the Remove list — which is true
the moment the meta flips done, before teardown runs — while #1582 made the archive a
data-branch git cycle instead of a file copy. The read now waits for the journal the way the
rest of the story waits for its states; the product surface was already fine (the dashboard
polls).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* The drain retire loop is proven end to end at the services layer (#1582)

Three integration tests on the real-git fixture, straight from the live smoke: a run whose
archived record reports the hand-off gets its entry checked off as a data-branch commit (and
only that entry); a run whose hand-off failed leaves its entry open — unpublished work is
not retired; and a drain still claims an entry whose ticket file is gone, recreating
tickets/ on the way — the batch-level proof of the missing-parent fix, which previously
stood the sweep down with the wrong message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* The data branch is named tf-data, and the presets stop repeating where data lives

Review follow-ups on #1595:

- the-framework_data -> tf-data everywhere: #1581 made tf- the prefix for
  every framework-minted branch, and the data branch was the one newcomer
  that ignored it. The name only exists on this PR's branch, so it is a
  pure rename.
- The per-preset "(on the data branch — see ...)" reminders are deleted.
  The system prompt already carries the fact twice — the tickets/TODO
  context bullets and the "The data branch" protocol section — so the
  presets keep only genuine behavior: drain's check-off-when-published
  timing and update_tickets' whole-update-as-one-commit rule.

* The data-branch protocol trusts the agent with git

Review follow-ups on #1595: the six-step command recipe goes — agents get
the contract instead (commit on tf-data pushed immediately, rebase on a
rejected push, never via the checkout or the PR). The no-remote line
stops being a standalone exception: the write is the commit, push is just
propagation, so it folds into the write sentence. 24 lines down to 7.

* The no-remote case stops being a supported mode of the data protocol

Per the review round: the protocol says commit + push, no special case. A
project without a remote is an erroneous state for the daemon to surface,
not one the prompt accommodates.

* A lock claim that conflicts is a lost race, and the prompt says so

Verified the arbitration mechanics on a scratch remote: two agents claiming
the same ticket race on the push — the loser's rebase hits an add/add
conflict on the .lock.md. The lock section now tells the agent what that
conflict means: back off, never resolve it in your own favor. Claims of
different tickets rebase cleanly and both land.

* The root tickets symlink is hidden from git the moment it is made

An uncommitted symlink at the repo root rides any sweeping git add -A onto
a code branch — the leak the drain E2E surfaced. The info/exclude mechanism
moves out of worktree-deps into git-exclude.ts so both callers share it;
the rule is root-anchored and only written when the link is ours, so a
user's own tickets path stays visible. The sibling leak (auto-onboard
skipping installProject) is #1600.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* The tickets exclude spares the data checkout's own cargo

The repo-level exclude speaks for every worktree at once — including the
tf-data checkout, whose root holds the real tickets/ the branch exists to
carry. A bare /tickets rule swallowed it: data commits silently dropped
every ticket file (CI's control.test caught it). The rule becomes a pair,
/tickets then !/tickets/ — a trailing slash never matches a symlink, so
the root link stays hidden while the directory keeps committing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

3 participants