This is the record of an abcd-managed repository

abcdBetav0.16.2 · 2026-11-06

Development record

drain-ledger-triage

specclosedspc-2609212015054359

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

  1. The remedy: field: capture gains --remedy "<text>" and the frontmatter key (the optional suggested_fix made 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).
  2. Eligibility, by field (internal/core/capture/eligible.go): open, nothing unshipped in blocked_by, remedy: present, category in the fixable set (bug, documentation, drift, inconsistency, tech-debt, ux), severity minor or nitpick; every other open issue receives its disposition by the rule that excluded it (criterion 1).
  3. 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).
  4. The lane: implement.Start with key iss-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).
  5. 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; a handback: on a lane report → the lane discarded and the same routing (criteria 4, 5, 6).
  6. Order: tech-debt, documentation, inconsistency, drift, bug, ux; nitpick before minor; oldest first (decision 7).
  7. The clock and the caps: the pace rule's window and ceiling; at the window's end the state file takes next_eligible_at and the process exits; --max <n>; the default is all (criterion 7).
  8. 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).
  9. The decision record: the spec's first delivery mints the ADR for the eligibility rule with abcd decide --adr from decision 4 and scope 2, adds the brief invariant, and the run refuses to start unattended without them (criterion 11).
  10. 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 major through (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_intents stamp as the only write; the window exit and --max; the refusal without the ADR.

Steps

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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 only suggested_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 is commands/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): capture refuses a new issue without a remedy; the automatic filers (the consistency pass, every promoted inbox report) write the one machine value none (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 with capture 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's handback (kind, reason, home) ends the lane before its validators, its worktree and branch discarded and the discarded head recorded. The bare abcd drain performs 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 with capture promote, the issue gaining the draft in related_intents and 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 into next_eligible_at in .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's abcd 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 in finished and next, 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 holds CLEAN outside it is queued, pinned to the pushed head, after the land fence (the forge refusing, its auto-merge is toggled once the arming is past abcd 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 and CLEAN or disarmed, reading a dropped one as dropped.
  • 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 under decision-needed (after the severity rule, before waits-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

idspc-2609212015054359
typespec
lifecycleclosed

References1

mentions9