fix(queue): refuse a title dispatch cannot spawn, and say what a spawn failure was - #70
Merged
Conversation
…n failure was
A title becomes the worker's thurbox session name, and thurbox refuses a name
it could not make a path segment of. `add` took `Rust crate, CI/CD and the
profile model`; `dispatch` then died with nothing but thurbox's exit status,
and the repair was a hand-edit of `title` in task.yaml and the brief's H1.
`add` now asks thurbox's own rule — empty, over the byte cap, leading `.`,
or holding `/`, `\` or `..` — about the RENDERED name, since that is the
string `session create` is handed: the glyph goes in front and the title is
cut to the cap first. Nothing thurbox accepts is refused.
And `dispatch` reports what thurbox said. It read only stderr; thurbox-cli
prints its structured failure on STDOUT (`{"error": ...}` under `--json`),
so the reason was being discarded. Both streams are read and the `error`
field unwrapped. A failed spawn still leaves that task queued, still lets the
rest of the set go out, and still does not double-spawn on a re-run.
…port spawn failures
The review found that `FIXER_TITLES["conflicting"]` carries the base branch into a session name, and a base branch holds `/` as a matter of course — the same defect this branch fixes for an operator's `--title`, on the one path where nobody can refuse it. It is a real finding and it is out of scope: this task's brief forbids changing `shepherd`, and the operator asked for exactly the two defects they hit. So the fixer title, `spawn_fixer` and the selftest section about them go back to what they were. The finding is reported to the lead in this task's result.md instead, to be queued as its own task. The skill note the same round added stays: it documents `add`'s new refusal, which is this task's own work.
…session naming reference
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
Fleet task queue-refusals-and-waits/01-refuse-unspawnable-titles. Fixes the first of two queue defects the operator hit on 2026-09-11; a sibling worker is fixing the second in parallel on feat/record-a-wait-on-a-condition (both touch scripts/lib/queue.py and scripts/queue-selftest.sh — an expected rebase, not a dependency).
The defect: a fleet task's title becomes the thurbox session name at dispatch, and thurbox refuses a session name it could not make a path segment of.
queue.sh addaccepted --title 'Rust crate, CI/CD and the profile model';dispatchthen died with nothing but thurbox's exit status echoed back, and the only repair was hand-editing 'title' in task.yaml and the brief's H1, because the queue has no retitle verb.Two halves, both here.
addrefuses a title that cannot become a session name, following the precedent already in the same command (it refuses a --branch no worktree could be cut for). The rule is MIRRORED from thurbox's own paths::validate_safe_name — empty, over the byte cap, a leading '.', or containing '/', '\' or '..' — read out of the thurbox source, and deliberately NOT widened: every character thurbox accepts must still be accepted, because a title is human-facing text. The check is on the RENDERED session name (worker glyph in front, cut to thurbox's 64-BYTE cap), not the raw title, because the rendered name is the string handed tosession create.dispatchreports what the spawn failure was. The brief assumed thurbox wrote the reason to stderr; it does not — I probed the real thurbox-cli and found it prints its structured failure on STDOUT as {"error": ...} under --json and exits non-zero, which is why reading only stderr left 'returned non-zero exit status 1'. Both streams are now read and thurbox's 'error' field unwrapped. Surrounding behaviour is deliberately unchanged: a failed spawn leaves that task queued, the rest of the set still goes out, and a re-run does not double-spawn.SCOPE IS SET BY THE OPERATOR'S BRIEF AND THE LAST REVIEW ROUND CROSSED IT — please do not raise it again. The brief's hard constraints are: do not change the session name rendering (glyph, prefix, 64-byte truncation), because thurbox cannot rename a live session; do not widen the rejected set beyond what thurbox rejects; scripts/queue.sh stays the only writer over the queue's records; and DO NOT ADD A RETITLE VERB AND DO NOT CHANGE block, plan, collect, shepherd, reap OR refuel — the operator asked for exactly the two defects they hit. The previous review round found that FIXER_TITLES['conflicting'] = 'Rebase PR #{n} onto {base}' carries a base branch (and so a possible '/') into a session name the shepherd writes itself, and fixed it along with spawn_fixer's error note. That finding is REAL AND CORRECT, and it is out of scope here: commit 10397ac deliberately reverts it, and this task's result.md reports it to the lead so it can be queued as its own task. Treat the shepherd/FIXER_TITLES/spawn_fixer state on this branch as the operator's decision, not as an oversight or an incomplete fix.
Verification: scripts/queue-selftest.sh gained section 20 at the END of the file (deliberately at the end, so the sibling worker's section and mine do not interleave on rebase). It asserts the refusal end-to-end the way section 16d asserts the --branch refusal (refused, names the offending character, creates nothing), the rendered-vs-raw distinction in both directions, thurbox's own unsafe_names_are_rejected case list plus ordinary titles that must still be accepted, and the spawn-failure message against a stubbed thurbox-cli failing on stderr and one failing with thurbox's real JSON on stdout — plus the three existing behaviours that must not break. scripts/queue.sh's usage header was updated because that header is the full usage, and the fleet-queue skill gained a note about the new refusal. ./scripts/check.sh is green end to end, including its queue section.
What Changed
queue.sh addnow refuses a--titlethat could not become a thurbox session name, alongside its existing--branchrefusal:session_name_refusal()inscripts/lib/queue.pymirrors thurbox'spaths::validate_safe_name(empty, leading.,/,\or.., over the byte cap) and widens it by nothing, judging the rendered name — worker glyph in front, cut to the 64-byte cap — rather than the raw title, and creating nothing when it refuses.dispatchreports what thurbox actually said when a spawn fails. The newspawn_failure()reads both stdout and stderr and unwraps thurbox's{"error": ...}JSON — which it prints on stdout, not stderr — so a failed spawn no longer surfaces asreturned non-zero exit status 1; the surrounding behaviour is unchanged (the task staysqueued, the rest of the set still goes out, a re-run does not double-spawn).scripts/queue-selftest.shgained section 20 at the end of the file, covering the refusal end-to-end, the rendered-vs-raw distinction in both directions, thurbox's ownunsafe_names_are_rejectedcase list plus titles that must still be accepted, and the failure message against stubs failing on stderr and on stdout-JSON;scripts/queue.sh's usage header and thefleet-queue/thurbox-sessionskills were updated to match.Risk Assessment
✅ Low: Well-bounded two-part fix: the new refusal is a verified faithful mirror of thurbox's own validate_safe_name applied to the exact string dispatch sends, the spawn-failure reporting is a strict improvement across all four caught exception types with surrounding dispatch behaviour unchanged, the fixer revert is byte-clean against the base as the intent states, and section 20 covers both halves end to end without source-content assertions.
Testing
The configured
./scripts/check.shwas already green as the baseline. On top of that I drove the queue CLI end to end against an isolated queue directory and, crucially, against the realthurbox-cli2.19.9 installed on this machine: I confirmed thurbox genuinely refuses the operator's title and prints its reason as JSON on stdout with an empty stderr, cross-checked the new refusal rule against the real thurbox on ten titles (ten agreements, no disagreement, nothing thurbox accepts refused), exercised the rendered-versus-raw distinction in both directions with the worker mark on and off, and reproduced the original failure message with the base-commit library and the new one with this branch on the same task. I also drove a live two-task wave on a real repo to show a failed spawn leaves its task queued while the other still goes out, that a re-run does not double-spawn, and that a retitle plus re-run sends it. The stderr-only spawn failure was NOT driven live and is reported untested: the real thurbox speaks on stdout, so that branch has no live product path here. Evidence is CLI transcripts rather than screenshots because this change has no rendered UI surface — the TUI queue pane is untouched by the diff. Worktree left clean and the operator's fleet restored to its original sessions../scripts/queue.sh add title-guard ci-cd --title 'Rust crate, CI/CD and the profile model' ...exits 2 naming '/' and quoting the rendered name; the topic directory still holds only PROMPT.md and to…thurbox-cli session create --json --name '🚀 Rust crate, CI/CD and the profile model' --repo-path /tmp/tbxprobewith streams split: exit 1, stdout carries the JSON error, stderr empty — evidence file…session_name_refusaland realthurbox-cli session createover the same ten rendered names (each accepted one created then deleted): 10 agreements, 0 disagreements — evidence fi…queue.sh addruns under FLEET_GLYPH_ROOT with GLYPHS=on then GLYPHS=off, and the same two names put to real thurbox-cli ('🚀 .hidden agenda' accepted, '.hidden agenda' refused with "Name cannot s…queue.sh showafter the failed dispatch reports state: queued, session: None, dispatch: no session dispatched; after a hand-retitle the re-run spawned session c0bc6d37 — evidence files 05 and 10queue.sh add --title 'Windows C:\paths and the model'and--title 'Wait.. then retry'both refused naming the character; both rendered names refused by real thurbox-cli with "Name contains invalid…PATH=<no thurbox-cli> python3 scripts/lib/queue.py dispatch ...prints "spawn failed: [Errno 2] No such file or directory: 'thurbox-cli'" — evidence file 07-spawn-failure-fallback-no-thurbox.txtqueue.sh dispatch wave/10-goes-out wave/11-boomagainst a real git repo and real thurbox: 11-boom reported thurbox's refusal and stayed queued, 10-goes-out got session f1c72608 and answered its…./scripts/queue.sh --helprenders theaddentry documenting that the title becomes the session name, wearing the worker's mark and cut to the byte cap, carrying no '/', '', '..' or leading '.' —…Evidence: add refuses the operator's real title, names the offending character, creates nothing
queue: --title 'Rust crate, CI/CD and the profile model' cannot become a session name: thurbox is asked to create '🚀 Rust crate, CI/CD and the profile model', and it contains '/', which thurbox refuses. ... exit=2 $ ls $FLEET_QUEUE_DIR/title-guard/ PROMPT.md topic.yamlEvidence: the real thurbox-cli refuses that name and prints its reason on STDOUT
exit=1 --- stdout --- {"error":"Name contains invalid characters","suggestion":"the command ran and failed; ..."} --- stderr ---Evidence: the queue's rule vs the real thurbox on 10 titles — 0 disagreements
queue.sh add real thurbox agree rendered session name REFUSE REFUSE yes '🚀 Rust crate, CI/CD and the profile model' ACCEPT ACCEPT yes '🚀 Rust crate, CI-CD and the profile model' ACCEPT ACCEPT yes '🚀 .hidden' REFUSE REFUSE yes '🚀 foo/bar' REFUSE REFUSE yes '🚀 foo..bar' REFUSE REFUSE yes '🚀 foo\bar' ACCEPT ACCEPT yes '🚀 Ship v2.1: metrics & alerts @ 99% — done?' ACCEPT ACCEPT yes '🚀 Réécrire le lecteur ~ étape 1' ACCEPT ACCEPT yes '🚀 a.b.c and #42 + [brackets] {braces} <angles>' ACCEPT ACCEPT yes '🚀 Codify the out-of-band identity and patch settings on the b' 10 titles, disagreements: 0Evidence: the rendered name is judged, not the raw title — mark on vs mark off, confirmed against real thurbox
marks ON -> title-guard/03-dot-mark (accepted) marks OFF -> queue: --title '.hidden agenda' cannot become a session name: ... a name beginning with '.' is not one it accepts. real thurbox: '🚀 .hidden agenda' ACCEPTED / '.hidden agenda' REFUSED: Name cannot start with '.'Evidence: dispatch's spawn failure, before and after, on the same task and the same real thurbox
=== BEFORE (base e559816) === title-guard/05-ci-cd: spawn failed: Command '['thurbox-cli', 'session', 'create', '--name', '🚀 Rust crate, CI/CD and the profile model', ...]' returned non-zero exit status 1. === AFTER (this branch) === title-guard/05-ci-cd: spawn failed: Name contains invalid characters === the task is left queued either way === state: queued session: NoneEvidence: adversarial titles: backslash and '..', refused here and by real thurbox
Evidence: a spawn failure with nothing on either stream falls back to the exception, not a blank message
title-guard/05-ci-cd: spawn failed: [Errno 2] No such file or directory: 'thurbox-cli'Evidence: the CLI's own usage now documents the title rule
Evidence: queue-selftest.sh section 20 — all 23 assertions
Evidence: a live wave with one unspawnable task: the rest still goes out, no double-spawn, retitle + re-run sends it
wave/11-boom: spawn failed: Name contains invalid characters dispatch: 2 named task(s), launched together — no concurrency cap. wave/10-goes-out -> f1c72608-9671-4554-87a8-ba6e577e0819 [wave/10-goes-out] state: dispatched session: f1c72608-... [wave/11-boom] state: queued session: None -- re-run -- queue: these tasks were named and are not ready to go out: wave/10-goes-out: dispatched, and only a queued task is dispatched (and 10-goes-out still holds its FIRST session) -- retitled by hand, re-run -- wave/11-boom -> c0bc6d37-e775-454a-a6a1-016f8690ce61Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
scripts/lib/queue.py:1464- Acknowledged tradeoff, no action expected. The refusal is evaluated with worker_glyph() read ataddtime, whilespawn_commandsre-reads it at dispatch time, so flipping GLYPHS between the two re-opens the exact defect for a leading-'.' title: add '.env rewrite' with GLYPHS=on (rendered '\U0001F680 .env rewrite', accepted), then set GLYPHS=off in orchestration/session-glyphs.conf — a documented supported action for a terminal that shears on wide glyphs — and dispatch renders '.env rewrite', which thurbox refuses, leaving the hand-edit of title in task.yaml and the brief's H1 that half 1 exists to prevent. The selftest at scripts/queue-selftest.sh:5133-5153 encodes both directions of this, so it is a known property rather than an oversight, and the intent forbids the only remedy that would close it (checking the raw title as well would refuse names thurbox accepts: 'do not widen the rejected set beyond what thurbox rejects'). Half 2 of this change means the dispatch failure now says 'Name cannot start with .' instead of a bare exit status, which is the mitigation actually available.✅ **Test** - passed
✅ No issues found.
./scripts/queue.sh add title-guard ci-cd --title 'Rust crate, CI/CD and the profile model' ...exits 2 naming '/' and quoting the rendered name; the topic directory still holds only PROMPT.md and to…thurbox-cli session create --json --name '🚀 Rust crate, CI/CD and the profile model' --repo-path /tmp/tbxprobewith streams split: exit 1, stdout carries the JSON error, stderr empty — evidence file…session_name_refusaland realthurbox-cli session createover the same ten rendered names (each accepted one created then deleted): 10 agreements, 0 disagreements — evidence fi…queue.sh addruns under FLEET_GLYPH_ROOT with GLYPHS=on then GLYPHS=off, and the same two names put to real thurbox-cli ('🚀 .hidden agenda' accepted, '.hidden agenda' refused with "Name cannot s…queue.sh showafter the failed dispatch reports state: queued, session: None, dispatch: no session dispatched; after a hand-retitle the re-run spawned session c0bc6d37 — evidence files 05 and 10queue.sh add --title 'Windows C:\paths and the model'and--title 'Wait.. then retry'both refused naming the character; both rendered names refused by real thurbox-cli with "Name contains invalid…PATH=<no thurbox-cli> python3 scripts/lib/queue.py dispatch ...prints "spawn failed: [Errno 2] No such file or directory: 'thurbox-cli'" — evidence file 07-spawn-failure-fallback-no-thurbox.txtqueue.sh dispatch wave/10-goes-out wave/11-boomagainst a real git repo and real thurbox: 11-boom reported thurbox's refusal and stayed queued, 10-goes-out got session f1c72608 and answered its…./scripts/queue.sh --helprenders theaddentry documenting that the title becomes the session name, wearing the worker's mark and cut to the byte cap, carrying no '/', '', '..' or leading '.' —…./scripts/check.sh./scripts/check.sh(baseline, already green before this phase)./scripts/queue.sh add <topic> ci-cd --title 'Rust crate, CI/CD and the profile model' ...against an isolatedFLEET_QUEUE_DIRthurbox-cli session create --json --name '🚀 Rust crate, CI/CD and the profile model' --repo-path /tmp/tbxprobe(stdout/stderr split, real thurbox-cli 2.19.9)cross-check script: 10 titles throughqueue.session_name_refusaland through realthurbox-cli session create, each accepted one deleted againFLEET_GLYPH_ROOT=<GLYPHS=on|off> ./scripts/queue.sh add ... --title '.hidden agenda', both renderings also put to realthurbox-cli./scripts/queue.sh add ... --title 'Windows C:\paths and the model'and--title 'Wait.. then retry', both rendered names also put to realthurbox-clipython3 /tmp/oldlib/queue.py dispatch <ref>(base commit e559816 library) vs./scripts/queue.sh dispatch <ref>on the same task and same real thurbox-cli./scripts/queue.sh show <ref>after each failed dispatch (state/session/dispatch fields)PATH=<no thurbox-cli> python3 scripts/lib/queue.py dispatch <ref>./scripts/queue.sh dispatch wave/10-goes-out wave/11-boomagainst a real git repo, then a re-run, then a hand-retitle and re-dispatch; both spawned sessions and their worktrees deleted./scripts/queue-selftest.sh(exit 0; section 20's 23 assertions all pass)./scripts/queue.sh --help(rendered usage foradd)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.