Spawn targets are now validated against the real registry

Three failures in two days shared one root: nothing checked a subagent_type against the agents that actually exist.

What was broken

#SymptomWhy nothing caught it
1/ork:assess spawned 4 agents by bare name; its default path failed at dispatchThe static guard extracts subagent_type="literal". Assess's spawn is Agent(subagent_type=agent_type, ...), a VARIABLE. No literal to read.
2ork:business-case (a SKILL) typed as a workflow stage; died as a generic parallel[N] failedNo runtime check existed. task-agent-advisor fell through to silent success on anything unrecognised.
3The workflow advisor stayed silent on a script with 3 valid stages and 1 invalidIt muted whenever the string agentType appeared anywhere. Two lines below, its own message says "bare names fail to resolve".

Which layer can see which bug

                          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.
The load-bearing idea: one choke point

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.

Acceptance, run against the BUILT hooks

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

Measurement, because a fix you cannot see recurs

Extend the existing stream, do not add a file

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.

Four mistakes made while building this, kept on the record

1. I repeated the exact bug I was fixing

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.

2. The plan said to register the hook in hooks.json and entries. That was wrong.

Chain members appear in neither; only the dispatcher is registered. Following the plan literally would have double-dispatched.

3. My first sweep reported "136 bare spawns"

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.

4. An acceptance case looked like a product defect and was my harness

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.

Deliberately not done

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.