feat(queue): record a wait on a condition outside the queue - #68
Merged
Conversation
`block` took only `--on <ref>`, so a task held by something that is not a task had no state it could be recorded in. On 2026-09-11 vending-machine-egress-resume/01-vm-identity-reconciliation was ready by every record fleet keeps and unrunnable in fact — its brief's first instruction reads Azure and `az` was not authenticated. `plan` listed it as ready and the reconciler correctly woke the lead to dispatch it; the only honest answer left was to refuse in conversation and leave the record silent. A blocker may now name a CONDITION instead of a task, with its own closed set of kinds and the same required `--why`. It puts the task in `waiting` and out of `ready`, so `notify_lead.py` stops counting it, and nothing clears it but `block --clear` naming it back — no timer, no `collect`, no `reap`.
…r in the queue README
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Give a task held by something OUTSIDE the fleet queue a state it can actually be recorded in, so the reconciler stops reporting it as work somebody could dispatch.
The incident: on 2026-09-11 vending-machine-egress-resume/01-vm-identity-reconciliation was ready by every record fleet keeps and unrunnable in fact — its brief's first instruction reads Azure and
azwas not authenticated.queue.sh blockrequired--on <ref>("the task this one waits for"), so nothing could be written down.planlisted the task as ready and scripts/lib/notify_lead.py correctly typed the reconciler's one line into the lead's terminal telling it to dispatch. The only honest response was to refuse in conversation and leave the record saying nothing.What was built, and the decisions behind it:
blocked_byentry now names EITHERtask:orcondition:, never both;--onand--conditionare a required mutually-exclusive argparse group. The existing task-to-task form is unchanged, deliberately, including clearing onlandedrather thandoneand printing UNCLEARABLE when the upstream is stuck/failed/abandoned.otherand the set would have stopped saying anything.--whystays required: a wait nobody explained is what this repo already refuses.--kind's argparsechoicesis the union because argparse has not read--on/--conditionyet; cmd_block refuses a kind from the wrong set, where it can say which set applies.blocker_clearedanswers False for it forever; onlyblock <ref> --clear --condition '<text>'removes one. No timer, nocollect, noreap, no inference from a later dispatch succeeding — a condition that expired on its own would put back exactly the silence the defect is about. reconcile.sh remains unable to clear one, which follows from the existing rule that the loop calls only watch/collect/shepherd/refuel/plan; reconcile-selftest.sh was left alone so it keeps asserting that.ref|kindfield with a!leader (no task ref can start with one); the shell probe flattens,and|out of the free-prose condition text, and the Lua side runs it throughscalar()because PyYAML quotes free prose containing ": " on disk and those quotes are the writer's, not the operator's.--conditionis refused rather than falling through to the--onpath: the flag being given satisfies argparse, but an empty wait is one nobody named and--clearcould never name back.add --touchesremains the answer and the existing refusal still refuses; the selftest asserts--kind file-overlapis refused on the condition form too.Scope constraints the user set: do not change dispatch's behaviour on a spawn failure and do not touch title validation — a parallel task is changing both. Keep changes contiguous in scripts/lib/queue.py and add the new selftest section at the END of scripts/queue-selftest.sh rather than interleaving, so the two diffs rebase cleanly.
Verification: ./scripts/check.sh is green, including its queue, reconcile and pane sections. queue-selftest.sh gained sections 20/20a/20b (about 50 assertions) and pane-selftest.sh gained five. AGENTS.md, scripts/queue.sh's usage header and .agents/skills/fleet-queue/SKILL.md section 3 all describe the new form.
Note on this run: an earlier pipeline run on this branch reached
documentand its agent exited on an account usage limit. Its review fix commit (193021c, "refuse a blank --condition and unquote it in the pane") was recovered withaxi sync --recoverand is part of the history being validated here.What Changed
queue.sh blockgains a second, mutually-exclusive form:--condition '<what>'beside--on <ref>, validated against its own closedCONDITION_KINDSset (missing-credential,awaiting-approval,closed-window,broken-dependency,undecided,other) with--whystill required. A blank--conditionis refused rather than falling through to the--onpath, and--kind file-overlapstays refused on both forms.blocker_clearedanswersFalsefor it forever,find_cycleskips it since it is not an edge, and onlyblock <ref> --clear --condition '<text>'removes one — which refuses when the task records no such condition.checkvalidates eachblocked_byentry against the set matching its form and rejects one naming both a task and a condition.blocker_view/blocker_line:plan,list,show, the run-log facts block, and the TUI pane, which draws a condition as⊘instead of↳, carries it in the existingref|kindfield behind a!leader, and unquotes it throughscalar().notify_lead.pygained header prose only — it still derives nothing fromplan --json. Docs updated inAGENTS.md,queue.sh's usage header, the queueREADME.mdand the fleet-queue skill;queue-selftest.shgained sections 20/20a/20b andpane-selftest.shfive assertions.Risk Assessment
✅ Low: Purely additive second blocker form with no behaviour change to the existing task-to-task path, every reader of
blocked_byupdated to the safe accessor, both prior review decisions verified present and correct, and roughly fifty new executable assertions across queue-selftest and pane-selftest covering the refusals, all five surfaces, and the incident case itself.Testing
Baseline
./scripts/check.shwas already green; I re-ran only the two targeted sections this change touches (check.sh queue pane, exit 0) and then drove the feature myself against the real CLI in a throwaway queue rather than relying on the selftests. The incident replays correctly: with the condition recorded,planmoves the task into the waiting block with the condition, the reason and "onlyblock --clearreleases it",plan --jsondrops it fromready, and the reconciler's line into the lead names one task instead of two. Nothing in the loop releases it — collect, reap, watch, shepherd and refuel all ran with conditions standing and the waits survived, while the ordinary task-form blocker still cleared on the land and not on the conclusion. Both recorded review fixes reproduce as failures on the pre-fix commits:block --condition ''raisedTypeError: argument of type 'NoneType' is not iterableon c694a36 and is now a plain refusal (exit 2), and the pane drew⊘ 'security review #42: …'with the writer's YAML quotes on c694a36 and draws it unquoted now. Visual evidence is a rendered-text artifact (pane-render.html) rather than a screenshot of a live thurbox: the pane is a TUI plugin, and putting it on a real screen would mean installing into the operator's thurbox config outside this worktree, so I fed the pane's own shell probe over live records into its offline renderer instead — the same rows, minus real colours.az, andplanstops calling it readyqueue.sh block … --condition 'az is authenticated for the mazet tenant' --kind missing-credential --why …thenqueue.sh planin an isolated FLEET_QUEUE_DIR — evidence/01-record-condition-and-plan.…queue.sh plan --jsonover the live queue lists only 02-document-the-tables as ready (evidence/02-reconciler-wake-line.txt); piping that intoscripts/lib/notify_lead.pytypes "1 task(s) ready … — v…landed, ready grows by one and not by two, an…block --clear --conditionnaming the exact text, and nothing elseTypeError: argument of type 'NoneType' is not iterable; the target commit refuses with the named-wait paragraph and exit 2, on both the record and the…--kind file-overlapon the condition form is refused and the refusal still points atadd --touches; the task record is unchanged afterwards--conditionis refused listing the condition set, a condition kind on--onis refused listing the task set plus a pointer to--condition, both flags together give arg…blocker_lineeverywhere;fleet-status.sh --jsongives"status": "outside"with"cleared": false; the run log writes "waits on a condition outside the queue — ……!'security review #42: somebody has to answer it'|awaiting-approval(evidence/06); renderin…,and|inside the condition text, stays readable everywhereshowandplankeep the text verbatim, the probe flattens the two field separators to spaces, and the pane draws one ⊘ row rather than splitting it into a bogus second edgedispatch --dry-run <ref>exits 2 quoting the condition line; baredispatch --dry-runplans only the two unheld tasksqueue check: ok — 1 topic(s), 4 task(s)) and evidence/13: a blocker naming both forms, a kind from the wrong set, and a missing reason are each reported, exit 1showreads "was held by awaiting-approval outside the queue (…); this task concluded, so it holds nothing" and the ready set is unaffectedEvidence: TUI queue pane rendered from a live queue — after vs. before the unquoting fix, at three widths
Evidence: Recording the condition, and the plan that follows
$ queue.sh block 01-vm-identity --condition "az is authenticated for the mazet tenant" --kind missing-credential --why "..." vending-machine-egress-resume/01-vm-identity waits on a condition outside the queue (missing-credential): az is authenticated for the mazet tenant Nothing clears this but you: ./scripts/queue.sh block vending-machine-egress-resume/01-vm-identity --clear --condition 'az is authenticated for the mazet tenant' $ queue.sh plan ready: 1 task(s) ... vending-machine-egress-resume/02-document-the-tables waiting: 2 task(s) — each held by a durable, recorded blocker vending-machine-egress-resume/01-vm-identity held by missing-credential outside the queue (az is authenticated for the mazet tenant) — onlyblock --clearreleases it: the brief's first instruction reads Azure and az account show failsEvidence: The reconciler's wake line: one task ready, not two
woke Gate Control: 1 task(s) ready — vending-machine-egress-resume/02-document-the-tables --- what was typed into the lead terminal --- fleet reconciler: 1 task(s) ready and nothing will dispatch them — vending-machine-egress-resume/02-document-the-tables. Run ./scripts/queue.sh dispatchEvidence: collect + reap land the upstream task; the condition is untouched
Evidence: Every reader agrees: list, show, fleet-status text
Evidence: fleet-status --json blocker view, the task.yaml on disk, and the run log line
Evidence: The pane's own shell probe run over the live queue
Evidence: Pane rendered from that probe output, widths 60 and 30
Evidence: Pane quoting regression: before (c694a36) vs after (193021c)
=== BEFORE the review fix (c694a36 interface/fleet_queue.lua), same live probe output === ⊘ 'security review #42: somebody has to answer… approval === AFTER (193021c) === ⊘ security review #42: somebody has to answer … approvalEvidence: Blank --condition: traceback before the fix, refusal after
### BEFORE (c694a36): TypeError: argument of type 'NoneType' is not iterable ### AFTER (193021c): queue: --condition is the wait itself, in words: --condition 'az is authenticated for the mazet tenant'. Nothing removes one butblock --clear --conditionnaming it back, so a blank one is a wait nobody could name and nobody could release. exit: 2Evidence: Pre-fix reproduction of the blank-condition traceback
Evidence: Every refusal path of the new form (wrong set, both flags, neither, file-overlap, no --why)
Evidence: Clearing is a hand: wrong text refused, exact text releases
Evidence: dispatch refuses the condition-held task by name; check calls the record valid
Evidence: check catches a hand-edited blocker naming both forms
Evidence: Both blocker forms on one task, with `,` and `|` in the condition text
~/.no-mistakes/evidence/01M282XZCR3BX0G6X5H1HVNTZ7/15-reconciler-after-the-hand-clears.txt)~/.no-mistakes/evidence/01M282XZCR3BX0G6X5H1HVNTZ7/16-condition-on-a-concluded-task.txt)~/.no-mistakes/evidence/01M282XZCR3BX0G6X5H1HVNTZ7/17-loop-commands-release-nothing.txt)Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
scripts/lib/pane_harness.lua:288- The new!-leader wire encoding has a producer (the awk rule in interface/fleet_queue.lua:413) and a consumer (edges()at interface/fleet_queue.lua:490), and only the consumer is exercised.pane_harness.luainjects the probe's tab-separated output directly intothurbox.runs["fleetqueue:s1"].stdout(line 341), so the shell/awk PROBE never runs undercheck.sh pane; the fixture hard-codes"!'az login: for the tenant'|missing-credential"as what the probe would emit. I checked the offsets by hand and they are right (- condition:is 13 chars,substr($0, 14)is correct), so this is not a live defect — but if the anchor or thegsub(/[,|]/)flattening drifted, every condition row would render wrong and the pane checks would stay green. Pre-existing in kind: the task-blocker encoding, the publish block and the archived count are all fixtured the same way. Noting it rather than proposing a change; closing it would mean building a probe-execution harness, which extends the change well past its intent.scripts/lib/queue.py:1824- The two--clearforms now answer the same question differently.block <ref> --on <other> --clearwhen no such blocker exists prints "0 blocker(s) cleared" and exits 0 (scripts/lib/queue.py:1776);block <ref> --clear --condition '<text>'when the text does not match raises "records no condition …" and exits non-zero (scripts/lib/queue.py:1825-1829). Concrete sequence: an operator who mistypes the condition by one character gets a refusal naming what is actually held, while the same mistype on--onis silently accepted as a no-op. The condition form's behaviour is the better one — an exact free-prose match genuinely needs to say when it missed, which is why the selftest asserts it at scripts/queue-selftest.sh:5293 — so the divergence is a deliberate improvement rather than a defect, and the remedy (making the task form refuse too) would change behaviour the intent explicitly marks as unchanged. Flagging the inconsistency only; no action.✅ **Test** - passed
✅ No issues found.
az, andplanstops calling it readyqueue.sh block … --condition 'az is authenticated for the mazet tenant' --kind missing-credential --why …thenqueue.sh planin an isolated FLEET_QUEUE_DIR — evidence/01-record-condition-and-plan.…queue.sh plan --jsonover the live queue lists only 02-document-the-tables as ready (evidence/02-reconciler-wake-line.txt); piping that intoscripts/lib/notify_lead.pytypes "1 task(s) ready … — v…landed, ready grows by one and not by two, an…block --clear --conditionnaming the exact text, and nothing elseTypeError: argument of type 'NoneType' is not iterable; the target commit refuses with the named-wait paragraph and exit 2, on both the record and the…--kind file-overlapon the condition form is refused and the refusal still points atadd --touches; the task record is unchanged afterwards--conditionis refused listing the condition set, a condition kind on--onis refused listing the task set plus a pointer to--condition, both flags together give arg…blocker_lineeverywhere;fleet-status.sh --jsongives"status": "outside"with"cleared": false; the run log writes "waits on a condition outside the queue — ……!'security review #42: somebody has to answer it'|awaiting-approval(evidence/06); renderin…,and|inside the condition text, stays readable everywhereshowandplankeep the text verbatim, the probe flattens the two field separators to spaces, and the pane draws one ⊘ row rather than splitting it into a bogus second edgedispatch --dry-run <ref>exits 2 quoting the condition line; baredispatch --dry-runplans only the two unheld tasksqueue check: ok — 1 topic(s), 4 task(s)) and evidence/13: a blocker naming both forms, a kind from the wrong set, and a missing reason are each reported, exit 1showreads "was held by awaiting-approval outside the queue (…); this task concluded, so it holds nothing" and the ready set is unaffected./scripts/check.sh./scripts/queue.sh topic add+ 4 ×add --brief-fileinto an isolatedFLEET_QUEUE_DIR, thenblock <ref> --condition 'az is authenticated for the mazet tenant' --kind missing-credential --why …./scripts/queue.sh planandplan --jsonbefore and after recording the condition./scripts/queue.sh plan --json | python3 scripts/lib/notify_lead.py --state-dir …(reconciler wake line), twice, with a capture shim standing in for the thurbox terminal./scripts/queue.sh attach+collect+reapover a merged pull request while the condition stood./scripts/queue.sh watch --for-secs 1,shepherd --dry-run,refuel --dry-runwith two conditions standing./scripts/queue.sh list,show <ref>,./scripts/fleet-status.sh,./scripts/fleet-status.sh --json, and the generated run log underorchestration/runs/the pane's ownPROBEshell text, extracted verbatim frominterface/fleet_queue.luaand run over the live queue, then rendered throughscripts/lib/pane_harness.luaat widths 30/60/70 — against both193021candc694a36copies of the paneblock --condition '',--condition ' ',--clear --condition ' ',--kind file-overlap, a task kind on--condition, a condition kind on--on, both flags, neither flag, and--conditionwith no--whyblock --clear --conditionwith text the task does not hold, then with the exact text./scripts/queue.sh dispatch --dry-run <condition-held ref>and baredispatch --dry-run./scripts/queue.sh checkon a valid record and on a hand-edited one naming both a task and a condition, a kind from the wrong set, and no reason./scripts/check.sh queue pane(queue-selftest §20/20a/20b and pane-selftest), exit 0✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.