drain-queue
Cross-brief autopilot — claim the oldest ready brief whose Brief NNN deps are satisfied via atomic push, run /autobuild on each, stop when the queue is empty, blocked, or hits a failure.
- License 许可
- AGPL-3.0
- 最近更新
- 2026-06-08
- Stars
- ★ 1
- Forks
- 0
01 前言 Front Matter
- name
- drain-queue
- description
- Cross-brief autopilot — claim the oldest ready brief whose Brief NNN deps are satisfied via atomic push, run /autobuild on each, stop when the queue is empty, blocked, or hits a failure.
- argument-hint
- [max-briefs | all]
- disable-model-invocation
- true
02 资源文件 1
- 许可证LICENSE.txt1.1 KB
03 SKILL.md 正文
Why This Skill Exists
/dev-pm triages and /dev-architect designs, but dispatching each **Status:** ready brief through /dev-builder → /dev-reviewer → /dev-review → PR open → status flip is mechanical work. /drain-queue treats docs/briefs/*.md files marked **Status:** ready as a self-draining queue. One brief at a time, on its own feature branch, each ending in an open PR. The queue stops on first failure so problems don't cascade.
This is the cross-brief autopilot. It composes /autobuild (within-brief autopilot) in a loop.
Safe to run in parallel across Conductor workspaces — see Concurrency below.
Companion brief: docs/briefs/188-cross-brief-autopilot.md. Doctrine: docs/adrs/035-brief-state-doctrine.md.
Repo Context
- Read
@CLAUDE.md— Ditto conventions, especially §How Work Gets Done and §Critical separation - Read
@docs/dev-process.md§Autopilot — the canonical guide to when to use this vs invoking roles directly - Read
@docs/state.md— current state, blockers, in-flight briefs - Read
@AGENTS.md— repo conventions
Base Branch
The integration branch is main. All of the following reference that branch:
- Claims are pushed to
origin/main - Feature branches are created from
origin/main - PRs target
main
The base branch is never checked out by this skill. Conductor workspaces are git worktrees, and a worktree cannot check out a branch that another worktree already has — so requiring a base-branch checkout would break parallel execution. All operations against base happen via origin/main (a fetched ref) and git push origin <local>:main (push to remote ref by name).
Brief State Model (load-bearing)
Brief state lives in markdown bold-prefix lines, NOT YAML frontmatter. Ditto briefs have no --- delimiters. The state line looks like **Status:** ready (or draft | in_progress | complete). Parse via line-prefix regex (e.g. grep -E '^\*\*Status:\*\*\s+(\w+)'), not via a YAML library. A YAML parser will return zero matches and the queue will appear permanently empty.
The atomic claim is a bold-line edit: **Status:** ready → **Status:** in_progress, committed and pushed to origin/main. Git's non-fast-forward push rejection is the mutex.
Arguments
- No arg or
1→ drain exactly one brief N(positive integer) → drain up to N briefsall→ drain until queue is empty or something fails
Default is 1. Higher values require explicit intent.
Pre-conditions
All must hold before draining starts:
- Working tree clean (
git status --porcelainempty) git fetch origin mainsucceeds (remote is reachable)- At least one
docs/briefs/NNN-*.mdexists onorigin/mainwith**Status:** ready(check viagit ls-tree origin/main docs/briefs/thengit show origin/main:docs/briefs/<file>filtered by Status line — NOT the local working tree, which may be on an unrelated branch) gh auth statusis logged in with at leastreposcopepnpm installis current (if unsure, runpnpm install)
If any fails, report and exit. Never attempt to repair pre-conditions autonomously.
Starting branch doesn't matter. This skill never checks out main. It creates its own throwaway claim branch from origin/main for each iteration.
Execution — Per-iteration loop
Repeat up to the max-briefs limit.
A. Refresh + populate PR maps + GC pass
git fetch origin main— if fetch fails, stop (remote unreachable or auth broken)- Single
gh pr list --state all --json number,state,title,body,mergedAt --limit 200call — parse the result and populate two in-memory maps for this iteration:- openMap:
{slug → {url, number}}for PRs in stateOPEN, whereslugis extracted from the PR body via regexbrief:(\d+-[a-z0-9-]+)(the token autobuild step 8 inserts) - mergedMap:
{slug → {url, number, mergedAt}}for PRs in stateMERGED - PRs in state
CLOSED(not merged) and PRs without abrief:NNN-<slug>token are ignored
- openMap:
- GC pass. List briefs on
origin/mainwith**Status:** in_progress(viagit ls-tree origin/main docs/briefs/+git showon each + grep). Sort by the merge timestamp of their corresponding mergedMap entry (oldest first; no entry → sort last). Process up to 10 briefs per iteration; remainder waits for the next iteration. For each brief in the processed batch:- Slug found in mergedMap: atomic flip —
git checkout -B claim-tmp origin/main, edit the brief file in place changing**Status:** in_progress→**Status:** complete(a single sed or text edit),git commit -am "chore(brief): mark <slug> complete (PR #N merged)",git push origin claim-tmp:main. On non-fast-forward rejection: silently retry on the next iteration (race-loss). The**PR:** <url>line on the brief file (added by autobuild step 9 to the feature branch and arrived onorigin/mainvia the merge) is left in place as informational history. - Slug found in openMap: do nothing (build is in progress somewhere; let it finish)
- Slug not found in either map: log a warning ("brief X is
Status: in_progressbut no matching PR found — orphan; manual cleanup needed"); do NOT flip
- Slug found in mergedMap: atomic flip —
- If
gh pr listfailed: retry once with a 2-second backoff. If still failing, skip the GC pass for this iteration (do NOT flip anything), log a warning, continue to step B.
No checkout of main, no pull. All reads from origin/main directly.
B. Claim the next brief via atomic push (THE MUTEX)
This is the critical step. The push to the shared base branch is a distributed compare-and-swap: only one workspace can successfully push a given claim.
-
Create a fresh throwaway claim branch from the just-fetched base:
git checkout -B claim-tmp origin/main-B(capital) resets the branch if it already exists from a prior iteration — this is the only legitimate "discard local state" operation in the skill, and it only ever discards a previous failed claim attempt or a previous GC commit
-
List candidate briefs on base —
git ls-tree origin/main docs/briefs/→ for eachNNN-*.mdfile,git show origin/main:docs/briefs/<file>and grep^\*\*Status:\*\*\s+(\w+)(use first match only — briefs may legitimately embed**Status:**strings inside HEREDOCs or example blocks, but the canonical state line is always the first occurrence at the top of the file under the**Date:**line) → keep those returningready. Sort by filename (oldest numeric prefix first —001-before002-before100-). If empty, stop — queue empty. -
Pick the first eligible brief. Iterate the sorted list and pick the oldest brief whose
Brief NNNdeps are all satisfied. For each candidate:- Read its
**Depends on:**line (single bold-line, may span continuation; treat the line as the prose immediately after the bold prefix) - Extract dependencies via regex
\bBrief\s+(\d+)\b(case-insensitive). ADRs (ADR-NNN), phases (Phase 14 ...), infrastructure references (credentials table), and parenthetical descriptions are informational and not enforced — ignored. The human gates non-brief blockers via theStatus: readyflip. - For each extracted
Brief NNN, first compute the dependency's full slug by listingdocs/briefs/NNN-*.mdonorigin/main(git ls-tree origin/main docs/briefs/ | grep -E "^[^ ]* docs/briefs/NNN-"); the (single) match's filename without the.mdextension is the dependency's slug (e.g.092-credentials-table). If zero matches → case (a) below. If multiple matches → ambiguous; log a warning and treat as unsatisfied for safety. - Then check satisfaction. The blocker is satisfied iff one of:
- (a) no file
docs/briefs/NNN-*.mdexists onorigin/main, OR - (b) the file has
**Status:** complete, OR - (c) the file has
**Status:** in_progressAND the slug is in this iteration'sopenMap(someone is actively building it; downstream can proceed because that PR will eventually merge), OR - (d) the file has
**Status:** in_progressAND the slug is inmergedMap(race window between merge and GC)
- (a) no file
- Otherwise the blocker is unsatisfied → skip this candidate, try the next
- The first brief with all blockers satisfied is the claim target. Call its full slug
<slug>(e.g.188-cross-brief-autopilot). - If no brief in the list is eligible, stop — queue blocked. Report which briefs are waiting on which unsatisfied blockers so the human can unblock or re-prioritise.
- Read its
-
Belt-and-braces: check the iteration's
openMapfor<slug>→ if a PR already exists (somehow), skip and continue iterating from step 3. -
Make the claim commit on
claim-tmp:- In-place edit the brief file: change the line
^\*\*Status:\*\* ready$to**Status:** in_progress git add docs/briefs/<slug>.mdgit commit -m "chore(brief): claim <slug>"
- In-place edit the brief file: change the line
-
git push origin claim-tmp:main— pushes the localclaim-tmpbranch to update remotemain. Git's non-fast-forward check is what makes this atomic.- Push accepted → you own this brief. Proceed to step C.
- Push rejected (non-fast-forward) → another workspace won the race. Recover: go back to step A. The race-loss recovery is automatic — the next iteration's
git fetch+git checkout -B claim-tmp origin/mainrebuilds the claim branch on top of whatever the winner pushed, the GC + PR-map step re-runs (catches any new merges), and step 3's eligibility check re-runs (so blocker shifts are re-evaluated). - More than 5 consecutive lost races → stop and report. Something is wrong (too many workers, or queue is nearly empty).
C. Rename the claim branch to the feature branch
The claim is now committed on claim-tmp and pushed to base. Rename in place — no new checkout needed.
git branch -m claim-tmp feature/<slug>- The branch already has the
**Status:** in_progressflip committed and is based on the updated base —/autobuildruns from this branch
D. Run the within-brief autopilot
- Invoke the
/autobuildskill /autobuildhandles: pre-flight hard-stops →/dev-builder→ fresh-subagent/dev-reviewer+/dev-review→ fix P0/P1 → commit + push feature branch → open PR → add**PR:** <url>line to the feature branch → report
E. Handle result
- Success: PR is open. Feature branch contains the implementation + the
**PR:** <url>line on the brief file. Proceed to step F before continuing the loop. - Failure:
/autobuildstopped. Brief is at**Status:** in_progressonorigin/main(claim already happened in step B). Do NOT continue the queue and do NOT run step F. Report the failure including the feature branch name so a human can recover. - Pre-flight hard-stop (drizzle journal or DB migration in §What Changes): same as failure — brief stays
in_progress, queue stops, report.
F. Final-pass /dev-review --fix (mandatory on success, up to 3 recursions)
/autobuild's step 5 runs /dev-review as a pre-PR gate via fresh subagent, fixing P0/P1 findings — but P2/P3 may accumulate. This step closes that loop: run /dev-review --fix recursively until the branch is clean, or escalate to human review after 3 passes.
Why recursion: a single --fix pass can introduce new findings as a side-effect of the fix, or leave residual P2/P3 issues that need another pass to surface once the top-level noise is cleared. Three passes is almost always enough; more usually means architectural and needs a human.
Still on the just-shipped feature branch:
- Set
attempt = 1,max_attempts = 3. Track the finding set across attempts to detect non-convergence. - Loop:
a. Invoke
/dev-review --fixagainst the branch's diff vsorigin/main(Ditto usesmainas the integration branch — different from the Catalyst-default base; the patched/dev-reviewskill already targetsorigin/main) b. Re-run the full/dev-builderverify list per.claude/commands/dev-builder.mdlines 81-86 (type-check +pnpm test+pnpm test:e2e+pnpm test:e2e:auto). Re-running only type-check would silently weaken the contract — fixes that break runtime behavior would slip through. Lint is omitted (this repo has no lint script). If any verify step regresses and/dev-review --fixcan't resolve it in this same pass, stop the drain — treat as a failure per the failure branch of step E. c. If/dev-review --fixmade changes:- Stage only the files it touched
- Commit:
chore(review): apply /dev-review --fix findings (pass <attempt>) - Push to the same feature branch so the open PR updates
d. Re-invoke
/dev-review(report-only, no--fix) to check residual findings - Clean (no findings): exit the loop
- Only unfixable findings remain (ambiguous, architectural, out-of-scope): exit the loop and go to step 3
- Fixable findings remain AND
attempt < max_attempts: incrementattempt, go to 2a - Fixable findings remain AND
attempt == max_attempts: exit the loop, go to step 4 (human escalation) e. Non-convergence guard: if the exact same finding set appears in two consecutive passes with no reduction, treat as non-convergent — go to step 4 even if attempts remain
- If unfixable findings remain after a clean convergence, post them as a single comment on the PR via
gh pr comment <url> --body "..."so the human reviewer sees them. Continue the drain loop normally. - Human escalation (max attempts reached or non-convergent):
- Do NOT continue the drain
- Post a PR comment listing unresolved findings, number of passes attempted, and whether the failure mode was "didn't converge" or "capped at 3 passes with findings still present"
- Add a note to the brief file (still at
**Status:** in_progress) under a## Drain-queue escalationsection - Report to the user in the final drain output. Soft stop — not a failure that corrupts state, but enough of a signal that a human should intervene before more briefs pile on top.
Only after step F exits cleanly (step 2d "Clean" or step 3 "unfixable only") does the loop continue to the next brief. Step 4 stops the drain.
The skill never returns to or checks out main — it stays on the most recent feature branch when the loop ends.
Stop conditions
- Queue empty (no
**Status:** readybriefs on base) - Queue blocked (every remaining ready brief has at least one unsatisfied
Brief NNNdep) max-briefsreached- Any
/autobuildinvocation fails (including pre-flight hard-stops) - Step F
/dev-review --fixintroduces unresolvable type-check regressions - Step F escalates to human review (3 passes without convergence, or non-convergent finding set)
- More than 5 consecutive lost claim races
- Any pre-condition breaks mid-loop
Concurrency — Running Multiple Workspaces in Parallel
Safe. This is the headline feature of the atomic-push claim, and the no-checkout design is what makes it work across Conductor's worktree model.
- N Conductor workspaces each running
/drain-queuewill each build aclaim-tmpfromorigin/mainand try to push it back to base - Git's non-fast-forward rejection ensures exactly one winner per brief
- Losers loop back to step A, fetch the winner's claim, rebuild
claim-tmp, and pick the next brief — noreset --hardneeded because the loser was never on a shared branch - Each workspace stays on its own branch the whole time, so worktrees never compete for the same checkout
- The single
gh pr listcall per iteration is one rate-limit token per iteration per workspace — comfortable within GitHub's 5000/hour limit even at N=8 workspaces - Expected throughput: near-linear in N, limited by CI concurrency, GH PR API rate limit, and claim-race retries
Known limitation: crash recovery. If a workspace pushes a claim but crashes before opening a PR, the brief is stuck at **Status:** in_progress on origin/main with no owner. The GC pass will catch this on the next iteration (slug not in either PR map → "orphan" warning logged). Manual recovery: human edits the brief back to **Status:** ready (or complete / draft as appropriate) via a PR.
Cost & throughput
A single /autobuild run has substantial wall-clock and LLM-token cost: /dev-builder (multiple LLM turns + pnpm test:e2e Playwright run + pnpm test:e2e:auto) + fresh-subagent /dev-reviewer + fresh-subagent /dev-review + up to 3 /dev-review --fix recursions. Empirical estimate: 30–60 minutes wall-clock per brief; LLM token cost on the order of $5–$30 per brief depending on complexity.
/drain-queue all against a 50-brief queue therefore commits to 25–50 compute-hours plus $250–$1500 in LLM spend, plus ~50 PRs requiring human merge review. Recommend starting with /drain-queue 1 for the first session and only escalating to /drain-queue all once trust and rate-limit headroom are confirmed.
Guardrails
- Never check out
main. It would block other workspaces from running this skill. Useorigin/main(fetched ref) for reads andgit push origin <local>:mainfor writes. - Never merge PRs.
/autobuildenforces this. Merge is the human taste gate. - Never auto-promote
Status: draft→Status: ready. That's/dev-pm's job (and ultimately a human's)./drain-queueonly operates on briefs already markedready. - Never edit briefs other than
**Status:**and the GC pass'scompleteflip. Step C edits exactly one line. The GC pass edits exactly one line. No other brief mutations from this skill. - No
--force,--no-verify, orreset --hardanywhere in this skill. Race-loss recovery is handled bygit checkout -B claim-tmp origin/main, which discards only the prior claim attempt or GC commit. - Stop on first real failure (as opposed to race-loss, which is recoverable). Broken builds in one brief can mask real failures in the next.
- Cap your run — default 1.
allandN>1are explicit opt-ins. - Step F
/dev-review --fixstays scoped to the feature branch's diff (git diff origin/main). It must not touch files unrelated to the brief. If/dev-reviewflags issues outside the diff, surface them in the final report — don't fix them. - Trust boundary:
**Status:** readyIS the trust boundary for autonomous build. Anything marked ready will be implemented and PR-opened by this skill. Pre-flight hard-stops (in/autobuildstep 2) cover DB-related risk only; everything else (package.jsondeps,.github/workflows/,.env, configs) relies on theStatus: readyhuman gate. Seedocs/adrs/035-brief-state-doctrine.md.
After draining
Report to user:
- How many briefs successfully shipped (PR open)
- Open PR URLs
- Per brief: step F outcome — number of
--fixpasses (1–3), what each pass fixed (one-line summary), final state (clean / unfixable-only / escalated) - Any brief where step F hit the 3-pass cap or a non-convergent finding set — name the brief, the PR, and the unresolved findings
- Any brief that stopped the drain and why (build failure, pre-flight hard-stop, etc.)
- Any orphan warnings from the GC pass (briefs
in_progresswith no matching PR) - How many
**Status:** readybriefs remain onorigin/main, and how many are currently blocked by unsatisfiedBrief NNNdeps (name the blockers) - How many lost claim races occurred (high numbers suggest too many parallel workers)
Suggest the user review the PRs. The merge decision is theirs.
04 同仓库更多 Skills
drain-queue
Cross-brief autopilot — claim the oldest ready brief whose Brief NNN deps are satisfied via atomic push, run /autobuild on each, stop when the queue is empty, blocked, or hits a failure.
dev-review
Post-coding exhaustive review — catches integration bugs, runtime crashes, performance issues, and convention drift across all changed files
autobuild
End-to-end autopilot for a single brief — pre-flight, /dev-builder, fresh-subagent reviewers, push, open PR, mark **PR:** line on feature branch. Stops at the human merge gate.
dev-review
Post-coding exhaustive review — catches integration bugs, runtime crashes, performance issues, and convention drift across all changed files
autobuild
End-to-end autopilot for a single brief — pre-flight, /dev-builder, fresh-subagent reviewers, push, open PR, mark **PR:** line on feature branch. Stops at the human merge gate.
typeset
Improves typography by fixing font choices, hierarchy, sizing, weight, and readability so text feels intentional. Use when the user mentions fonts, type, readability, text hierarchy, sizing looks off, or wants more polished, intentional typography.