The post-merge review's nine findings, fixed as directed - #1594
Conversation
…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
|
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
|
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:
Now or as follow-ups, your call:
|
…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
|
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 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 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 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 Verified on the head: both typechecks, a full build, 1483 node / 771 dashboard (81 files). Generated by Claude Code |
…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>
#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 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>
Implements every direction from the review round on #1536, one commit per instruction, in the order given.
What each commit does
handoff: localholds —removeProjectWorktreekeeps 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.framework "<what to build>"line D4 orphaned is gone; a test pins the absence.readAgentSpecconsumes 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.daemon-services.tsline claiming the pinned entry "outlives the agent's local process" is replaced with what holds (the pushed drain lock for tickets), andauto-pm.SPEC.mdnow owns the ticketless hand-off window as accepted-not-closed, with the rationale on record.onStartrefuses oncestopAgentshas 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.preset:/event:(keys, resolution, echo, chips — absence pinned beside the other retired spellings), the six transport-less option fields plus themodesevent kind andOPEN_LOOP_MODES, the unreachable "not enabled on this server" guards, and thecontrol.SPEC.mdsentence that contradictedcontext.SPEC.md. The surviving option docs stop naming D4's flags.hasRemoteasked 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
handoff: localsession'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.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 inconfig.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