Development record
several-headless-agents-work-one
Summary
The design record for itd-2610170634394993, planned on the product thinker's sign-off of 2026-10-23 (dcn-2610230757556283). It builds on reservations (itd-2609091034175565), announcements (itd-2609151838312703), the load gate, and the stall slice (#1101). It is bound by adr-2610222026262980, accepted at that sign-off and stated as invariant 25. It was drafted on 2026-10-22 and revised after the Opus and Sonnet review round 1. Every step is test-driven: each test is watched fail first.
Seven parts, each landing as its own pull request in this order:
- The pass.
land,build,drainandstatuscompare what the records on the remote say should happen with what is happening, and act on the difference before they act. No process stays alive. Every deadline is derived when the records are read (dcn-2610180720366282). - Dependencies. A
Depends-On: #Nline in the pull request body, whichlandwrites from the agent's declaration (dcn-2610180720362692). A dependent branch is not queued while #N is open. Once #N merges, the branch is rebuilt by merging the default branch in, then preflighted and queued (dcn-2610180720369860). A dependency that was closed unmerged, cannot be resolved, or forms a cycle is refused with its reason. The re-submitting agent is given the pull requests that merged since its merge base and touch its footprint (dcn-2610180720407459). - Fencing and version. A push of the work branch and a tag, made by abcd under a claim, travel in one
git push --atomictogether with a renewal of the claim branch. If the claim's tip moved, the renewal is not a fast-forward and the forge refuses the whole push. A queue entry cannot share a push: abcd re-reads the claim tip immediately before it enqueues and passes the pull request's head asexpectedHeadOid(GitHub's enqueue mutation), which narrows the window to the one call. That window is stated in the ADR, not claimed closed. A take-over waits for expiry plus a fixed delay. Coordination records gainabcd-coord-version: 1, and a reader refuses a higher version, naming both (dcn-2610180720415626, ADR rule 3). The atomic push is probed first (dcn-2610180720397970 left it to planning): step 3 opens with a live probe on a scratch repository. - Budgets. The machine budget is the load gate's slots, weighted per job (dcn-2610180720376153). The model budget is one pool per computer, which every session on that computer draws on, sized by the repository's setting and itd-2610100704270254's class weights (dcn-2610222025116937, its reach ruled per computer in dcn-2610230757556283). It is kept on the computer, as the load gate's slots are, with no pushes; a session on another computer or account draws on that computer's own pool. Every wait on either budget has a deadline and ends in a named refusal.
- The central view in
abcd status(dcn-2610180723342596). One line per working copy (dcn-2610180720397774), keyed on the reservation's copy key. The line is fixed-form: a stage, the ids it waits on, and a state of working, waiting (with its next look) or stopped (flagged). It is carried on the person's existingabcd-statusbranch (dcn-2610180720402007), pushed only when it changes and only after the checklist is written, and carries its own expiry (dcn-2610180720402361). It reads live, then not seen for a stated time, then takeable, then gone. Its state follows the renewal, the process and the last commit, with the agent's own words as a hint. - Own credential (C3, dcn-2610222025116835). abcd's GitHub App issues each build or drain lane abcd launches a token of its own: a user access token from the device flow (dcn-2610222048069284). The person authorises the App once in the browser by entering a code; the device flow needs only the client id, and its token refreshes without the client secret (docs.github.com, "Generating a user access token for a GitHub App" and "Refreshing user access tokens"). The refresh token (six months, replaced at each refresh) is the only issuing secret. It lives on the person's computer in the system's credential store, outside every session and on the fence's read-deny list. Each launch is handed an eight-hour access token through abcd's credential helper, the expiry the product thinker kept at sign-off; a container or a separate account on the same computer gets a fresh one before the old lapses, and a cloud session, which cannot be reached after launch, stops pushing when its token lapses and queues what it still has to push as owed work. The lane uses the token only through abcd's own git credential helper and forge calls, never
GH_TOKENorGITHUB_TOKEN. Commits stay authored by the person. The App is installed on the one repository, which bounds the token's reach. Branch deletion is refused by rulesets on the protected prefixes only (dcn-2610180720415625). The installation token stays the fallback behind theforgecredseam. - The login check before abcd launches a build or drain lane (C1, dcn-2610222030477927, narrowed by dcn-2610230710469123). It covers only the lanes abcd launches on its own; the person's own sessions, away mode included, keep their login, and a drain the person starts and watches may count as attended. It runs before the lane's token is issued and reads only signals that carry no secret:
gh auth status --hostname <host>, by exit code, with its output discarded;- whether
GH_TOKEN,GITHUB_TOKEN,GH_ENTERPRISE_TOKENorGITHUB_ENTERPRISE_TOKENis set, never its value; - whether gh's hosts file or
~/.git-credentialshas a line for the host, matched on the host alone.
It never asks a credential helper, so a login kept only in a keychain, or an SSH key, is out of its sight. The refusal names each place it found a login and says what it cannot see. It has a deadline of 10 seconds, for gh only.
The person's web step (C3). The person registers abcd's GitHub App once in the browser, through the manifest flow, which abcd prepares and links (dcn-2610180720389628), installs it on the repository, and authorises it by entering the device code. Only the person can take this step. Following the principle instructions-to-a-person-are-verified-and-linked, the exact URL is written when the step is put to the person, checked against the docs on that day (docs.github.com, "Registering a GitHub App from a manifest").
Footprint
- packages: internal/core/coord, internal/core/forgecred, internal/core/landing, internal/core/claim, internal/core/implement, internal/core/statusline, internal/surface/cli, commands/land.md, commands/build.md, commands/drain.md, commands/status.md, .abcd/development/brief/04-surfaces, .abcd/development/brief/05-internals, .abcd/development/brief/02-constraints/03-invariants.md, .abcd/work/rulesets
- tests: internal/core/coord, internal/core/forgecred, internal/core/landing, internal/core/claim, internal/core/implement, internal/core/statusline, internal/surface/cli
Steps
- The
coordpackage, as a pure function It reads the records and returns the decisions. Table tests: the same records give the same decisions, and a pass stopped mid-way and re-run from the records reaches the same decisions (technical criterion 2).- packages: internal/core/coord
- tests: internal/core/coord
Depends-OnParse it, and refuse a closed-unmerged, unresolvable or cyclic dependency with its reason (story 2).landdoes not queue while the dependency is open, then merges it in, preflights and queues (story 1); this is a live test against a scratch repository on the forge, per real-tests-over-fakes. The re-submit summary (technical criterion 5).- packages: internal/core/coord, internal/core/landing, internal/surface/cli
- tests: internal/core/coord, internal/core/landing
- Fencing and version
Opens with the live probe of
git push --atomicwith a claim renewal on a scratch repository. A queue or tag under a moved tip is refused; a take-over waits for expiry plus the delay (story 3); the version field, and the refusal of a newer one (technical criterion 3).- packages: internal/core/claim, internal/core/landing, internal/core/coord
- tests: internal/core/claim, internal/core/landing
- Budgets
Weighted load slots, and the per-computer model pool kept beside them; every wait has its deadline and named refusal (technical criterion 1 and story 10).
- packages: internal/core/implement
- tests: internal/core/implement
- Central view
The line format and when it is pushed (technical criterion 4), the expiry ladder, and that the state follows the machine (stories 4 and 5).
- packages: internal/core/statusline, internal/surface/cli
- tests: internal/core/statusline, internal/surface/cli
- Forge credential
The device-flow token is issued with the narrow permissions and its eight-hour expiry and reaches git and the forge only through abcd's helper (story 9). A settings change, a deletion on a protected prefix, and a request to a repository outside the token's reach are each refused by the forge (stories 6 and 7); this is a live test with a test App on a scratch repository.
- packages: internal/core/forgecred
- tests: internal/core/forgecred
- Login check
Each signal is detected and no value is ever read, printed or logged: a test plants a token in each place and greps every output and log for it, and a test helper that records being called shows no credential helper starts. The refusal names each place, and a clean environment passes (story 8); this is a live test in a container. The person's own session is not checked.
- packages: internal/core/forgecred, internal/surface/cli
- tests: internal/core/forgecred
- Brief and docs
Invariant 25 is rewritten from the rule the build must meet to the present tense, naming the tests that hold each part; the command pages and generated docs follow.
- packages: .abcd/development/brief/02-constraints/03-invariants.md, commands/land.md, commands/build.md, commands/drain.md, commands/status.md
- tests: internal/surface/cli
Size
About 12 to 15 days of agent work. Steps 2 and 6 carry the live forge tests and are the long poles.
Frontmatter
| id | spc-2610230758581403 |
|---|---|
| type | spec |
| lifecycle | open |
.abcd/development/specs/open/spc-2610230758581403-several-headless-agents-work-one.md
open on GitHub ↗ · commit history ↗
References1
- itd-2610170634394993Headless agents in parallel coordinate through the shared remote, not conversationimplements
mentions23
- adr-2610222026262980Coordination between headless agents is bounded by deadlines, fenced by the claim's tip, version-checked, and done under each session's own credential
- dcn-26101807203626922026-10-18 — A dependency is a Depends-On line in the pull request's body (facilitator-on-delegation, itd-2610170634394993 planning prep): a branch's wait on another change is written as Depends-On: #N, which land writes from the agent's declaration and the pass enforces, and the session's short status line mirrors it as a hint only; a field on the claim is not used, since a reservation is per record and per person while a dependency belongs to a change, and the line's exact spelling is plumbing for the brief (grounds: docs.mergify.com/merge-protections/builtin; Zuul's Depends-On footer; itd-2609091034175565)
- dcn-26101807203662822026-10-18 — The coordination pass runs on demand (facilitator-on-delegation, itd-2610170634394993 planning prep): land, build, drain and status run the pass from the records they have just fetched before they act; there is no timer lease and no process that must stay alive, and every deadline and expiry is derived when the records are read, as a reservation's release already is (grounds: the draft's research note on controllers; kubernetes.io/docs/concepts/architecture/controller; brief invariant 7; itd-2609151838312703 decision 14; itd-2609091034175565 decision 13)
- dcn-26101807203698602026-10-18 — A waiting branch is rebuilt by merging, not restacking (facilitator-on-delegation, itd-2610170634394993 planning prep): once its dependency lands, the dependent branch takes the default branch merged in and is never rewritten, because the merge queue lands by merge so the dependency's commits keep their ids, and a restack would break the reachability of resolved_by stamps (RS002, RS003); a restack is used only when the dependency was squash- or rebase-merged by hand, which the review ruleset allows; each rebuild needs a fresh preflight receipt and so takes a load slot (grounds: .abcd/work/rulesets/main-protection.json; main-review.json; AGENTS.md RS002 and RS003; git-spice)
- dcn-26101807203761532026-10-18 — The machine budget is the load gate's slots, weighted per job (facilitator-on-delegation, itd-2610170634394993 planning prep): preflights, race runs and every other heavy job draw on one pool, the load gate's existing slots (load.preflights), each job weighted, with a dead holder's slot reclaimed as it is now; no second pool is added (grounds: principles/one-canonical-primitive.md; airflow.apache.org pools; internal/core/implement/load_slots.go)
- dcn-26101807203896282026-10-18 — Registering the App takes one step in the browser (facilitator-on-delegation, itd-2610170634394993 planning prep): creating a GitHub App needs the person on the website once, through the manifest flow, so the draft states it as a step only the person can take, as the checklist draft already lists it, and never promises setup without manual steps (grounds: docs.github.com registering-a-github-app-from-a-manifest; itd-2610170638184486)
- dcn-26101807203977742026-10-18 — One central-view line per working copy (facilitator-on-delegation, itd-2610170634394993 planning prep): a session in the view is a working copy, keyed on the reservation's existing copy key, since the checkout is the unit of isolation (grounds: itd-2609091034175565 decision 33; AGENTS.md concurrent sessions; internal/core/claim/message.go)
- dcn-26101807203979702026-10-18 — The claim's generation number is its tip commit (facilitator-on-delegation, itd-2610170634394993 planning prep): every act goes through a fast-forward push, which is already a compare-and-swap, and a take-over moves the tip, so no separate counter is added; the forge enforces the fence only on a push, so for queueing and tagging the criterion says abcd refuses the act, and whether one atomic push can carry the work branch and the renewal together is left to probe at planning (grounds: internal/core/claim/claim.go; internal/core/implement/loop/reservation.go; Chubby section 2.4; git-scm.com/docs/git-push --atomic)
- dcn-26101807204020072026-10-18 — A session's state line is fixed-form (facilitator-on-delegation, itd-2610170634394993 planning prep): the line's state is a stage from a fixed list plus the record ids it waits on, carried on the person's existing abcd-status branch, never free text, and no branch per session is added (grounds: itd-2609151838312703 C3 and decisions 1 and 9; internal/core/claim/stage.go)
- dcn-26101807204023612026-10-18 — A session's line carries its own expiry (facilitator-on-delegation, itd-2610170634394993 planning prep): a line is renewed and expires on its own, counted from its first fetch, so a session that holds no reservation still shows as live or gone (grounds: itd-2609151838312703 C9; kubernetes.io node leases)
- dcn-26101807204074592026-10-18 — A re-submitting agent is given the merged pull requests that touch its footprint (facilitator-on-delegation, itd-2610170634394993 planning prep): the draft's criterion asking for a summary of what landed becomes the list of pull requests merged since the branch's merge base that touch its footprint, which a test can check (grounds: the draft's research note on multi-agent coding, Xia et al.)
- dcn-26101807204156252026-10-18 — Branch deletion is refused by rulesets, not by the token (facilitator-on-delegation, itd-2610170634394993 planning prep): a token able to push can also delete branches, so the criterion that a session cannot delete a branch it did not create is narrowed to the protected prefixes the rulesets already guard, and other branches stay deletable (grounds: docs.github.com permissions-required-for-github-apps; brief invariant 16; .abcd/work/rulesets/reservations-protection.json)
- dcn-26101807204156262026-10-18 — Refusing a newer coordination record starts with the release that adds a version (facilitator-on-delegation, itd-2610170634394993 planning prep): readers already shipped ignore an unknown trailer, so the criterion's refusal can hold only from the release that adds a version field, and the criterion says so (grounds: internal/core/claim/message.go; kubernetes.io version-skew-policy)
- dcn-26101807233425962026-10-18 — The central view is abcd status (facilitator-on-delegation, itd-2610170634394993 planning prep): the view of what every session holds, does and waits on is shown by abcd status, which the product thinker named the people's view of what everyone else is doing; abcd peers keeps the meaning they reserved for it, the sessions running now seen locally and later over a private network, which this intent's decision 4 dropped (grounds: itd-2609151838312703 decision 21; the draft's decisions 3 and 4)
- dcn-26102220251168352026-10-22 — Agents' own limited access ships in the same release as the coordination (product thinker's ruling of 2026-10-22, itd-2610170634394993 C3): autonomous sessions get short-lived tokens for one job from abcd's GitHub App from the first release of this intent, not in a later piece; the person registers the App once in the browser before that release is usable, a step only they can take. This is what makes C1 work: a run kept away from the person's login needs those tokens to push
- dcn-26102220251169372026-10-22 — The model budget is shared across sessions, not a fixed count for each abcd run (product thinker's ruling of 2026-10-22, itd-2610170634394993 C-confirm; a typed reversal: reverses dcn-2610180720379757 and the product thinker's 2026-10-10 answer 'each abcd run' in itd-2610100704270254): the sub-agent budget the repository sets is one pool that every abcd session draws on, so the draft's press release keeps 'a share of the model budget' and is not narrowed; the product thinker accepted that one busy session can slow the others. dcn-2610180720379757's narrowing of the press release and the model-budget scope condition is withdrawn
- dcn-26102220304779272026-10-22 — abcd looks for the person's forge login before it starts an unattended run, and refuses to start where it finds one (facilitator, on state-of-the-art grounds, applying the product thinker's 2026-10-22 ruling C1 on itd-2610170634394993): before an away-mode run, build or drain starts unattended, and before abcd issues the session a token of its own, abcd checks only signals that carry no secret: the forge CLI's login status for the host by exit code alone, with its output discarded (the CLI prints no token unless asked with --show-token); whether the token variables the forge CLI and git read (GH_TOKEN, GITHUB_TOKEN and their enterprise forms) are set, never their values; and whether the stored-credential files hold an entry for the host (the forge CLI's hosts file and ~/.git-credentials), by the host line alone. It never asks a git credential helper for the host, since a helper answers with the token itself and on macOS may raise a keychain prompt that hangs an unattended start; so a login kept only in a keychain is out of the check's sight, and the refusal says so rather than claiming the machine is clean (brief invariant 16). It refuses to start, naming each place it found a login, and never removes or hides one; the guarantee rests on where the run is started (a cloud session, a container or a separate user account), which abcd states as the person's setup step. The session's own token, issued after the check, never travels in those variables (grounds: cli.github.com/manual/gh_auth_status; cli.github.com/manual/gh_help_environment; git-scm.com/docs/gitcredentials; adr-2610222026262980 rule 4)
- dcn-26102220480692842026-10-22 — A session's own token is a user access token from the device flow, and its refresh token stays on the person's computer (facilitator on the product thinker's delegation of 2026-10-14 for technical choices grounded in the state of the art, itd-2610170634394993; keeps dcn-2610180723332655, the person's name marked as made through abcd, which the product thinker accepted in the 2026-10-22 C3 question): the person authorises abcd's GitHub App once through the device flow (a code entered in the browser), which needs only the App's client id, so no client secret is shipped to any machine, and a device-flow token is refreshed without the secret; the refresh token, which lasts six months and is replaced at each refresh, is the only issuing secret, and it is kept on the person's computer in the system's credential store (the macOS keychain, or a file only the person's account can read under ~/.abcd.noindex on Linux), outside every session and on the fence's read-deny list; at each unattended launch abcd on that computer refreshes and hands the session only the eight-hour access token, through abcd's own credential channel; a container or a separate user account on the same computer is handed a fresh token before the old one lapses, and a cloud session cannot be reached after launch, so its pushes stop when its token lapses and its remaining pushes are queued as owed work for the person's return or a relaunch, which the walk states; the token reaches only the repositories the App is installed on, so abcd asks for the App to be installed on the one repository; the installation token stays the fallback behind the forgecred seam, with the commit author still the person, should the device flow be withdrawn (grounds: docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app; docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens; docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/best-practices-for-creating-a-github-app)
- dcn-26102307104691232026-10-23 — The product thinker's ruling of 2026-10-23, restated, narrowing their 2026-10-22 C1 answer (dcn-2610222025104969): only the helpers abcd launches on its own, the build and drain lanes, must run away from the person's GitHub login. The person's own sessions working while they are away keep running on their own computer under their login, and a drain the person starts and watches may count as attended. It settles choice 1 of the coordination intent (itd-2610170634394993) and choice 2 of the terminal-drain intent (itd-2610222215485265), and the login check before an unattended run (dcn-2610222030477927) covers abcd's own launches only.
- dcn-26102307575562832026-10-23 — The product thinker's walk and sign-off of itd-2610170634394993 on 2026-10-23 (about 09:50 BST, in abcd-ae's session), restated. The shared model budget is per computer: sessions on one computer share one pool, and a session on another computer or account draws on that computer's own pool, which narrows the draft's scope condition that the budget spans every account of an organisation. The ten person-visible stories are kept, the budget story in its per-computer form. The eight-hour key of a cloud run is kept: asked why eight, the product thinker was told that GitHub sets eight hours for an app's user access token and that its renewal stays on the person's computer, and kept it. The technical list is accepted in full. The press release, the mechanism and its grounds, the scope conditions and the reason for doing it now are signed off as written. Choice 1 had been ruled earlier (dcn-2610230710469123). The intent is planned on this sign-off.
- itd-2609091034175565Before an agent starts on an item, it reserves it on the shared remote, and everyone who works on the project sees the reservation
- itd-2609151838312703Sessions under any account, on any machine or in the cloud announce what they are doing through short fixed-form announcements on the shared remote, a person reads everyone's status with one command, and what is really a hold stays a reservation
- itd-2610100704270254An abcd run sets how many sub-agents it keeps alive by the models they run on