v0.7.0 — P1: plugin-prefix mismatch silently disables 4 safety-gate hooks + breaks skill-invocation analytics
## Problem
CC's Skill tool input and subagent_type fields can carry a plugin prefix (`tmb:` or `tmb-rc:`) depending on context (project-local override vs global plugin agent vs slash-command vs direct invocation). Five plugin hooks compare these fields against the bare role name only — when CC supplies the prefixed form, the comparison fails and the hook silently skips. Four of the five are SAFETY GATES; the fifth is the analytics hook that caused L6 step 14 to fail.
## Evidence (verified 2026-05-17/18)
**L6 step-14 reproduction (Phase B branch, post-prompt-rewrite):**
bro invoked the Skill tool TWICE per `tool-uses.jsonl`:
```
{"name":"Skill","input":{"skill":"tmb:tmb_planning"}}
{"name":"Skill","input":{"skill":"tmb:tmb_recovery"}}
```
But `skill_invocations` rows remain 0 — `skill-invocation-record.sh:61` does `WHERE name = '$SKILL_NAME'` with `SKILL_NAME='tmb:tmb_planning'`, but the catalog stores `name='tmb_planning'` (no prefix). No row matches → silent exit → no junction row.
**Affected hooks:**
| # | Hook | Line | Comparison | Severity |
|---|---|---|---|---|
| 1 | `scripts/hooks/skill-invocation-record.sh` | 61 | `WHERE name = '$SKILL_NAME'` | analytics; silent — step 14 fails |
| 2 | `scripts/hooks/pr-reviewer-no-worktree.sh` | 35 | `[ "$SUBAGENT" = "pr-reviewer" ]` | **safety gate silently disabled** — pr-reviewer can spawn with worktree |
| 3 | `scripts/hooks/require-task-spec.sh` | 15 | `[ "$AGENT_TYPE" != "swe" ]` | **safety gate silently disabled** — SWE can spawn without task_id in prompt |
| 4 | `scripts/hooks/require-feature-branch-active.sh` | 21 | `[ "$AGENT_TYPE" = "swe" ]` | **safety gate silently disabled** — SWE can spawn off wrong branch |
| 5 | `scripts/hooks/pr-reviewer-spawn-prompt-shape.sh` (Phase B NEW) | 26 | `[ "$SUBAGENT" = "pr-reviewer" ]` | **safety gate silently disabled** — malformed pr-reviewer spawn prompts pass |
**Working hooks for pattern comparison** (handle both forms explicitly):
- `scripts/hooks/swe-atomic-close.sh:75` — `[ "$AGENT_TYPE" != "swe" ] && [ "$AGENT_TYPE" != "tmb:swe" ]`
- `scripts/hooks/git-push-guard.sh:93` — `[ "$AGENT_TYPE" = "tmb:swe" ] || [ "$AGENT_TYPE" = "swe" ]`
**Why this wasn't caught:**
- Existing lint `tests/lint/no-hardcoded-plugin-name.sh` catches `="tmb"` style hardcodes in path-construction code. It does NOT catch role-comparison patterns that omit the `tmb:` prefix variant.
- Two hooks (`swe-atomic-close`, `git-push-guard`) added the `|| "tmb:swe"` workaround ad-hoc when their authors hit the bug. The pattern wasn't extracted into a helper; new hooks (including Phase B's `pr-reviewer-spawn-prompt-shape.sh`) inherited the wrong shape.
## Plan (structural, prevents future regression)
### 1. New helper — `scripts/hooks/lib/normalize-role.sh`
```bash
# Strip plugin prefix from a role identifier.
# CC passes role names with or without a `<plugin>:` prefix depending on
# context (project-local override vs global agent vs slash-command). Strip
# the prefix to get the canonical role name for comparison.
#
# Usage: role=$(normalize_role "$RAW")
normalize_role() {
printf '%s' "${1#*:}"
}
```
`${1#*:}` strips the shortest match up to and including the first colon. If no colon present, returns the input unchanged. Works for `tmb:swe` → `swe`, `tmb-rc:pr-reviewer` → `pr-reviewer`, raw `swe` → `swe`.
### 2. Hook fixes (5 files)
Each hook sources the helper and normalizes before comparing:
```bash
. "$(dirname "${BASH_SOURCE[0]}")/lib/normalize-role.sh"
# was: [ "$SUBAGENT" = "pr-reviewer" ] || exit 0
SUBAGENT=$(normalize_role "$SUBAGENT")
[ "$SUBAGENT" = "pr-reviewer" ] || exit 0
```
Equivalent for `AGENT_TYPE` and `SKILL_NAME` in the affected hooks. For `skill-invocation-record.sh`, normalize before the catalog SQL.
The two already-correct hooks (`swe-atomic-close.sh`, `git-push-guard.sh`) should ALSO migrate to the helper — collapse the `|| "tmb:swe"` pattern into the normalized form. Same semantics, less drift surface.
### 3. New lint — `tests/lint/no-bare-role-compare.sh`
Scans `scripts/hooks/*.sh` for `subagent_type`/`agent_type`/`skill` comparisons against bare role names without a normalize_role call earlier in the same script. Allowlist: the helper itself, and hooks that explicitly handle both forms (transitional during migration).
Failure mode: clear error "hook `<file>:<line>` compares `<field>` against bare `<role>` without normalize_role. Either source `scripts/hooks/lib/normalize-role.sh` and normalize, or add the `|| tmb:<role>` form."
### 4. New L3 hook-script tests
For each fixed hook, add an L3 test (`tests/hooks/*.test.sh`) that sends both the raw form (`"swe"`) and the prefixed form (`"tmb:swe"`) through the hook and asserts the gate fires identically. Covers regression of the same bug.
## Acceptance criteria
- Helper `scripts/hooks/lib/normalize-role.sh` exists with one function.
- All 5 affected hooks source the helper + normalize before compare.
- The 2 already-correct hooks migrated to the helper (semantic-equivalent).
- New lint `no-bare-role-compare.sh` catches the regression pattern; passes on dev.
- L3 tests for each fixed hook verify both raw + prefixed forms fire the gate.
- L6 step 14 passes naturally — `skill_invocations` row lands when bro fires `Skill(skill='tmb:tmb_planning')`.
- L0+L1+L2+L3+L4 all green; no regressions.
## Out of scope
- Changing CC's Skill/Agent tool input format (not under plugin control).
- Migrating the role-handling logic into MCP server (helper is hook-local).
- Refactoring agent_type values stored in the DB (catalog stays canonical without prefix).
## Coordination
- L6 step 14 (`14-skill-invocation-recorded`) has been failing since at least 2026-05-15 due to bug #1 (analytics). Closing this issue + re-running L6 step 14 should turn it green.
- The 4 safety-gate bugs are CRITICAL — they have been silently passing tests because the test suite invokes hooks with raw role names (`swe`), matching the buggy comparison. Real-world CC use with prefixed forms bypasses the gates.
- This issue blocks shipping #2904 Phase A+B (the slim-skills MR) cleanly — Phase B's NEW `pr-reviewer-spawn-prompt-shape.sh` has the same bug and would land it into v0.7.0 dev.
## Note on source
Discovered during post-Phase-B L6 step 14 re-test on 2026-05-17. The fix to step 14's prompt (commit `ed01f2a`) made bro invoke `Skill` correctly — exposing the underlying analytics hook bug. Codebase grep then surfaced the 4 safety-gate siblings.
issue