Development record
drain-ledger-triage
Summary
The design record for itd-82 as revived on 2026-09-21 (decisions 1 to 8)
after two adversarial reviews: abcd drain classifies the open ledger by
field, hands the eligible issues to the implement loop's issue key one at a
time, routes the rest back by kind, and runs until nothing eligible is left
under the pace rule.
Scope
- The
remedy:field:capturegains--remedy "<text>"and the frontmatter key (the optionalsuggested_fixmade first-class under this name; a migration reads the old key as the new one where present); the issue schema names it; the surface page documents it (criterion 1). - Eligibility, by field (
internal/core/capture/eligible.go): open, nothing unshipped inblocked_by,remedy:present, category in the fixable set (bug,documentation,drift,inconsistency,tech-debt,ux), severityminorornitpick; every other open issue receives its disposition by the rule that excluded it (criterion 1). - The judgement: a host pass over each eligible issue's remedy asking one question, "does this remedy change what a user sees or a trust boundary", returning a validated yes/no with a reason; yes hands back, no changes nothing (criterion 2).
- The lane:
implement.Startwith keyiss-N(decision 10 on the parent), one at a time under the ceiling; the loop's own definition of done, resolve and pull request apply (criterion 3). - The hand-back, by kind: user moment →
capture promote <iss-N>(itd-119's verb) and nothing else written; trust rule → a flag in the summary with the question and the decision-record marker on the open issue (decision 10); above severity or outside the fixable set → a flag naming the rule; other → a flag with the proposed home; ahandback:on a lane report → the lane discarded and the same routing (criteria 4, 5, 6). - Order:
tech-debt,documentation,inconsistency,drift,bug,ux;nitpickbeforeminor; oldest first (decision 7). - The clock and the caps: the pace rule's window and ceiling; at the
window's end the state file takes
next_eligible_atand the process exits;--max <n>; the default is all (criterion 7). - The summary: every disposition with its reason, the order rule,
every cap, in text and
--json; a run without a merge exits 0 (criterion 8). - The decision record: the spec's first delivery mints the ADR for the
eligibility rule with
abcd decide --adrfrom decision 4 and scope 2, adds the brief invariant, and the run refuses to start unattended without them (criterion 11). - Safety and pages: issue ids validated by shape; the command page (criteria 9, 10).
Out of scope
- The lane's internals (the parent); picking among intents (the sibling).
- Writing a classification onto the issue (decision 8); the decision-record marker is a hand-back's flag, not a classification (decision 10).
- A run flag that lets
majorthrough (decision 11): a repository's drain record loosens its own rule.
Approach
internal/core/capture gains the field and the eligibility function;
internal/core/implement gains Drain, a loop over the ordered eligible
set that calls Start with the issue key and reads each lane's outcome; the
judgement follows the host-pass shape (request file, validated return); the
hand-back writes go through capture promote and the summary writer. The
ADR is minted in the first delivery and reviewed with the diff.
Footprint
- packages: internal/core/capture, internal/core/implement, internal/core/issueschema, internal/surface/cli
- tests: the eligibility table over a fixture ledger; the order; the
hand-back routing per kind; the
related_intentsstamp as the only write; the window exit and--max; the refusal without the ADR.
Steps
- The judgement before a lane opens (scope 3)
- packages: internal/core/implement, internal/core/capture
- tests: an eligible issue whose remedy changes a user-visible surface or a trust boundary is handed back before any lane opens; one that changes neither opens a lane
- needs: none
- landed: 80fa3da48
- The summary's remaining counts (scope 8)
- packages: internal/core/implement, internal/surface/cli
- tests: the summary carries every count scope 8 names, in text and --json
- needs: 1
- landed: 378348a14
- Merged lanes are finished (iss-2610090936460789)
- packages: internal/core/implement
- tests: a lane parked at land whose pushed head is on the default branch is removed and marked done on the next drain move, outside the pacing window
- needs: none
- landed: 91765d9c5
- One issue or package held back for now (iss-2610090642385898)
- packages: internal/core/implement, internal/surface/cli
- tests: an issue on the skip list is passed over with its reason in the plan's passed entries, and the list clears when the drain ends
- needs: none
- landed: 6594725b5953
- A pull request that leaves the merge queue is put back (iss-2610090642376032)
- packages: internal/core/implement/loop
- tests: a landed pull request left unqueued at CLEAN is enqueued, and one dropped after a failed merge-group check is reported with the failed check
- needs: none
- landed: 2b96ec1cca1d
- The decision-record marker on the issue (decision 10)
- packages: internal/core/issueschema, internal/core/capture, internal/core/implement
- tests: a trust-rule hand-back writes a
decision_needed:field holding its question onto the open issue and changes nothing else in the record; the dry run and the next drain pass a marked issue over with the marker as its reason; invariant 19 and the drain page name the marker - needs: 1
- landed: 2e297483e687
How the criteria are satisfied
| Criterion | Where |
|---|---|
| 1 mixed ledger routed by field | scope 1, 2, 5 |
| 2 judgement only hands back | scope 3 |
| 3 the issue-keyed lane | scope 4 |
4 user moment promoted, related_intents stamp only |
scope 5 |
| 5 trust rule flagged and marked, nothing minted | scope 5, step 6 |
| 6 handback stops and routes | scope 5 |
7 window exit; --max |
scope 7 |
| 8 the summary | scope 8 |
| 9 ids by shape | scope 10 |
| 10 the page | scope 10 |
| 11 refuses without the ADR | scope 9 |
Progress
This section says which pieces of the scope have landed; the spec stays open until the run itself closes it.
- Landed (the field-only slice): scope 1, 2, 6 and 9, and the dry-run half
of scope 8 and 10. The
remedy:field (capture --remedy, the schema's allow-list, the reader and the committed-ledger gate; a record carrying onlysuggested_fix:reads that value as its remedy, and no record is rewritten); the field-only eligibility rule and the order (internal/core/capture/eligible.go);abcd drain --dry-run, which gives every open issue one disposition with its rule and reason, in text and--json, and writes nothing; the eligibility decision record (adr-2609291342092738) and invariant 19; and the start refusal, which names the record when the rule has none and otherwise refuses because the issue-keyed lane is not built. The command page iscommands/drain.md, listed in the agents block until the product thinker rules on the person's fourteen-verb ceiling. - Landed (the remedy required, rulings BX3 and H12 of 2026-09-29):
capturerefuses a new issue without a remedy; the automatic filers (the consistency pass, every promoted inbox report) write the one machine valuenone (filed automatically), which the capture surface refuses from a person; the dry run lists a record carrying it as ineligible until a person writes a real remedy withcapture remedy, the verb that writes or replaces the field on an open issue. A record filed before the rule stays readable and is listed as ineligible. - Landed (the run): scope 4, 5 and 7, and the run's half of scope 8 and
10.
abcd build <iss-N>and the drain start the implement loop keyed by the issue: the key is an issue id by shape, the checks are this rule read as the dry run reads it and the peers, the brief is the record with its remedy as the work and the reproduce-then-fix definition of done, the validators run without the fidelity audit, the receipt must declare the issue fixed, and the landing resolves it and opens one pull request (loop/check.go,loop/issuebrief.go,loop/receipt.go). A receipt'shandback(kind, reason, home) ends the lane before its validators, its worktree and branch discarded and the discarded head recorded. The bareabcd drainperforms one move per invocation (loop/drain.go): one lane at a time in the drain order; a lane's hand-back routed by kind (a user-visible change promoted withcapture promote, the issue gaining the draft inrelated_intentsand nothing else; a trust rule flagged with its question, nothing minted; a design finding or a second package flagged with its home); every field hand-back flagged naming its rule; the drain's window closing intonext_eligible_atin.abcd/.work.local/run/drain.json;--max <n>ending the drain at the cap. The summary is text and--json, and a move that opens or merges nothing exits 0 saying why. - Landed (the judgement, step 1): scope 3. Before the drain opens the next
eligible issue's lane it writes a host-pass request
(
.abcd/.work.local/run/drain-judgement.request.md) asking whether the remedy changes what a user sees or a trust boundary, records the issue and the remedy's digest, and opens nothing;abcd drain --judgement <file>takes the strict answer (loop/judgement.go). A yes hands the issue back before any lane opens, routed as a lane's hand-back of its kind (from: "judgement"); a no opens the lane; an answer over an issue no longer eligible decides nothing, and one over a remedy rewritten since is refused and asked again. Every answer is kept in the drain's state and summary. - Landed (the summary's remaining counts, step 2): scope 8. Every move's
summary carries
counts, in text and--json: the lanes opened by outcome, the pull requests they opened, the hand-backs by route (promotions, decision records, homes), the rule's flags, the host judgements, the issues passed over, and the spend in agent runs (each verified implementer's receipt and each validator's return across the drain's lanes, the budget check's unit; a lane whose run cannot be read is named, the spend then a floor). A lane done before the drain read it keeps its pull request number, and a drain that ends with every lane ended and none in a pull request says so as a normal ending (one stopped with a lane still in progress does not). - Landed (merged lanes finished, step 3; iss-2610090936460789). Every
drain move, after the outage check and before the window clock, takes the
landing's last step for each issue lane in the checkout parked at land whose
pushed head is on the default branch (
loop/drain_finish.go): its worktree and branch are removed and the lane is done, whatever the drain's or the run's window clock says, since the cleanup does no new work. The lanes are read from the checkout's runs, not the drain's state, so an ended drain's lanes and a person'sabcd build <iss-N>lanes are finished too; the default branch is fetched once per move, only while a lane is parked, and the forge is asked nothing about a lane not yet on it. A lane the landing refuses to finish is named infinishedandnext, and the move goes on. - Landed (a pull request that leaves the merge queue is put back, step 5;
iss-2610090642376032). While an armed lane waits where a merge queue
gates the default branch, each landing step reads its pull request's place
in the queue (
loop/queue.go): one in the queue is waited on, its place named; one the forge holdsCLEANoutside it is queued, pinned to the pushed head, after the land fence (the forge refusing, its auto-merge is toggled once the arming is pastabcd land's 30-minute grace); one that left the queue and its arming is refused for a person, naming the forge's removal reason and the check that failed on its last merge group, and is never queued or armed again, except that under another session's release hold it is a wait. A drain's move takes the same watch for each parked lane its listing shows armed andCLEANor disarmed, reading a dropped one asdropped. - Landed (the decision-record marker on the issue, step 6; decision 10).
A trust-rule hand-back, a lane's or the judgement's, writes
decision_needed:holding its question onto the open issue (capture.MarkDecisionNeeded,capture/decisionneeded.go) and changes nothing else in the record; nothing is minted. The issue schema knows the key, and the reader carries it. It is written in the checkout, uncommitted, as a promotion is, and only where the checkout holds the record as origin's default branch does; a marker a killed move already wrote is found, not written twice, and an issue no longer open is not marked. The field rule hands a marked issue back underdecision-needed(after the severity rule, beforewaits-on-ruling) with the question as its reason, so the dry run and every later drain pass it over until a person writes the decision record and takes the field out; the drain that wrote it counts it once, as its own decision-record hand-back. Invariant 19, the drain page and the drain and capture brief chapters name the marker.
Frontmatter
| id | spc-2609212015054359 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-2609212015054359-drain-ledger-triage.md
open on GitHub ↗ · commit history ↗
References1
- itd-82`abcd drain` fixes the issues that need no decision, and hands the rest back by kindimplements
mentions9
- adr-2609291342092738A drain takes an issue alone only when its fields say it needs no decision
- dcn-26101000000000032026-10-10 — Five points spc-2609212015054359 step 1 (scope 3, the host judgement before a drain's lane opens; itd-82 criterion 2) left open, settled by lane-1 of run-2610101724174688 in building it. (1) The host-pass shape is the loop's own: the drain writes one request, `.abcd/.work.local/run/drain-judgement.request.md`, records the issue and the sha256 of the remedy it shows in its state as `judging`, and opens nothing; the host writes strict JSON to `.abcd/.work.local/run/drain-judgement.json` and hands it back with a new flag on the bare verb, `abcd drain --judgement <file>`, which accepts only that path. The answer echoes `issue` and `remedy_sha256`, so an answer written for an earlier request, or over a remedy rewritten since, cannot be taken for this one; a remedy rewritten while the answer is out refuses the answer and the next move asks again. (2) The spec's yes/no carries a kind with a yes, `user-visible` or `trust-rule` (`trust-rule` when both, since it names the decision a person owes first), and a yes is routed exactly as a lane's hand-back of the same kind (promoted with `capture promote`, or flagged as needing a decision record with the reason as its question), with `from: "judgement"`; the intent's press release routes "an issue that needs a decision" by kind whatever found it, and a second routing for the judgement alone would be a design the record does not ask for. (3) The request tells the host to answer yes when the remedy does not let it tell, since the judgement guards a failure that runs one way and can only hand back. (4) An answer over an issue that left the eligible set since the request is recorded with `applied: false` and a note and decides nothing, which is criterion 2's "never makes an ineligible issue eligible" at the moment the answer arrives; a no is bound to the remedy's digest, so a rewritten remedy is asked about again before its lane opens. (5) `--max` keeps counting lanes, as it did: an issue the judgement hands back opened no lane, and the intent's "issues attempted" is read as lanes attempted. `abcd build <iss-N>`, a person's own start of one issue's lane, asks no judgement: the judgement is the drain's unattended pass, and the person typing the id is the judgement.
- dcn-26101000000000052026-10-10 — Three defects the reviewers of PR #886 (itd-2609201925079472, run-2610100550297632) noted below their bar are deferred, not fixed in the change that set the bundled pause to zero: iss-2610100913086397 (a rate-limited validator whose partial return cannot be saved aside leaves its lane stuck, its named `--restart` refused), iss-2610100913088696 (the rate-limit checkpoint matches an await by receipt path alone, against its comment) and iss-2610100913096412 (the run-start quota query has no deadline and its error is not home-redacted). Reason: each needs a tested change to the rate-limit and budget code in `internal/core/implement/loop/`, which steps 3 to 5 of spc-2609212015054359 (itd-82, the drain) rework next, and none is reachable in a run today without a second fault: a refused save-aside on a rate limit, an operator restart racing the dispatch, or a quota-reporting runner, of which none ships. They are taken up with the rate limit's own wait (iss-2610100602595553), or by the drain's steps where those touch the same code.
- iss-2610090642376032A pull request the implement loop lands can leave the merge queue silently. Twice it sat CLEAN with auto-merge armed and never entered the queue (#837, #844; the enqueue mutation then failed with a forge error and only toggling auto-merge queued it), and four times across this run and the v0.13.2 cut a failed merge-group check (the macOS job cap, or a flaky test) dropped it from the queue (#839, #854, #867, #869) with nothing re-queuing it. The loop arms auto-merge and walks away, so a run that trusts it stalls unseen; one drop cost about 45 minutes in this run.
- iss-2610090642385898A drain has no way to hold back one issue or one package for now. A peer's verbal hold is invisible to it (only implement claim or a branch carrying the record keeps a lane off an issue), and while a fix is in flight in one package the only way to keep the drain out of that package is to stop the whole drain, so this run drove a colliding lane anyway and let whichever landed second rebase.
- iss-2610090936460789A lane's worktree and branch are cleaned up only by a final 'abcd implement step' run after its pull request merges, and nothing runs it: the land stage waits (exit 3) until the merge, the drain records the lane as 'pull-request' and moves on, and no hook, drain move or schedule comes back. On 2026-10-09 all 18 overnight drain lanes sat merged but still at the land stage, their worktrees in the store and their runs counted as building on the status board (iss-2610090824041378). The step was also refused when it was finally run, because the run's pacing window had elapsed ('nothing before 14:36Z'), so even finishing a merged lane waits hours.
- iss-2610110918418459The implement loop's landing never writes a spec step's '- landed:' line, so a spec the loop landed part of still reads those steps as unlanded: a later 'spec close --remainder' carries steps that already merged into the remainder, and a fresh 'abcd build' of the same spec opens a lane for them again. Found by the fidelity audit of itd-2609212103565953 (receipt rcp-2257cc3a38d9, criterion ac-3 MET_WITH_CONCERNS); spc-2609202134338445's Progress already says the landing does not mark a non-closing lane's step.
- iss-261025221438449035-drain.md:678-679 states a spec's delivery state ('spc-2609212015054359, which stays open for the decision-record marker and the issues its plan names'), which .abcd/development/brief/README.md:13 says lives in the roadmap and never in brief prose.
- itd-119An Issue Graduates Into An Intent Without Retyping