| # | Symptom | Why nothing caught it |
|---|---|---|
| 1 | /ork:assess spawned 4 agents by bare name; its default path failed at dispatch | The static guard extracts subagent_type="literal". Assess's spawn is Agent(subagent_type=agent_type, ...), a VARIABLE. No literal to read. |
| 2 | ork:business-case (a SKILL) typed as a workflow stage; died as a generic parallel[N] failed | No runtime check existed. task-agent-advisor fell through to silent success on anything unrecognised. |
| 3 | The workflow advisor stayed silent on a script with 3 valid stages and 1 invalid | It muted whenever the string agentType appeared anywhere. Two lines below, its own message says "bare names fail to resolve". |
assess bug #3279 future typo
(variable) (runtime)
────────────────────────────────────────────────────────────
static guard (before) no no no
static guard (after) YES no no
runtime validator (new) YES YES YES
subagent-start no* no* no*
* fires AFTER the dispatch decision, so it can never block.
It now names the cause instead of logging to a file nobody reads.
Every spawn crosses PreToolUse[Agent], whether it came from a skill, a workflow, a nested subagent, or a hand-typed call. Putting the registry check there covers all callers at once, which is why the runtime hook carries more than all four static fixes combined.
1. Agent ork:business-case -> ask
"not a registered agent. No similarly-named agent exists;
it may be a SKILL rather than an agent (#3279)."
2. Agent security-auditor -> ask
"is a bare name ... fails at dispatch. Use ork:security-auditor."
3. Agent ork:security-auditor -> (none = allowed)
4. Agent vercel:ai-architect -> (none = allowed)
other plugins pass through; we cannot enumerate their registries
5. Workflow, 2 valid + 1 invalid stage:
"1 workflow stage(s) name an agent that does not resolve:
- subagent_type "ork:business-case" is not a registered agent..."
The two valid stages are not mentioned.
hooks typecheck exit 0 hooks build exit 0 hooks unit suite 305 files, 7526 passed, 2 skipped hook registry closure PASS, 0 dead hooks (the #959 gate) skills tests 0 failed agents tests 0 failed security suite 17 passed, 0 failed static guard, negative-tested IN PLACE: baseline exit 0 bare literal reintroduced exit 1, names file + fix phantom ork: target exit 1, names missing agent file restored exit 0
subagent-spawns.jsonl is already schema-locked, already records
subagent_type, and its validator requires only timestamp: every other
field is optional. So spawn_verdict is backward-compatible by construction
and breaks none of its four readers. A new file would have cost a validator, a canonical,
a SCHEMA_LOCKED row, a count assertion, an inventory row, a new orphan surface, and a
re-stamp of the "7 locked paths" count duplicated across six documents.
spawn_verdict : 'ok' | 'bare' | 'unknown'
'skip' (other plugins, placeholders) is OMITTED rather
than recorded as a fourth state nobody can act on
audit-activation already correlates subagent_type against the agent
inventory, so the invalid-spawn RATE is queryable with tooling that
exists. That is what turns the ask-to-deny flip into a data decision.
The new registry read used getProjectDir(). Under vitest that resolves to
src/hooks, so it found ZERO agents and the validator silently degraded to
allow-all: the same inert-check failure subagent-validator had. The
registry.size > 20 assertion caught it and is kept for that reason.
Chain members appear in neither; only the dispatcher is registered. Following the plan literally would have double-dispatched.
A greedy .*[=:] stripped the ork: prefix it was testing for.
Corrected: 136 literal spawns, all correctly namespaced, zero bare. The static surface was
healthy the whole time and the real gap was much narrower.
Case 5 came back SILENT, apparently proving the advisor never fired. The output guard
had stripped additionalContext because my payload omitted hook_event_name,
so the guard saw an unknown event. PreToolUse is in the allowed set. With the field CC
actually sends, it fires correctly.
outputDeny NOT used. This reads a registry off disk, so a false
positive should be a prompt you wave through, not a
wall. The dispatcher propagates ask and deny verbatim,
so the flip is one line once the telemetry shows a rate.
package-lock reverted. The only diff was a stale version field
(9.5.2 -> 9.6.1); release-please owns versioning.