Development record
Drains run unattended and visibly
Press Release
A drain runs unattended on a hosted session or a plain terminal for a day. Its judgement of each remedy routes through the same model routing the build roles use, so no person needs to write the answer by hand, and a hosted session that restarts takes its own reservations back with one flag rather than waiting for them to lapse. The pace and sub-agents a drain was started with reach every lane it opens and can be changed while it runs, an issue it handed back is looked at again once its record changes, a lost connection to origin waits like any other outage, and abcd peers shows each drain lane's stage so every person can see what the other accounts' drains are doing.
Why This Matters
The product thinker wants drains to run on several accounts through a day without a person minding each one. A person on any of those accounts should be able to see what the others are doing. Today a drain needs a person or a host agent at every judgement: in a plain terminal someone writes the JSON answer, its sha256 digest included, by hand, and a hosted session that restarts loses its local tier, so its new copy reads its own reservations as another copy's, and the advice names a flag the drain does not take (iss-2610161015422306). The pace and sub-agents a drain was started with reach only its first lane (iss-2610161015417715). A drain that has run for a day cannot be re-paced, and keeps the issues it handed back excluded even after their records change (iss-2610161015457468). A lost connection to origin refuses the move instead of waiting (iss-2610161015448977). Each reservation act asks the forge who the person is again (iss-2610161015433495). And abcd peers lists each account's reservations but nothing of what a drain lane is doing, so one person cannot tell a working lane from a stuck one on someone else's account (iss-2610161015434746).
These matter less than the faults that stop a drain outright, which the bundle's other two members close. They decide whether a day's drain needs minding.
Mechanism
Facilitator-on-delegation, 2026-10-16: the product thinker delegated the planning decisions of this bundle; this claim is the facilitator's, grounded in the evidence of the issues it closes.
We expect a drain to run unattended for a day, because each step that today needs a person either goes through a route abcd already has or becomes a wait that resumes by itself. The judgement goes through the model routing the build roles already use, and abcd computes the digest itself. A lost connection becomes the outage wait the loop already has, and a restart is recovered with the take-over the build already offers. We expect it to be visible, because a lane's stage rides on the reservation every account already fetches. It would be shown wrong if a drain started in a plain terminal with a routed judge needs a person's answer before it ends, or if a person on one account cannot name the stage of another account's drain lane from abcd peers.
Scope Conditions
- Holds where the person has routed the drain's judge to a runner on their own machine. With no route, the judgement goes to the host as it does today.
- Holds for a hosted session whose restart keeps the clone's history on origin but not its local tier. Its own reservations are moved to the new copy with
--take-over; work not pushed before the restart is lost. - Holds for the stage names the loop already has. A stage is shown as it was at the reservation's last renewal, so it can be up to half a lease old.
- Holds while
abcd peerslists the reservations. When the people's status view takes that listing over, the same stage and hand-back rows go with it.
Acceptance Criteria
- Given Frank, a DevOps engineer, has routed the drain's judge to a command-line model on Frank's machine, when Frank runs
abcd drainin a plain terminal and it reaches an issue's judgement, then the judgement is asked of that model and its answer taken, with abcd computing the remedy's digest itself, and no person writes the answer. (iss-2610161015422306) - Given Frank's hosted session restarted mid-drain and lost its local tier, when Frank runs
abcd drain --take-over, then the drain moves Frank's own reservations from the lost copy to this one as it reaches their issues. (iss-2610161015422306) - Given a drain whose reservation is held by the same person's other copy, when the drain refuses to take it, then the advice names
abcd drain --take-over, a flag the drain accepts. (iss-2610161015422306) - Given Maya started a drain with
--pace 120/30 --sub-agents 4, when the drain opens its second and later lanes, then each lane runs on that pace and sub-agent ceiling, not on what configuration resolves. (iss-2610161015417715) - Given Maya's drain is running, when Maya runs
abcd drain --pace 60/15 --sub-agents 2, then the drain takes and records the new values instead of refusing them. (iss-2610161015457468) - Given a running drain that has taken new values, when it opens its next lane, or its lane in progress reaches its next stage, then that lane runs on the new values. (iss-2610161015457468)
- Given an issue Maya's drain handed back whose record has since changed on origin's default branch, when the drain next moves, then the issue is eligible again and judged afresh, and an old handed-back run for it no longer passes it over. (iss-2610161015457468)
- Given origin does not answer the fetch at the start of a drain move, when the drain moves, then it enters the drain's outage wait and probes again later, as for any other lost connection, instead of refusing the move. (iss-2610161015448977)
- Given a person whose git email is not a GitHub no-reply address, when one drain move makes several reservation acts, then the forge is asked for the person's username once in that move. (iss-2610161015433495)
- Given Carol, a tech lead, on another account from Maya's, when Carol runs
abcd peers, then each of Maya's drain lanes is listed with its stage as of its last renewal. (iss-2610161015434746) - Given Maya's drain handed issues back in the last day, when Carol runs
abcd peers, then those hand-backs are listed with their kind. (iss-2610161015434746)
Decomposition
Run by hand under decompose-before-filing on 2026-10-16, facilitator-on-delegation (dcn-2610161040352440), and graded in the decomposition-calibration note.
| Part | Type | Home | Verdict |
|---|---|---|---|
| The drain's judgement routed to a command-line model, and a restart recovered with take-over (iss-2610161015422306) | capability | this intent, on the runner routing the build roles use | file here |
| Pace and sub-agents carried into every lane and changeable while the drain runs; handed-back issues re-admitted when their records change (iss-2610161015417715, iss-2610161015457468) | capability | this intent | file here |
| A failed origin fetch waits as an outage (iss-2610161015448977); the holder cached per process (iss-2610161015433495) | capability | this intent | file here |
| A drain lane's stage and recent hand-backs shown in abcd peers (iss-2610161015434746) | capability | this intent, riding on the reservation; the people's status view of itd-2609151838312703 inherits the rows when it takes the listing over (iss-2610150726331622) | keep both |
Verdict: FILE-AS-IS, as a member of the drain-many-accounts bundle. It builds on itd-2610161016394967 (the hand-back tombstones it re-admits from, and the per-move fetch its outage wait wraps) and itd-2610161016391855 (the stage a stuck lane reports).
SOTA
Facilitator-on-delegation, 2026-10-16:
- Routing a model-run role. abcd's build roles already go through one runner routing, so a new role reuses it (path 1, adopt the native seam). A hosted model API used directly was not adopted, because the person's runner is the existing route.
- Resuming on another machine. CI systems and hosted agents keep resumable state on the shared remote or in a store the platform documents as surviving. abcd's take-over already moves a reservation between copies, so it is adopted (path 2). State kept outside the checkout was declined under
durable-state-lives-where-the-platform-says-it-survives. - Showing what workers are doing. Job dashboards read status from the record every worker writes. A trailer on the reservation renewal costs no new act, so it is adopted (path 2).
Open Questions
None open; the product thinker delegated this bundle's planning decisions on 2026-10-16, and each is recorded under Decisions. The technical questions the build meets are in the bundle's spec.
Audit Notes
Fidelity review — receipt rcp-cf03d6bdefc1 (verifier abcd:intent-auditor claude-opus-5-5).
Provenance: abcd:intent-auditor@claude-opus-5-5 · rubric_hash sha256:effa65b3e9e88ff29433b443ec2be159522a8b0b71cf1434526514aa61edb13e · prompt_hash sha256:e5fc73c86bafbce8b71e1ec703f5997d44d1df7522d223d4e97d949841fa259d Input attestations: diff:ca99ea28b..80764362b, git diff --binary from the spec's plan commit to its close commit on main@sha256:3dd8bdc15c06e682367717938ba552c6d0ff87892ad2ab67d827b858c61e5b91; commits:this intent's step commits and follow-ups: f87eea869, 7837234d7, d0bd60137, 77bf8def1, ffda8dd52, d7d49ff2f, 764f355de, 6add08b19, 4358e61c8, 1dd3e4a0d, 9d61af9ed, bab97a0ae, d8c42ece7, 058d35c81, 0efecf94c, aae1f72a3, 3e1fa4f06, 385972ec4, d13e39bed, 9f9976a5d, a26850419, 00e95902b, da43b25ba, a1c16e21f; post-close 730aca57b@-; tree:origin/main at 2e03c61e4, read in an audit worktree at eac09d94f, which adds only audit notes; file:line refs below are at that tree@-;
Acceptance rollup: MET 6 · MET_WITH_CONCERNS 5 · NOT_MET 0 · INCONCLUSIVE 0
Per-criterion verdicts:
- ac-1 — MET_WITH_CONCERNS: Routed to the claude runner in the person's own layer, the move that writes the host's request also starts the runner on its own brief, reads its digest-less answer, fills in the remedy digest abcd computed and takes the answer in that move with no --judgement given (tested against a fake claude harness). Concern: the delivery is narrower than 'a command-line model': a route to the opencode runner is skipped with a diagnostic and the judgement stays on the host, and the judge is never started where the checkout's or the temporary directory's real path holds a parenthesis or a comma (iss-2610171043574168, still open), so in those setups a person answers as before. evidence: internal/core/implement/loop/drain_judge.go:127 — "return o.Runners != nil && d.Transcripts != nil && o.Runners.RouteFor(RoleDrainJudge).Runner != runner.Host" evidence: internal/core/implement/loop/drain_judge.go:293 — "a := JudgementAnswer{SchemaVersion: ra.SchemaVersion, Issue: ra.Issue, RemedySHA256: j.RemedySHA256," evidence: internal/core/implement/loop/drain.go:1020 — "if taken, clause, started, err = routedJudgement(repoRoot, iss, v, judging, plan, &st, o, d, now); err != nil {" evidence: internal/core/implement/loop/drain_judge_test.go:57 — "if j.RemedySHA256 != remedyDigest(issueRemedy) {" evidence: internal/surface/cli/drain.go:258 — "o := loop.Options{Roots: &roots, Runners: cfg, TakeOver: takeOver}" evidence: internal/core/runner/config.go:253 — "if role == roleDrainJudge && name == OpenCode {" evidence: .abcd/work/issues/open/iss-2610171043574168-the-routed-drain-judge-cannot-run-for-a.md:15 — "The routed drain judge cannot run for a checkout or temporary directory whose real path holds a parenthesis or a comma"
- ac-2 — MET_WITH_CONCERNS: With --take-over, the move's view lets this person's own other-copy reservation of the issue through and the reservation taken before the judgement takes it over for this copy, the lane opening on it (tested with a GitHub no-reply git identity). Concern, neither tested nor documented: the let-through is keyed on capHolder, the holder read with no network act, which is empty for a person whose username only the forge token can name (git email not a no-reply address and no machine-layer reservation.github_username), so for that person the view still passes every such issue over and nothing is taken over; and only the issue's own reservation is exempted, so a lost-copy reservation whose footprint overlaps another of that copy's live reservations is still passed over, in the view and again by claim's crowding, until one lapses. evidence: internal/core/implement/loop/drain.go:939 — "takeOver = capHolder" evidence: internal/core/implement/loop/drain.go:863 — "capHolder = claim.ReaderHolder(repoRoot, claimOptions(now))" evidence: internal/core/claim/render.go:168 — "h, err := resolveHolder(root, who, o, false)" evidence: internal/core/claim/holder.go:163 — "cannot tell which GitHub user you are without asking GitHub: user.email is not a GitHub no-reply address" evidence: internal/core/claim/crowd.go:257 — "!(takeOver != "" && r.State == StateLive && sameHolder(r.Holder, takeOver))" evidence: internal/core/claim/crowd.go:266 — "if r.State != StateLive || r.Record == id || r.Copy == CopyThis {" evidence: internal/core/implement/loop/drain_takeover_test.go:60 — "if !ok || r.Copy != claim.CopyThis || r.State != claim.StateLive || r.Holder != "octo" {" evidence: internal/core/implement/loop/reservation_test.go:26 — "users.noreply.github.com"
- ac-3 — MET_WITH_CONCERNS: A plain move that passes over an issue held by this person's other copy appends 'to move it to this one, run
abcd drain --take-over', and the drain registers that flag, listed in its help and generated reference (tested). Concern: the advice is gated on the same offline holder, so a person only the forge token names is told only who holds the issue and nothing of the flag; a hold the reservation push itself meets is passed over in claim's own words, 'you are already building ... on another computer ... pass --take-over', notabcd drain --take-over; and under --take-over a pass-over for a footprint overlap repeats the advice although the flag was given. evidence: internal/core/implement/loop/drain.go:946 — "to move it to this one, runabcd drain --take-over" evidence: internal/core/implement/loop/drain.go:945 — "claim.SameHolder(r.Holder, capHolder)" evidence: internal/core/implement/loop/drain_takeover_test.go:41 — "if why := plain.Passed[i].Reason; !strings.Contains(why, "abcd drain --take-over") {" evidence: internal/surface/cli/drain.go:234 — "cmd.Flags().BoolVar(&takeOver, "take-over", false," evidence: internal/core/claim/remote.go:403 — "you are already building %s on another computer, until %s; pass --take-over to continue here" evidence: internal/core/implement/loop/drain.go:994 — "res.Passed = append(res.Passed, Excluded{ID: v.ID, Check: r.Check, Reason: termsafe.Sanitize(r.Reason)})" - ac-4 — MET: Every lane the drain starts is handed the drain's stored pace (lane.pace = st.Pace) with the flag fields cleared, and a lane's start takes o.pace instead of resolving configuration; the test begins a drain with --pace 120/30 --sub-agents 4 and finds its second lane, opened in a move given no flag, on 120/30 with four sub-agents. evidence: internal/core/implement/loop/drain.go:1046 — "lane.Pace, lane.SubAgents, lane.FixRounds, lane.pace = nil, nil, nil, st.Pace" evidence: internal/core/implement/loop/loop.go:376 — "pace = *o.pace" evidence: internal/core/implement/loop/drain_repace_test.go:49 — "if got := paceNumbers(p); got != [4]int{120, 30, 4, BundledFixRounds} {"
- ac-5 — MET: A move of a running drain given --pace or --sub-agents calls repaceDrain where it used to refuse through the resume check, sets the drain state's pace and writes it with the move; the test moves a running drain with --pace 60/15 --sub-agents 3 without error and reads the new numbers back from the drain's state. evidence: internal/core/implement/loop/drain.go:398 — "// A running drain takes a new pace rather than refusing it" evidence: internal/core/implement/loop/drain.go:401 — "if repace, repaced, err = repaceDrain(repoRoot, &st, o, flags); err != nil {" evidence: internal/core/implement/loop/drain_pace.go:61 — "st.Pace = &next" evidence: internal/core/implement/loop/drain_repace_test.go:63 — "moved, err := Drain(repo.Root(), Options{Pace: strp("60/15"), SubAgents: strp("3")}, DrainOptions{})" evidence: internal/core/implement/loop/drain_repace_test.go:74 — "if err != nil || st.Pace == nil || paceNumbers(*st.Pace) != [4]int{60, 15, 3, BundledFixRounds} {"
- ac-6 — MET: After the move's state is written, each lane in progress has its run's pace replaced and logged, and its next stage reads the run's pace for the sub-agent ceiling and the window, while the next lane starts on st.Pace; the test finds the lane in progress and the next lane both on the new numbers, and a refused move takes none. evidence: internal/core/implement/loop/drain_pace.go:83 — "this lane runs on it from its next stage" evidence: internal/core/implement/loop/drain.go:469 — "if err := repace.lanes(repoRoot, st, now); err != nil {" evidence: internal/core/implement/loop/schedule.go:64 — "return s.Pace.SubAgents.Value" evidence: internal/core/implement/loop/loop.go:818 — "end := st.WindowStartedAt.Add(time.Duration(st.Pace.WorkMinutes.Value) * time.Minute)" evidence: internal/core/implement/loop/drain_repace_test.go:78 — "the lane in progress takes the new pace for its next stage" evidence: internal/core/implement/loop/drain_repace_test.go:86 — "the next lane opens on the new pace"
- ac-7 — MET: Each hand-back route keeps the blob of the issue's record on origin's default branch, an issue whose blob there has changed is no longer counted as taken, and a run that was handed back no longer passes its issue over; the tests change the record on origin and see the issue judged afresh and a new run opened. The blob on the closed-pull-request route landed only after the close, in 730aca57b, which is on main. evidence: internal/core/implement/loop/drain_readmit.go:115 — "return r.Blob == "" || now == "" || now == r.Blob" evidence: internal/core/implement/loop/drain.go:893 — "return r.Key == v.ID && !r.issueHandedBack()" evidence: internal/core/implement/loop/drain_repace_test.go:119 — "once its record changed on origin the issue is judged again" evidence: internal/core/implement/loop/drain_repace_test.go:150 — "the finished run no longer passes the issue over" evidence: git:730aca57b — "fix: keep the judged record on a closed pull request's hand-back route"
- ac-8 — MET: A fetch that fails at a move's start records an origin probe in the drain's state on the outage gate's schedule (one, five and ten minutes, then hourly, giving up eight hours into the hourly stage), plans nothing, asks nothing and returns without a refusal; tests cover the failed fetch, the probe schedule, the recovery and the give-up. evidence: internal/core/implement/loop/drain_origin.go:427 — "kind, why = DrainOriginFetch, "could not fetch "+claim.Remote+": "+termsafe.Sanitize(ferr.Error())" evidence: internal/core/implement/loop/drain_origin.go:444 — "s := implement.DefaultSchedule" evidence: internal/core/implement/loop/drain.go:550 — "return finish(started || probed)" evidence: internal/core/implement/loop/drain_origin_test.go:222 — "a move that cannot read origin: %v; want it to wait" evidence: internal/core/implement/loop/drain_origin_test.go:390 — "func TestADrainWaitingOnOriginFollowsTheOutageScheduleAndGivesUp(t *testing.T) {"
- ac-9 — MET: The real user-endpoint call keeps a definite answer for the process, keyed on the checkout and a digest of the forge credential's environment and gh's account store, and every reservation act a drain move makes resolves against the same checkout root; the test makes a reserve, a renewal and a release as a person whose email is not a no-reply address and counts one gh call. evidence: internal/core/claim/holder.go:278 — "if a, ok := holderCache.get(k); ok {" evidence: internal/core/claim/holder.go:283 — "holderCache.put(k, forgeAnswer{login: login, id: id})" evidence: internal/core/claim/holdercache_test.go:89 — "func TestThreeReservationActsAskTheForgeForTheHolderOnce(t *testing.T) {" evidence: internal/core/claim/holdercache_test.go:106 — "if n := gh.calls(); n != 1 {"
- ac-10 — MET_WITH_CONCERNS: Every renewal the loop pushes for a lane (a drain move's, a lane step's, the land fence's) carries Abcd-Stage from the loop's own stage names, and abcd peers shows it beside a live reservation in text and JSON; the surface test lists another holder's lane at the implement stage. Concern: a lane opened on the reservation its judgement took, or on a take-over, names no stage until its first renewal, about half a lease after the hold was taken, so a new lane is listed with no stage (iss-2610170948520608, still open). evidence: internal/core/implement/loop/reservation.go:96 — "req := claim.Request{Record: st.Key, Renew: true, Lease: renewalTerm(*own), Stage: renewalStage(*st)}" evidence: internal/surface/cli/peers.go:438 — "if r.State == claim.StateLive && claim.ValidLaneStage(r.Stage) {" evidence: internal/surface/cli/peers.go:517 — "line += " stage " + r.Stage" evidence: internal/surface/cli/peers_stage_test.go:120 — "if stages["iss-200"] != "implement"" evidence: .abcd/work/issues/open/iss-2610170948520608-in-a-drain-a-lane-opens-on-the.md:15 — "peers see none for the lane until its first renewal, about half a lease later." evidence: internal/core/claim/prepare.go:211 — "if req.Stage != "" && !req.Renew && (!req.Drain || req.TakeOver) {"
- ac-11 — MET_WITH_CONCERNS: abcd peers reads each holder's hand-back tombstones released in the last day from the fetched view and prints each with its record, kind and time, newest first; the surface test lists another holder's hand-backs with their kinds. Concern: only the newest ten per holder are listed with a kind and the rest only counted (dcn-2610170757251547), and only a hand-back published as a tombstone still at the issue's tip is listed, so one the drain keeps pending, one for an issue with no found_at, which reserves nothing, or one since reserved again does not appear. evidence: internal/core/claim/recenthandbacks.go:56 — "if r.State != StateReleased || r.ReleasedBy != ReleasedByTombstone || r.HandedBack == "" {" evidence: internal/surface/cli/peers.go:550 — "hb.Record, hb.Kind, hb.ReleasedAt)" evidence: internal/core/claim/recenthandbacks.go:24 — "const HandBacksShown = 10" evidence: internal/surface/cli/peers.go:553 — "and %d more" evidence: internal/surface/cli/peers_stage_test.go:132 — "if len(qh.HandBacks) != claim.HandBacksShown || qh.More != recent-claim.HandBacksShown {" evidence: internal/core/implement/loop/drain.go:916 — "r := noFoundAtHandBack(v.ID, now)" evidence: .abcd/development/brief/04-surfaces/35-drain.md:322 — "route that the hand-back is not yet published, and why; the drain keeps it"
Gap audit:
- honoured:
- The drain's judgement routes through the runner routing the build roles use, as a drain-judge role, with abcd computing the remedy digest itself evidence: internal/core/runner/config.go:213 — "known := map[string]bool{roleImplementer: true, roleDrainJudge: true}" evidence: internal/core/implement/loop/drain_judge.go:293 — "RemedySHA256: j.RemedySHA256,"
- The pace and sub-agents a drain was started with reach every lane it opens evidence: internal/core/implement/loop/drain.go:1046 — "lane.Pace, lane.SubAgents, lane.FixRounds, lane.pace = nil, nil, nil, st.Pace"
- A running drain's pace and sub-agents can be changed while it runs, applied from the next lane and the lane in progress's next stage evidence: internal/core/implement/loop/drain.go:401 — "if repace, repaced, err = repaceDrain(repoRoot, &st, o, flags); err != nil {" evidence: internal/core/implement/loop/drain_pace.go:83 — "this lane runs on it from its next stage"
- An issue the drain handed back is looked at again once its record changes on origin evidence: internal/core/implement/loop/drain_readmit.go:115 — "return r.Blob == "" || now == "" || now == r.Blob"
- A fetch of origin that fails at a move's start becomes a wait on the outage schedule rather than a refusal evidence: internal/core/implement/loop/drain_origin.go:444 — "s := implement.DefaultSchedule" evidence: internal/core/implement/loop/drain.go:550 — "return finish(started || probed)"
- The holder the forge names is asked once per process evidence: internal/core/claim/holder.go:283 — "holderCache.put(k, forgeAnswer{login: login, id: id})"
- abcd peers shows a live drain lane's stage, riding on the reservation renewal every account already fetches evidence: internal/core/implement/loop/reservation.go:96 — "Stage: renewalStage(*st)}" evidence: internal/surface/cli/peers.go:517 — "line += " stage " + r.Stage"
- diverged:
- A hosted session that restarts takes its own reservations back with one flag: delivered only where the person's username resolves without the forge and the lost copy's holds do not overlap; a person only the forge token names gets no take-over and no advice naming the flag, and overlapping holds wait until they lapse evidence: internal/core/implement/loop/drain.go:863 — "capHolder = claim.ReaderHolder(repoRoot, claimOptions(now))" evidence: internal/core/implement/loop/drain.go:939 — "takeOver = capHolder" evidence: internal/core/claim/crowd.go:266 — "if r.State != StateLive || r.Record == id || r.Copy == CopyThis {"
- A lost connection to origin waits like any other outage: delivered for the move's opening fetch, but a reservation push whose own fetch fails mid-move refuses the move
evidence: internal/core/implement/loop/reservation.go:618 — "settle what it names, then run the verb again"
evidence: internal/core/implement/loop/drain.go:998 — "return refuseMoved(err)"
evidence: .abcd/development/brief/04-surfaces/35-drain.md:364 — "
originnot answering" - No person needs to write the judgement's answer by hand: only the claude runner can take the role, the judge cannot start under a path holding a parenthesis or comma, and a runner that does not answer is not started again for that request, so the host's request stands until its hold lapses evidence: internal/core/runner/config.go:253 — "if role == roleDrainJudge && name == OpenCode {" evidence: internal/core/runner/runner.go:234 — "toolRe = regexp.MustCompile(" evidence: internal/core/implement/loop/drain_judge.go:249 — "so it is not started again and the request is the host's"
- abcd peers shows each drain lane's stage: a lane opened on its judgement hold or on a take-over shows none until its first renewal evidence: .abcd/work/issues/open/iss-2610170948520608-in-a-drain-a-lane-opens-on-the.md:15 — "peers see none for the lane until its first renewal, about half a lease later."
- abcd peers lists each holder's recent hand-backs with their kind: bounded to the newest ten per holder with a count of the rest, and only tombstones still at the tip evidence: internal/core/claim/recenthandbacks.go:24 — "const HandBacksShown = 10" evidence: .abcd/work/decisions/dcn-2610170757251547-abcd-peers-shows-at-most-ten-hand-backs.md:6 — "abcd peers shows at most ten hand-backs per holder"
- missing:
- Evidence that a drain runs unattended for a day: the spec names the three-account lab, rerun on the built binary before close, as that evidence, and no rerun is recorded in the close or anywhere in the record tree evidence: .abcd/development/specs/closed/spc-2610161044461448-drain-many-accounts.md:75 — "Before this spec closes, the three-account lab of 2026-10-16 is run again on the built binary, every account on the same build." evidence: git:80764362b — "chore: close the drain-many-accounts spec and ship its three intents"
Scope-condition dispositions:
- cond-2610161044461615 — narrowed: A route in the person's own layer starts the runner and, with no route, the host's request is written as before (tested), but only the claude runner can take the role and the judge cannot start under a path holding a parenthesis or a comma. narrowing: holds where the person's own machine layer routes roles.drain-judge.runner to the claude runner and the real paths of the checkout and the system temporary directory hold no parenthesis or comma; an opencode route, or such a path, leaves the judgement with the host exactly as with no route evidence: internal/core/runner/config.go:248 — "which is skipped: only the person's own" evidence: internal/core/runner/config.go:253 — "if role == roleDrainJudge && name == OpenCode {" evidence: internal/core/implement/loop/drain_judge_test.go:111 — "func TestWithNoRouteTheDrainJudgementIsTheHostsRequest(t *testing.T) {" evidence: .abcd/work/issues/open/iss-2610171043574168-the-routed-drain-judge-cannot-run-for-a.md:15 — "so every move hands the request to the host."
- cond-2610161044469300 — narrowed: With a GitHub no-reply git identity the take-over moves the lost copy's reservation as the drain reaches it (tested), but the let-through keys on the holder read without the network and exempts only the issue's own reservation, not the footprints of the lost copy's other holds, which that copy was allowed to overlap. narrowing: holds where the person's GitHub username resolves without asking the forge (a GitHub no-reply user.email, or reservation.github_username in the machine layer) and for each lost-copy reservation whose footprint shares no path with another live reservation of that copy; otherwise the new copy passes those issues over until the lost copy's reservations lapse evidence: internal/core/implement/loop/drain_takeover_test.go:27 — "lost := otherCopy(t, bare, patEmail)" evidence: internal/core/implement/loop/drain.go:863 — "capHolder = claim.ReaderHolder(repoRoot, claimOptions(now))" evidence: internal/core/claim/render.go:168 — "h, err := resolveHolder(root, who, o, false)" evidence: internal/core/claim/crowd.go:266 — "if r.State != StateLive || r.Record == id || r.Copy == CopyThis {" evidence: internal/core/claim/crowd.go:95 — "if r.Copy == CopyThis && mine {"
- cond-2610161044467671 — narrowed: The renewal's stage vocabulary is exactly the loop's performed stages, held together by a test, and a drain move renews the lane at half its lease, but a lane shows no stage before its first renewal and a stopped drain's last stage stays shown until the reservation lapses. narrowing: holds while the lane's drain keeps moving (each move renews at half the lease, one hour at the two-hour default); a lane opened on its judgement hold or a take-over names no stage until its first renewal, and once moves stop the last stage stays on show for up to a full lease, until the reservation lapses evidence: internal/core/claim/stage.go:24 — "var LaneStages = []string{LaneStageWorktree, LaneStageBrief, LaneStageImplement, LaneStageValidate, LaneStageLand}" evidence: internal/core/implement/loop/renewal_stage_test.go:26 — "func TestEveryStageTheLoopRenewsWithIsAClaimLaneStage(t *testing.T) {" evidence: internal/core/implement/loop/reservation.go:159 — "return own, !now.Before(half), """ evidence: internal/core/lease/lease.go:19 — "const Default = 2 * time.Hour" evidence: internal/surface/cli/peers.go:438 — "if r.State == claim.StateLive && claim.ValidLaneStage(r.Stage) {" evidence: .abcd/work/issues/open/iss-2610170948520608-in-a-drain-a-lane-opens-on-the.md:15 — "peers see none for the lane until its first renewal, about half a lease later."
- cond-2610161044460285 — survived: abcd peers lists the stage and hand-back rows, and the people's status view, abcd status, already embeds, fills and renders the same reservations block, so the rows travel with the listing. evidence: internal/surface/cli/peers.go:438 — "if r.State == claim.StateLive && claim.ValidLaneStage(r.Stage) {" evidence: internal/surface/cli/status.go:65 — "reservationsBlock" evidence: internal/surface/cli/status.go:166 — "failed, listed := out.reservationsBlock.fillView(root, now, fetched.Gap, ferr)" evidence: internal/surface/cli/status.go:309 — "renderReservations(w, out.reservationsBlock)" evidence: internal/surface/cli/status.go:115 — "drain lane's stage and each holder's hand-backs from the last day."
Decisions
- 2026-10-16, facilitator-on-delegation (dcn-2610161040352440): the three drafts are one bundle, drain-many-accounts; this one is minor and builds on the other two.
- 2026-10-16, facilitator-on-delegation (dcn-2610161040390001): the judgement is a drain-judge role on the runner routing; with no route it goes to the host as today; abcd computes the digest.
- 2026-10-16, facilitator-on-delegation (dcn-2610161040398791): a restarted hosted drain recovers with
abcd drain --take-over; abcd keeps no drain state outside the checkout. The press release sentence that said a restart keeps the drain state and copy key was narrowed to this on 2026-10-16. - 2026-10-16, facilitator-on-delegation (dcn-2610161040399117): pace and sub-agents reach every lane and can be changed while the drain runs; changed records are re-admitted.
- 2026-10-16, facilitator-on-delegation (dcn-2610161040394970): the lane stage rides on the reservation renewal and shows in abcd peers with recent hand-backs; the holder is cached per process. The wait on a failed fetch is delivered with the bundle's first step, under dcn-2610161040350256.
- 2026-10-16, facilitator-on-delegation (dcn-2610161044187673): the pre-pass's questions are answered. Brief invariant 7 is amended for each new drain network act in the step that adds it, invariant 19 gains the clause on issues naming an intent, a published hand-back is a judgement kept on the reservation and not a classification written onto the issue, and each sibling overlap is kept both.
Grounds
- pursued: facilitator-on-delegation, 2026-10-16: the product thinker wants drains on several accounts to run through a day without minding and to be seen by everyone; we expect routing the judgement, waiting out lost connections and showing lane stages on the reservation to remove the remaining reasons a person must step in. Shown wrong if a drain with a routed judge needs a person's answer before it ends, or a person cannot name another account's lane stage from abcd peers.
Frontmatter
| id | itd-2610161016401115 |
|---|---|
| type | intent |
| lifecycle | shipped |
| kind | bundle-member |
| severity | minor |
.abcd/development/intents/shipped/itd-2610161016401115-a-drain-runs-unattended-on-a-hosted.md
open on GitHub ↗ · commit history ↗
References2
- itd-2610161016391855A stuck drain lane hands itself backbuilds on
- itd-2610161016394967Drains on several accounts share the workbuilds on
Referenced by9
- adr-2610222222326104abcd drives agents with no host session only on the person's own invocation, on the person's own account behind a wall, and hands back what it cannot finishrelated
- iss-2610161015417715A drain's --pace and --sub-agents reach only the lane opened in its first move; later lanes resolve their pace again from configuration, so the old drain's first lane ran 10080/0 with six sub-agents and its next ran 120/300 with two.related
- iss-2610161015422306The drain's host judgement has no route to a command-line model, so an unattended drain needs a host agent looping; in a plain terminal a person writes the JSON answer, sha256 digest included, by hand; and a hosted session that restarts loses .abcd/.work.local, so its new copy key reads its own reservations as another copy's and the advice names --take-over, which drain does not accept.related
- iss-2610161015433495With a git email that is not a GitHub noreply address, every reservation act calls gh api user again, uncached.related
- iss-2610161015434746abcd peers shows each account's reservations but nothing of a drain lane's stage, its judgements or its hand-backs, so one person cannot see what another account's drain is doing.related
- iss-2610161015448977A failed fetch from origin refuses the drain move instead of entering the drain's outage wait.related
- iss-2610161015457468A resumed drain keeps what it began with for its whole life: its stored pace and sub-agents cannot be changed (--pace is refused while it runs), the issues it handed back stay excluded until it ends, and an issue with an old handed-back run is passed over with 'drive or finish that run' for as long as that run directory exists.related
- iss-2610262044274697The drive draft treats a plain-terminal drain that runs on its own as new, but the shipped drain bundle says drains already run unattended in a plain terminalrelated
- spc-2610161044461448drain-many-accountsimplemented by
mentions17
- adr-2611011231015519The launches abcd makes spend a person's money only within the budget they set, past it only through named residuals
- dcn-26101610403502562026-10-16 — A drain plans from origin's default branch (facilitator-on-delegation, drain-many-accounts): each drain move fetches origin inside the drain's lock before it reads anything, and takes one snapshot of the ledger from origin's default branch, which the plan, the lane's start check, the judgement's remedy digest and the reservation footprint all read; a fetch that fails, a default branch the fetch could not read, or a missing origin/HEAD means the move cannot plan and enters the drain's outage wait, never planning from what the checkout last saw; the dry run plans from the last fetched view and says how old it is, so it still writes nothing; a checkout behind origin is warned about, not refused, because the lane is cut from origin anyway; and any reservation of a record finished on origin's default branch backs off, with or without a tip (grounds: a queue consumer reads the row it locks, as SELECT ... FOR UPDATE SKIP LOCKED; iss-2610160957404827, iss-2610161015448977).
- dcn-26101610403524402026-10-16 — The three drain drafts are one bundle (facilitator-on-delegation, the product thinker having delegated the planning of the drain-many-accounts bundle on 2026-10-16): itd-2610161016394967, itd-2610161016391855 and itd-2610161016401115 are planned as one bundle, drain-many-accounts, with severity critical, major and minor; itd-2610161016401115 builds on the other two, which are independent of each other; the decomposition files each draft whole as an intent and routes two parts to clauses in brief invariants 7 and 19, and none to a new ADR, principle or brief page (grounds: the three are user moments of one run on several accounts, which the product thinker asked to have delivered together, and every part is drain behaviour inside records that already exist).
- dcn-26101610403900012026-10-16 — The drain's judge is a routed role (facilitator-on-delegation, drain-many-accounts): the drain's judgement is a new drain-judge role on the runner routing the build roles use; with no route the request goes to the host as today; abcd computes the remedy digest itself in either case (grounds: one routing for every model-run role, which the build roles already use; iss-2610161015422306).
- dcn-26101610403949702026-10-16 — A drain lane's stage shows in abcd peers (facilitator-on-delegation, drain-many-accounts): a drain lane's stage is published as a closed-vocabulary trailer on its reservation's renewal commits, and abcd peers lists it beside the reservation with each holder's hand-back tombstones of the last day; the people's status view inherits the same rows when it takes the reservation listing over (itd-2609151838312703, iss-2610150726331622); and the holder a process resolves through the forge is cached for that process (grounds: the reservation is the record every account already fetches, so the stage costs no new act; iss-2610161015434746, iss-2610161015433495).
- dcn-26101610403987912026-10-16 — A restarted hosted drain takes over its holds (facilitator-on-delegation, drain-many-accounts): a hosted session that restarts recovers its drain with abcd drain --take-over, which moves the person's own live reservations from a lost copy to this one as the drain reaches them; abcd keeps no drain state outside the checkout, and the draft's press release is narrowed to match (grounds: the principle durable-state-lives-where-the-platform-says-it-survives, since where a hosted session's files survive a restart is the host's and abcd stays host-agnostic; iss-2610161015422306).
- dcn-26101610403991172026-10-16 — A drain's pace reaches every lane (facilitator-on-delegation, drain-many-accounts): a drain carries its resolved pace and sub-agent ceiling into every lane it opens, and a running drain takes a new --pace or --sub-agents, recording them in its state and applying them from the next lane it opens and the lane in progress's next stage; a handed-back issue whose record blob on origin's default branch has changed is eligible again, and a finished handed-back run no longer passes its issue over (grounds: a setting the person gives is the drain's for its whole life, and a changed record is a new question; iss-2610161015417715, iss-2610161015457468).
- dcn-26101610441876732026-10-16 — The drain bundle's pre-pass answers (facilitator-on-delegation, drain-many-accounts): brief invariant 7's sentences naming what drain and implement step fetch and push are amended, in each building step that adds an act, to name the per-move fetch, the reservation before a judgement, the hand-back tombstone, the renewal of the lane in progress, the parked lanes' pull-request listing, the fork question and the sync's fetch of the default branch; brief invariant 19 gains a clause that an issue whose related_intents names an intent is not taken; a hand-back published on origin is the record of a judgement kept on the reservation, not a field classification written onto the issue, so invariant 19's re-derivation stands; and each member keeps both against its siblings itd-82, itd-2609091034175565 and itd-2609151838312703, building on the first two (grounds: the invariants are the record of what may touch the network and what a machine may take, so they move with the surface, as the principle spec-moves-with-the-surface requires).
- dcn-26101707572515472026-10-17 — abcd peers shows at most ten hand-backs per holder (facilitator-on-delegation, drain-many-accounts, step 18): the newest ten of the last day, newest first, with a count of the rest, never dropped silently (grounds: ten keeps one holder's block to a screenful while a session reading it before a pick still sees the latest of a busy day; refines dcn-2610161040394970).
- dcn-26102222163464482026-10-22 — The plain-terminal drain is one intent that refines the shipped drain bundle (facilitator, on the product thinker's delegation of 2026-10-14 and 2026-10-16, itd-2610222215485265): the decomposition table routes it FILE-AS-IS, since its fence (itd-2610161621364075), its routed judge's isolation (commands/drain.md) and the consent to start agents are rules already in force, cited rather than restated; it refines itd-2610161016401115, whose press release promised a plain-terminal drain and which delivered the routed judge but no loop, rather than reopening a shipped record; revised after review round 1: the path reverses the host-delegated boundary for an opt-in use, as itd-2609201916151817's decision 5 flagged, so that reversal is recorded as adr-2610222222326104, proposed until the sign-off (grounds: the decomposition-calibration note under 2026-10-22; itd-84)
- iss-200guard's env wrapper table listed -S/--split-string as a value-owning flag for the SEPARATE-TOKEN spelling only ('env -S git push --force origin main'), but GNU env -S/--split-string also accepts the value glued to the flag ('env -Sgit push --force origin main', 'env --split-string=git push --force origin main'), which re-splits identically and executes identically; a fix attempted in bug-hunt loop round 9 (bugfix/env-split-string-guard-bypass) closed only the separate-token form and was BLOCKed at pre-PR review by both an independent correctness reviewer and an independent security reviewer, who each independently reproduced that the glued forms remain a complete, silent bypass of every guard blocker on the live PreToolUse path; suggested remediation (from both reviewers, converging independently): in commandOf/skipWrapperArgs, treat -S/--split-string as a value-owning env flag in all three spellings — the exact separate-token form (-S as its own token, value in the following token), the glued form (-S<value>, len>2), and the --split-string=<value> form (value non-empty) — and in every case read the first word of <value> as command position, rather than either treating the whole glued token as a consumed wrapper flag or skipping the following token outright in the separate-token case — no nested re-tokenizing required, contained to the same two functions
- iss-2610150726331622abcd peers does two unrelated things: it lists records that sibling worktrees and local branches hold that this checkout does not (an agent's duplicate check before filing), and it fetches and lists the reservations on the shared remote. The second duplicates abcd status from itd-2609151838312703, the people's view of what everyone else is doing. The product thinker ruled on 2026-10-15 that peers keeps its name for its future meaning, which sessions are running now, local first and later over a private network such as Tailscale or WireGuard (the session register itd-2609150819440345), and that the local duplicate check belongs under abcd status --local.
- iss-2610170948520608In a drain, a lane opens on the reservation the drain took to ask its judgement and pushes nothing at its start, so the reservation names no lane stage, and peers see none for the lane until its first renewal, about half a lease later.
- iss-2610171043574168The routed drain judge cannot run for a checkout or temporary directory whose real path holds a parenthesis or a comma: the path-scoped Read and Edit grants and the checkout denies are refused by the runner's tool-name check, so every move hands the request to the host.
- iss-2610252224317335spc-2610161044461448 required the 2026-10-16 three-account lab to be rerun on the built binary, every account on one build, before it closed. No rerun is recorded in the close commit (80764362b), the history or the lab store, so three shipped intents rest on per-step tests with two or three clones of one local bare origin: itd-2610161016391855 (evidence from drains on several accounts), itd-2610161016394967 (drains share the work, end to end) and itd-2610161016401115 (a drain runs unattended for a day).
- 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-2610222215485265abcd drain --drive works through the ledger from a plain terminal