Skip to content

A run that could not spawn subagents was indistinguishable from one that chose not to - #69

Open
syncytium2 wants to merge 7 commits into
mainfrom
subagent-preflight
Open

syncytium2 wants to merge 7 commits into
mainfrom
subagent-preflight

Conversation

@syncytium2

Copy link
Copy Markdown
Owner

The process runs a substantial deliverable as parallel subagents and permits a single-pass
self-review for a small one. Between those two sentences is a hole: when the Agent tool is
unavailable, a run does not fail — it falls back into the mode the process already
sanctions. Eleven roles still appear, each with real findings; the roster gate and the grants
gate both still pass. What is lost is the independence, and nothing counted independence.

docs/reviews/plugin_adoption_docs_murderboard_2026-08-26.md is that run, in this repo. It is
legible as such only because its author wrote a "Stated deviation" section nothing asked for —
prose, volunteered, exactly the shape this repo keeps converting into gates.

Three parts, and the load-bearing one is the probe

Probe, do not infer (SKILL.md step 1a). Spawn one throwaway subagent and require a token
back, before anything is spent. Nothing weaker proves it: availability is settled by the
session's tool list, a launch flag, the permission mode, an instruction injected by the
harness, or already being inside a subagent — and several of those appear in no file. If the
probe fails the run stops before role 1, while a "no" is still cheap.

Declare it, and check the declaration (murderboard_roster.sh). An Execution: line beside
the existing Mode: line, same posture for the same reason: undeclared exits 0 so every report
a consumer has already written keeps passing, and --require-execution opts in. A bare
single-pass is rejected until it says which kind — chosen is the process working as
designed on a one-liner, forced is an environment defect that recurs silently until someone
fixes it.

Name the knob (murderboard_subagents.sh). Not a gate, and it says so on every run
including its clean one. It reports the blockers a file can show, so that once the probe has
failed, "allow subagents" is one edit rather than a scavenger hunt. Demonstrated rather than
asserted: run in the session that wrote it — where subagents were in fact blocked by an
injected instruction — it correctly reported nothing on disk and refused to call that clean.

Two parsers died to get here

Peer review from murderboard-b1 defeated the first two matchers with constructed inputs. The
pair that settled it, one character apart:

"parallel subagents; role 4 could not reach the web"   -> subagents
"parallel subagents, role 4 could not reach the web"   -> single-pass

Same sentence, opposite verdicts. Another defeated the length bound by being more detailed,
so a terser and less honest line passed where a fuller one failed. The diagnosis generalises:
textual distance was standing in for grammatical attachment — does a failure attach to the
fan-out or to one role? — and no threshold separates those, because distance is not what
distinguishes them.

So the gate stopped parsing the sentence. A controlled token, prose left alone:

Execution: subagents — 11 spawned, role 4 could not reach the web
Execution: single-pass (forced) — the Agent tool was unavailable
Execution: single-pass (chosen) — a one-line caption

The remaining limit — a mislabelled head whose prose denies the fan-out in non-mode words — is
stated in three places and pinned as fixtures, so a later attempt to "fix" it turns red the
test that carries the argument against it.

Interaction with the cost gate, tested not assumed

murderboard_model_gate.sh landed while this sat. It is a PreToolUse hook on
Skill|Agent|Task and the probe is an Agent call, so the two meet. Against the gate on a
blocked-model transcript: the probe returns 0, a role fan-out returns 2. No
misattribution — but a green probe answers can I spawn a subagent, never can I spawn eleven
of these
, and step 1a now says so, and says a later refusal is a cost refusal to hand back
rather than an availability one to record as single-pass (forced).

Gates

All 9 gate selftests pass (roster 47, model gate 29, subagents 9) and all 10 tests pass, against
the merged tree. Version 0.10.1 -> 0.11.0; the gate accepts it. Every pre-existing review
report still passes unchanged.

murderboard_subagents.sh (390 lines) has had no reviewer but its author.

🤖 Generated with Claude Code

defazio2 and others added 7 commits September 4, 2026 14:07
…hat chose not to

The process runs eleven roles as parallel subagents for a substantial deliverable and
permits a single-pass self-review for a small one. Between those two sentences is a hole:
when the Agent tool is unavailable a run does not fail, it falls back into the mode the
process already sanctions. Eleven roles still appear, each with real findings; the roster
gate and the grants gate both still pass. What is lost is the independence, and nothing
counted independence.

docs/reviews/plugin_adoption_docs_murderboard_2026-08-26.md is that run, in this repo.
It is legible as such only because its author wrote a "Stated deviation" section nothing
asked for -- prose, volunteered, exactly the shape this repo keeps converting into gates.

Three parts, and the load-bearing one is the probe.

PROBE, DO NOT INFER (skills/murderboard/SKILL.md step 1a). Before anything is spent, spawn
one throwaway subagent and require a token back. Nothing weaker proves it: availability is
settled by the session's tool list, a launch flag, the permission mode, an instruction
injected by the harness, or already being inside a subagent -- and several of those appear
in no file. The 2026-08-26 block was of that kind. If the probe fails the run stops before
role 1, while a "no" is still cheap. Two things the human's "go ahead" cannot buy: the
declaration must say forced, and a deliverable making an attribution claim does not proceed
at all, because the process makes role 2 a separate agent precisely so it does not inherit
the drafter's search history.

DECLARE IT, AND CHECK THE DECLARATION (murderboard_roster.sh). An `Execution:` line beside
the existing `Mode:` line, same posture for the same reason: undeclared exits 0 so every
report a consumer has already written keeps passing, and --require-execution opts in. A bare
`single-pass` is rejected until it says which kind -- CHOSEN is the process working as
designed on a one-liner, FORCED is an environment defect that will recur silently on every
run until somebody fixes it, and unqualified it reads as the first.

Two defects in that gate, found and fixed before it shipped, both now negative controls:
the cause was searched for across the whole report, where words like "chosen" appear in
ordinary finding prose and would have passed on essentially anything; and subagents was
tested before single-pass, so "single-pass, no parallel fan-out" -- the honest way to write
the degraded case -- read as a boast.

NAME THE KNOB (murderboard_subagents.sh). Not a gate, and it says so on every run including
its clean one. It reports the blockers a file can show -- a permissions.deny rule, an
allow-list without Task, a PreToolUse matcher that runs on the Agent tool, a CLAUDE.md
sentence forbidding subagents -- so that once the probe has failed, "allow subagents" is one
edit rather than a scavenger hunt. Its exit 0 is not an all-clear. Demonstrated rather than
asserted: run in the session that wrote it, where subagents are in fact blocked by an
injected instruction, it correctly reports nothing on disk and refuses to call that clean.

Wired into this repo's own SessionStart, deliberately NOT into hooks/hooks.json: the blocker
it finds lives in the user's settings, so an install would announce it in every project
including the many where nobody intends to run a review. That comment claimed "one hook,
deliberately only one" and now says which candidate was weighed and rejected.

Plumbing: STAMPED_FILES, the revendor file set and remap, both CI selftests (one against
fixtures, one against a real checkout, since every fixture is a directory the test wrote),
README, CLAUDE.md and its drop-in paragraph. Plugin 0.8.0 -> 0.9.0, chosen against a 0.8.0
main with no open PRs and no branch holding a higher number.

The 2026-08-26 record is annotated with the Execution line its own prose already supports,
marked as added on 2026-09-04 and otherwise untouched -- leaving the one worked example of
the problem with no instance of the fix would have been the worse choice.

Gates: all eight selftests pass (roster 31, subagents 9), all ten tests pass, the version
gate accepts 0.8.0 -> 0.9.0, and every existing review report still passes unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…to imagine

Peer review from murderboard-b1 with constructed inputs, not a read. Every claim
reproduced before acting on it; all three findings stand.

THE CAUSE MATCHER WAS ASYMMETRIC, and that was the bug rather than a missing word.
`forced` held a list of unavailability idioms; `chosen` held `small`, `short`, `caption`,
`one-liner`. Those describe the DELIVERABLE, and a forced run also has a short deliverable
-- so a forced run phrased outside the idiom list that mentioned its own brevity was filed
as a deliberate judgement call. Four of five constructed forced runs classified as `chosen`,
including "Task tool errored out, small deliverable". A recurring environment defect
recorded as a choice is exactly what this field was added to prevent.

The fix is not a longer forced list. That is a denylist over unbounded English, which is the
argument this repo's own reviewer-shell-guard makes against denylists. Only words describing
the DECISION can separate a choice from a constraint, so `chosen` keeps `chose`/`by
choice`/`deliberate` and nothing about the artifact.

THE MODE MATCHER READ A DENIAL AS AN ASSERTION. Both branches matched substrings with no
regard to negation, so "subagents unavailable, ran inline" counted as evidence OF subagents
-- a degraded run recorded as a full one, the misclassification with the highest cost here.
Ordering could not have saved it: the line never says "single-pass". The commonest honest
words for a degraded run -- inline, sequentially, by hand, myself -- were in no list at all,
so they hard-failed as unparseable.

Both now share one predicate: does this line say the fan-out did not happen? Matched in both
directions, since "no subagents" and "subagents unavailable" are the same claim written from
either end, and bounded to one clause by [^.;]{0,40} -- because "parallel subagents; role 4
could not reach the web" is a role's failure, not the fan-out's, and a line-global search
for "could not" would call that degraded. Vocabulary is about the MECHANISM (denied, timed
out, errored, hit a limit, fell back) because phrasings are unbounded and mechanisms are not.

A LINE CLAIMING BOTH IS NOW REFUSED, NOT GUESSED AT. "11 parallel agents spawned; role 5
also self-reviewed" is a full run and "single-pass, no parallel fan-out" is a degraded one,
and no substring rule separates them. Reported, never resolved -- the discipline the grants
gate already applies to a role declaring two verdicts, since picking one is how the wrong
one reaches the record.

ON THE STRICT POSTURE FOR `unrecognized`, which the review flagged as a backward-compat
risk: kept, and the reason is now written beside it. `Execution:` is new as of today, so no
report anywhere carries one -- there is no legacy line for this branch to redden. The risk
was real in principle and absent in fact, and the day that stops being true the comment is
wrong rather than merely stale.

Twelve constructed inputs are fixtures now, asserting on the CLASSIFICATION rather than the
exit code: the exit code collapses "read it wrong" and "refused to guess" into the same 1,
and that difference is the whole review. Roster selftest 31 -> 43.

Rebased onto 3a6a8fb (#63 merged; README outside the payload glob, so we never contended).
All 8 gate selftests and all 10 tests pass; version gate still accepts 0.8.0 -> 0.9.0.

Not reviewed by anyone but me: murderboard_subagents.sh, all 390 lines. b1 reviewed the
roster changes asked about and said so rather than letting the scope be assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the sentence

Second review round from murderboard-b1. The proximity bound I asked them to attack was
defeated four ways, including by my own fixture. Reproduced all four before acting.

THE PAIR THAT SETTLES IT, one character apart:

    "parallel subagents; role 4 could not reach the web"   -> subagents
    "parallel subagents, role 4 could not reach the web"   -> single-pass

Same sentence, opposite verdicts. And three in the expensive direction, a degraded run
read as full: a denial in a second sentence ("spawned the eleven. Every one of them failed
to start"), a denial past the 40-character bound ("subagents were, after three separate
attempts and a timeout, unavailable"), and a denial in no vocabulary at all ("parallel
fan-out; nothing came back").

The middle one is the one worth staring at: it defeats the bound BY BEING MORE DETAILED.
A terser, less honest line passed where a fuller one failed. That is a gate rewarding the
wrong behaviour, not a threshold wanting a tweak.

THE DIAGNOSIS, which is why no setting of the bound would have worked. Proximity was
standing in for GRAMMATICAL ATTACHMENT -- does the denial attach to the fan-out or to one
role? -- and

    "parallel subagents could not start"           the fan-out
    "parallel subagents, role 4 could not start"   a role

have nearly the same span and opposite meanings. Widen it and false positives rise; narrow
it and real denials are missed. A cheap measurable quantity standing in for the property
that actually matters is the defect this repo names in other people's work, and it had been
built into one of its own gates.

SO THE GATE STOPS PARSING THE SENTENCE. A controlled token, and the prose is left alone:

    Execution: subagents — 11 spawned, role 4 could not reach the web
    Execution: single-pass (forced) — the Agent tool was unavailable
    Execution: single-pass (chosen) — a one-line caption

Everything before the separator must be a token exactly; everything after is never read.
The cause is a token too, in the parenthetical, which retires the Q1 asymmetry outright --
artifact prose can no longer reach it. Separators are authoring marks a writer puts in
deliberately (em/en dash, double hyphen, spaced hyphen, second colon), NOT commas or
semicolons: "subagents, none of which started" would otherwise hand back "subagents".

A line with no token is refused with the grammar printed. That is a failure that TEACHES
rather than one that misclassifies, and it is the right trade: every defeat above now
either reads correctly or is refused, and none is silently wrong.

Deleted rather than tuned: FANOUT_ABSENT, fanout_denied, EXEC_DEGRADED and the proximity
bound. The replacement is shorter than what it replaces.

Contradiction detection survives, in ONE direction only and deliberately: a `subagents`
token whose prose describes an inline run is refused; a `single-pass` token mentioning a
fan-out is not, because that is almost always saying the fan-out did not happen, and
checking it would resurrect the negation problem this design retires.

The window for imposing a format was open exactly now -- Execution: is one day old and no
report anywhere carries one, which b1 verified against all four committed run records
rather than taking from me. It closes the moment the first one does.

Also fixed, found by the rewritten suite: `**Execution:** subagents` was refused for its
formatting rather than its content, because cutting at the first colon leaves the closing
`**` in the head. The mode line is careful not to do that and this now matches it.

Roster selftest holds at 43, with the fixtures rewritten -- 17 execution cases now,
including all four new defeats as explicit refusals. All 8 gate selftests and all 10 tests
pass.

Still unreviewed by anyone but me: murderboard_subagents.sh, 390 lines. b1 has said twice
that they have not read it, which is worth more than a nod that they had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third review round from murderboard-b1, and the finding is a documentation gap with an
explicit "do not fix it" attached. Verified before acting: three of their four original
cases still record as a full run when the head is mislabelled.

    Execution: subagents — spawned the eleven. Every one failed to start
    Execution: subagents — parallel fan-out; nothing came back

Head trusted, tail compared against MODE words only, "failed to start" is not one.

WHY THAT IS RIGHT AND STAYS. The tail check uses mode vocabulary -- inline, by hand,
single-pass -- because those can only describe how the RUN went. Denial words like "could
not" and "failed" describe a run OR a role, and that ambiguity is the attachment problem
that killed the proximity bound. Catching the mislabelled head needs exactly that
vocabulary, and the fixture "subagents — 11 spawned, role 4 could not reach the web" is a
role-level failure in a genuinely full run which false-positives on it. It would trade a
stated limit for an unstated false positive, which is the worse deal.

SO IT IS STATED, in the shape murderboard_agents.py already uses for its own limit -- a
gate that understates itself is defective the same way one that overstates itself is. The
one-liner is in the code beside the check; the reasoning is in doc_review_process.md so the
next person does not rediscover it by trying; the skill says the token has to be true and
nothing downstream can check that.

AND PINNED AS FIXTURES, which the review did not ask for. Four cases assert the documented
behaviour, including the role-level one. A later attempt to "fix" the first three turns the
fourth red, so the limit cannot be quietly changed without the test that explains why
saying so. Writing a boundary down protects the next reader; asserting it protects the
boundary. Roster selftest 43 -> 47.

Credit where I would have been slower: b1 named why the asymmetry works before criticising
it -- mode words cannot attach to a role, denial words can -- which is a sharper statement
of my own design than I had.

All 8 gate selftests and all 10 tests pass. murderboard_subagents.sh remains unreviewed by
anyone but me, said for the third time rather than allowed to fade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…te changed what the probe proves

Main moved six commits in three days while this branch sat unmerged. Seven conflicts, all
the same shape — both sides appended to the same list — so all resolved by keeping both:
STAMPED_FILES, the re-vendor file set and remap, the CI selftest steps, and the two
sentences in README/CLAUDE.md that count the tools. The count is now "five gates, one
preflight that is deliberately not a gate".

VERSION. 0.9.0 was chosen against a 0.8.0 main and is now a DOWNGRADE, which is exactly the
state .github/plugin_version_gate.py exists to refuse and would have refused. Re-derived
rather than incremented: main is 0.10.1, nothing in flight holds higher (reader-inference,
the one open PR, sits at main's own 0.10.1), so 0.11.0. Gate accepts 0.10.1 -> 0.11.0.

AND ONE REAL INTERACTION, which is the reason re-baselining is not just conflict clerical
work. Main added murderboard_model_gate.sh: a PreToolUse hook on Skill|Agent|Task that
reads the prompt and blocks a murderboard fan-out on an expensive model. Step 1a's probe is
an Agent call, so the two now meet.

Tested rather than reasoned about, against the gate itself on a blocked-model transcript:

    the step-1a probe      rc=0   allowed
    a real role fan-out    rc=2   BLOCKED

So the probe is NOT misattributed — a cost block does not masquerade as subagents being
unavailable, which was the failure I went looking for. What it does mean is narrower and
worth stating: a green probe answers "can I spawn a subagent", never "can I spawn eleven of
these". On a blocked model the probe goes green and the fan-out is refused afterwards. Step
1a now says so, and says which way to read that refusal: it is a COST refusal, so stop and
hand it back rather than recording single-pass (forced) and carrying on. Degrading there
would convert a policy decision into a fake capability finding, in the exact field this
branch added to stop that.

Also corrected the template's leftover `Execution: parallel subagents`, which predates the
token grammar — the token is `subagents`.

All 9 gate selftests pass (roster 47, model gate 29, subagents 9) and all 10 tests pass,
against the merged tree rather than against either side of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught it, correctly: "the plugin payload changed but the version is still 0.11.0".
#68 merged an hour after this branch chose 0.11.0 against a 0.10.1 main, so the number was
equal rather than ahead by the time the job ran.

This is the decay the version gate's own docstring describes -- a number is only valid
against the main it was chosen for, and nobody edited it; the queue moved underneath. Third
time this branch has re-derived rather than incremented: 0.9.0 -> 0.11.0 -> 0.12.0, each
against the main in front of it at the time, with the branch list checked for a higher
holder. Nothing in flight holds above 0.11.0 (reader-inference is still at 0.10.1).

Merged origin/main 81a0927 cleanly first -- no conflicts this time; #68 rewrote the cost
preflight's prose and step 1a's reference to it still reads true.

All 9 gate selftests and all 10 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants