/drain
Drain the ready queue autonomously — the overnight workhorse that
loops /work-ticket → /review-pr → agent-merge gate → post-merge
validation for each safely-classified bead while you sleep.
When to use it
Use /drain when the ready queue is groomed, every candidate bead
carries a safety:* label, and you want the framework to advance
multiple beads through the full implementation cycle without attending
each step. The drain pre-flights production health, snapshots the
queue via bd ready --json, filters by safety class, and works
through the list one bead at a time. Each bead gets its own PR, CI
run, reviewer verdict, merge gate, and production validation before
the next bead starts.
/drain is specifically the merge loop — it moves code from ready
to closed. For promoting a safety:flagged feature from “dark on
production” to “visible to users,” reach for /goal.
V1 trade-off: the operator’s machine must stay on and the Claude Code session must stay open. Remote unattended operation is a future ticket. You make the flag-flip release decision in the morning; drain only ships code, never lifts a feature flag.
Safety classes
The drain filters the ready snapshot by safety class before touching anything:
| Class | Included by default | Flag to include |
|---|---|---|
safety:internal | Yes | (always) |
safety:flagged | Yes | (always) |
safety:reversible | No | --include-reversible |
safety:hot | Never | drain always refuses |
For the full taxonomy and decision tree, see
docs/safety-classes.md.
How it fits in the lifecycle
bd ready is the queue, not a column. The drain reads its snapshot at
run start and does not pull new arrivals mid-run.
What it does
Pre-flight
Before setting the loop condition the drain verifies:
ghCLI is authenticated asva-worker.beads/exists;bd ready --jsonis valid JSON;bd dep cycles --jsonreturns[](cyclic graph = STOP)agent-merge.ymlanddrain-merge-bridge.ymlexist onmainsafety:*label vocabulary is seeded in both the repo (PR labels) and the beads tracker- Every configured external signal (deploy-status service, error-rate baseline) points at this repo — not a different product’s service
- Production baseline is healthy (
node scripts/sentry-baseline.mjsmust be ≤2× the 24-hour median; fails open with an abort notice)
Pre-flight fails closed: any failed check stops the drain and reports to the operator before a single bead is touched.
Snapshot, sort, filter
After pre-flight clears, the drain takes an immutable snapshot:
bd ready --json | jq '[.[]
| select(.issue_type == "task")
| select(((.labels // []) | map(select(startswith("safety:"))) | length) == 1)]'Beads are ordered by priority, then bead id. bd ready is already
blocker-aware — blocked beads never appear in the output. The
hand-rolled topo-sort of the GitHub-Projects era is retired; the
tracker owns the dependency graph.
A drain-run bead is created as the audit anchor:
bd create "Drain run $(date -u +%Y-%m-%dT%H:%MZ)" --type=chore -l drain-runEvery per-bead audit comment and the end-of-run close land on this bead.
Per-iteration cycle (one bead per turn)
For each bead in the snapshot:
-
Claim atomically —
bd update <id> --claim. If the claim fails another worker has it; skip to the next. The claim is the in-progress signal — there is no board to move anything on. -
Invoke
/work-ticket <bead-id>withDRAIN_CONTEXT=true. The worker handles branch, code, CI, and the chained/review-pr; it appliesin-review+pr:<N>labels and copies the bead’ssafety:*label onto the PR (the bead is the label source of truth). -
If CI is red after 3 fix attempts — apply
drain-blockedlabel, post abd commentexplaining what failed, increment the consecutive-failure counter, end the turn. -
If the review verdict is NO-GO (
verdict:no-golabel) — same as above. -
If CI is green and verdict is GO (
verdict:go):- Wait for the full check rollup to reach a terminal conclusion (nothing PENDING / IN_PROGRESS / QUEUED). Time budget: 15 minutes.
- Dispatch the drain-merge bridge via
repository_dispatch. The bridge (.github/workflows/drain-merge-bridge.yml) callsagent-merge.ymlviaworkflow_call— the only path through which the gate permits merges per its safety contract.
-
If the gate denies the merge —
drain-blockedlabel +bd commentnaming the denial reason. -
Wait for the deploy — poll the deploy plane until the merge’s deploy is
live(10-minute budget). Skip entirely when no deploy plane is configured. -
Run post-merge validation — smoke tests or a content-marker curl probe. For
safety:internalbeads the lightweight curl probe is acceptable. -
Verify before close — only after
gh pr view <N> --json state,mergedAtconfirms the merge:bd close <id> --reason "PR #N merged to main via agent-merge gate, deploy validated"Workers never
bd close. Drain does — but only here, only after verified merge.
Audit trail
After each turn the drain posts a bd comment on the worked bead
naming: the drain-run bead id, the outcome (Done / Blocked /
Blocked-Rolled-Back / Merged-NotDeployed / Skipped), the PR number,
the merge commit, and the Sentry baseline before vs after. A one-line
entry also lands on the drain-run bead so the full sequence is
readable in one place.
Wake-up summary
When the loop condition is met (or the drain aborts), the per-bead
outcomes are piped through scripts/emit-drain-summary.mjs and
printed to the operator. The summary covers shipped, blocked,
rolled-back, merged-not-deployed, and skipped beads, plus the
production status and recommended morning actions. The drain-run bead
is closed with a summary reason at the end.
Agent-merge gate
The agent-merge gate (agent-merge.yml) is the autonomous-merge path
in Gemba Flow. The drain dispatches it via drain-merge-bridge.yml
(repository_dispatch → workflow_call); it re-verifies 7 conditions
before every merge:
- The PR body carries a
Bead: <id>citation - The PR carries a
safety:*label - Every check in the rollup is conclusive and green
- The PR is open against
main - No
do-not-mergelabel is present - (Configurable) A second-identity reviewer approval is present
- The dispatch came through the bridge, not a direct
gh pr merge
The gate is opt-in and configurable: REVIEWER_APPROVAL_LOGIN keeps
the second-identity requirement (upstream default); forks that
deliberately empty this config get a loud notice. Operator-merge
remains the shipped default posture — the gate is the autonomous path
for drain runs that have earned the trust.
The drain never calls gh pr merge directly. The gate is the only
path.
State persistence and resumption
The drain writes state to disk at 9 trigger points
(/tmp/drain-state-<drainId>.json) so a /drain --resume can
re-enter after an API interruption without re-doing already-shipped
work. All writes go through scripts/write-drain-state.mjs
(schema-validated, atomic write). See
docs/drain-resumption.md
for the mechanism and edge cases.
Invocation shapes
/drain # safety:flagged + safety:internal; no end time
/drain --until 06:00 # stop at 6 am if the queue is not empty by then
/drain --include-reversible # opt in to safety:reversible beads (rate-limited)
/drain --max-tickets 5 # cap the number of beads processed
/drain --dry-run # emit the plan summary; do not execute
/drain --resume <drainId> # resume an interrupted drain run
/drain --resume # auto-detect the most recent interrupted run--dry-run runs everything through snapshot + sort + filter + plan
emit, then stops without setting the loop condition. It still creates
and closes a drain-run bead so the plan has an audit anchor.
Honest limits (v1)
- Operator-active. The machine must stay on; the session must stay
open. True unattended overnight runs are a future API-bridge ticket
(see ADR-006 in
docs/TECHNICAL-ARCHITECTURE.md). - No human write UI. Board projections
(
.gembaflow-boards/{kanban,techtree}.html) are read-only; board state is mutated viabdin the operator terminal. safety:hotis always refused. Hot beads wait for the operator; drain posts an audit note on each refused bead and continues.safety:flaggedships dark. The drain merges code behind feature flags; it never lifts a flag. The morning release decision belongs to the operator.
Related commands
/groom-backlog— makes beads ready before invoking/drain/work-ticket— the per-bead workhorse drain calls internally/review-pr— auto-chained from/work-ticketon green CI/goal— picks up from where/drainleaves off; promotes a dark-shippedsafety:flaggedfeature to GA
Canonical spec: .claude/commands/drain.md