feat(zcode-parity): schema-compliance coverage for all ZCode-emitting hooks + plugin channel - #1030
Merged
Merged
Conversation
… hooks + plugin channel
Closes the test-coverage gap surfaced by the plugin-channel migration. ZCode parses hook
stdout against the HookJSONOutput schema (CCt, .strict()): unknown top-level keys are
rejected (hook.run.failed, output discarded). A prior regression shape {hookEventName,
additionalContext} at top level was silently rejected. This commit pins the allowed
top-level key set for every ZCode-emitting hook via dedicated schema-compliance arms.
## What (per-hook)
Schema arms added (assert allowedTopLevel Set + hookEventName NOT at top level):
- inject-subagent-context, inject-subagent-digest, validate-prompt (modified)
- check-worker-dispatch-channel, inject-project-digest, runtime-bridge-dispatch (new tests)
- end-of-turn-reminder, session-start (auditor GAP-1/3)
- runtime-bridge-dispatch forwarded-output arm via stub dispatch.ts (auditor GAP-2)
Plus (from prior session, in this branch):
- emitPlugin backend in scripts/render-harness-config.mjs (plugin channel is the only
zcode-working hook path — zcode strips project-config hooks via security policy)
- emitZcode cleaned (hooks branch removed; 4 degradation note-ops declared loudly)
- 11 relocated hook twins in plugin/hooks/ (T-PLUG-A path-rewrite)
- Output-shape fix in ALL _emit_ctx hooks: top-level hookEventName removed (ZCode CCt.strict)
- deps-hash-check SSOT-triad (source/dogfood/plugin byte-identity, 3-way guard)
- research-patch 2026-07-04-zcode-harness-visibility.md corrected (false claim removed)
- baselines regenerated (deps-hash + check-doc-authority-header SSOT changed)
## Test results
- hooks/ suite: 748 -> 783 PASS (+35 arms: 9 schema-compliance + 26 supporting)
- render-check green, byte-identical 2/2, plugin-paths 60/60
## §1.7 forward-check (compliance with existing disciplines)
- Capability-commit gate: N/A — no install.sh/settings.json/CLAUDE.md changes (test-coverage only).
- Build-vs-reuse SSOT: N/A — test coverage of EXISTING hooks, no new mechanism.
- Doc-authority: no authority-bearing docs changed.
- Language-discipline: all output in English, matches existing test-file precedent.
- CI-tool-pinning: no workflow/shell-pin changes.
- Pattern precedent: allowedTopLevel Set follows deps-hash-check.test.ts:164 (established).
## §1.7 backward-check (cold sweep via agents/backward-sweep-auditor.md, no diff knowledge)
Class of this change = ZCode-emitting hooks (JSON output parsed by CCt.strict()) + paired tests.
Surfaces where class-X occurs (16 dogfood emitters + 14 plugin twins enumerated):
- 14/16 dogfood emitters SWEPT-CLEAN (schema arm present).
- 3 GAP-FOUND by cold auditor (closed in this commit):
- end-of-turn-reminder: unconditional {decision,reason,systemMessage}, no guard -> added arm.
- session-start (plugin-only): no test.ts at all -> created dedicated test.
- runtime-bridge-dispatch: forwarded output from dispatch.ts unguarded -> added stub arm.
- Architectural GAP (NOT closed — owner-decision, separate PR): 3 consumer-only hooks
(inject-project-digest, check-doc-authority-header, inject-output-language) are NOT in
.ai-factory/harness-model.json -> emitPlugin does not render them -> plugin channel does not
deliver them to ZCode consumers. Adding consumer-only hooks changes the SSOT model semantics
(needs framework-vs-consumer categorization in hooks entries).
## review-sidecar (external cold review): 0 BLOCKER, 4 CONCERN, 4 NIT
- CONCERN-high (false honest-scoping claim): FIXED — stub-dispatch arm now tests forward path;
header comment updated to match.
- CONCERN-medium-1 (whitespace test passed for wrong reason): FIXED — split into gate-rejection
+ trim-tolerance arms with correct discriminators.
- CONCERN-medium-2 (all arms assert exit-0 against 12 bail points): documented as CAVEAT in
file header; structural limit of non-blocking hook testing; mitigated by stub arm exercising
the END of the hook path.
- CONCERN-low (runtime-bridge.test.ts:85-121 contradicts hook behavior): pre-existing, noted.
- NIT-2 (allowedTopLevel Set duplicated in 17 files): deferred to follow-up cleanup PR
(refactor of 17 passing files is out of scope here; Sets currently identical, no drift).
Prior-art: skipped — test coverage of existing hooks, no new capability
…channel-hooks # Conflicts: # .claude/hooks/deps-hash-check.sh # packages/core/hooks/deps-hash-check.sh # packages/core/hooks/deps-hash-check.test.ts # tests/install-sh/baselines/react-native/brownfield.fingerprint # tests/install-sh/baselines/react-native/greenfield.fingerprint # tests/install-sh/baselines/react-next/brownfield.fingerprint # tests/install-sh/baselines/react-next/greenfield.fingerprint # tests/install-sh/baselines/react-spa/brownfield.fingerprint # tests/install-sh/baselines/react-spa/greenfield.fingerprint # tests/install-sh/baselines/ts-server/brownfield.fingerprint # tests/install-sh/baselines/ts-server/greenfield.fingerprint
artyhoo
added a commit
that referenced
this pull request
Jul 21, 2026
…ent-gate/@matcher-parity preventer (#1054) * docs(spec): matcher-widening design — 6-hook fix + lightweight probe (3-round review) Captures the design after 3 rounds of dual top-down + bottom-up review. Scope: widen 6 PostToolUse gate hooks Edit|Write -> Edit|Write|MultiEdit (incl. check-doc-authority-header from merged #1009, discovered when the worktree base was refreshed off stale de7fc9d -> 4a9b508). Single atomic PR: the fix + a ~15-LOC grep probe (parity for case-TOOL hooks, @file-content-gate marker for path-only hooks) + a live RED fixture. Full principle No.33 deferred to incidence N>=2. * docs(spec): round-4 — dual-layer gate (edit-time + CI-backstop) + missed edit-site Round-4 review (fresh base 4a9b508) found: - R1: reuse-first path — extend check-hook-marker.sh (edit-time) + channel-coverage.sh (CI) instead of a new standalone probe. Lands preventer at earliest reachable channel (rule-enforcement-channel- selection §4), avoids consumer dead-token (R2). - R5: missed edit-site tests/install-sh/gh-934-ship-doc-authority-hook.test.sh:56 (hardcoded Edit|Write equality from #1009) — added to §2 inventory. - R4 nit: citation BFR §5 -> dual-implementation-discipline §9 (probe != principle slot). Decision (option B): DUAL-LAYER gate — edit-time catches early, CI catches reliably (the original bug was itself an edit-time-hook bypass; single-layer trust in the same mechanism class is not robust). * fix(hooks): widen 6 PostToolUse gate matchers Edit|Write -> Edit|Write|MultiEdit MultiEdit emits a SEPARATE PostToolUse event (tool_name:MultiEdit) that an Edit|Write matcher does NOT catch (falsifier: docs/2026-07-17-matcher- widening-design.md §1). A MultiEdit that violates a gate's rule slipped past silently. The real bug shape is body<->registration drift: 3 of 6 hooks already had 'case "$TOOL" in Edit|Write|MultiEdit)' in their body but were registered Edit|Write. SSOT-first: harness-model.json (5) -> render --write -> setup.d/10-skills.sh (206,246) + install.sh (469,493) (inject-matching-rule + check-doc-authority- header shipped) + gh-934 tests (session-ux 56/57/136, doc-authority-hook 56) + re-capture 4 greenfield npm baselines (brownfield stable, 3 python stable). inject-memory-codification stays Write-only (A5 — creation moment). check-doc-authority-header from merged #1009 (was invisible until the worktree base was refreshed off stale de7fc9d -> 4a9b508). Gates GREEN: render --check, byte-identical 11/11, refresh-covers-full- delivery, both gh-934 tests (11/11 + 14/14), shellcheck clean. STEP 2 (dual-layer preventer: extend check-hook-marker.sh + channel- coverage.sh + RED fixtures) follows in this same PR. * docs(spec): night-mode kickoff for STEP 2 resume Self-contained SSOT for an unattended resume session. Includes the STEP 0 hypothesis-verification block (verify state on disk BEFORE work), the resume point (STEP 1 done at f707d68d1; STEP 2 Layer 1 in-progress uncommitted), the strict task order A-G, the autonomy/fork policy for an operator-away run, critical nuances (shipped-only tolerance, A5, frozen intentional-skip list), and the policy reminders (Decision B dual-layer APPROVED, do not re-litigate). * fix(hooks): propagate matcher widening to plugin channel (post-rebase) STEP 1 widened 5 PostToolUse gate matchers Edit|Write -> Edit|Write|MultiEdit in the CC channel (.claude/settings.json) + SSOT (.ai-factory/harness-model.json). At STEP 1's base the plugin channel (plugin/hooks/hooks.json, added by staging zcode-parity #1030/#1036/#1046) did not exist; post-rebase it carried the same 5 gate matchers still narrow. Widened them surgically (exact-string sed, 5 lines) rather than via `render-harness-config.mjs --write`, which would clobber the hand-maintained ZCode-parity twins (inject-project-digest, inject-output-language, warn-subagent-report-zcode) that emitPlugin does not reproduce from the SSOT. Diff = 5 matcher lines only; twins intact (5 refs); valid JSON. See docs/superpowers/specs/2026-07-17-matcher-widening-decisions.md D2 + finding F1 (pre-existing, ungated staging SSOT/render drift — out of scope, surfaced only). Prior-art: skipped — rendered-output matcher widening + session decisions doc, no new capability (no dependency, no new code module). * feat(hooks): @file-content-gate edit-time gate (Layer 1 of matcher-widening preventer) Prevents the body<->registration matcher-drift class that STEP 1 fixed from recurring. A hook that validates a file's *content* (path-only — no internal tool_name filter) MUST be registered with matcher Edit|Write|MultiEdit, else a MultiEdit edit that violates the rule slips past silently (the registration matcher is the gate's ONLY tool-filter). 3 of 6 hooks had drifted this way. Mechanism (edit-time, earliest reachable channel per README goal): - New `# @file-content-gate:` marker declared on the 4 path-only content-gate hooks (validate-prompt, check-doc-authority, check-doc-authority-header, and check-hook-marker documents its own extension). - check-hook-marker.sh extended: when a hook declares @file-content-gate AND is registered in the framework-self .claude/settings.json, assert its matcher contains Edit, Write AND MultiEdit; else _adv_violation (exit 1 on CC). - Tolerance: a shipped-only hook (no framework-self settings.json entry, e.g. check-doc-authority-header) is SKIPPED — the gate has no framework-side matcher to read for it; its matcher is enforced by the install-sh firing tests + Layer 2. Tests: 3 new arms in check-hook-marker.test.ts (paired-negative Edit|Write -> exit 1; paired-positive Edit|Write|MultiEdit -> exit 0; shipped-only absent-from-settings -> exit 0). Full suite 11/11 green on the rebased base (staging #1030 runHook object shape + ZCODE arm reconciled). CI-backstop (population sweep) follows in Layer 2. Design: 4-round-reviewed spec docs/superpowers/specs/2026-07-17-matcher-widening-design.md. Prior-art: skipped — edit-time gate extension to an existing hook + marker comments on existing hooks, no new capability (no dependency, no new code module >=50/80 LOC, no new subdir). * docs(spec): refine F1 — two plugin generators (sidecar gated, hooks.json twin-entry not render-reproducible) * feat(hooks): @matcher-parity rule + Layer 2 CC-config population backstop Closes the case-TOOL coverage gap (cold-review MAJOR) and places the CI backstop in the CC-config bucket, NOT the harness-agnostic channel-coverage probe (operator-flagged). Layer 1 (edit-time, check-hook-marker.sh): - New @matcher-parity invariant: a hook with `case "$TOOL" in <tools>)` must have its registered matcher ⊇ that case-arm set. Covers the 3 case-TOOL gates that carry no @file-content-gate marker (check-kickoff-traps, check-worker-dispatch-channel, check-hook-marker itself). Uses each hook's existing `case` author-declaration — no new marker, no allowlist. Self-calibrating: inject-memory-codification (A5, Write-only) stays GREEN (matcher Write ⊇ case-arm Write) with no hardcoded MultiEdit demand. - Refactored the settings.json matcher lookup into _reg_matcher(), anchored on the closing `"` so check-doc-authority no longer substring-matches check-doc-authority-header. Layer 2 (CI backstop, packages/core/hooks/check-hook-marker.test.ts): - A vitest describe block runs the REAL edit-time gate against EVERY tracked hook against the live settings.json — catches a matcher narrowed directly in harness-model.json/settings.json even when no hook .sh is edited (the vector Layer 1 cannot see). Reuses the gate verbatim (no parallel population reader). 2 RED fixtures prove non-vacuousness; ≥5-hook sentinel. - Bucket: CC-config (hooks), NOT tests/agnosticism/ — the MultiEdit requirement is CC-specific (inert on other harnesses), so it must not contaminate the harness-agnostic PORTABLE verdict. Supersedes the kickoff Task-C plan (channel-coverage.sh extension + tests/fixtures/matcher-drift/). Suite 17/17 green. See decisions doc D3 (bucket) + D4 (parity rule). Prior-art: skipped — edit-time gate extension + test arms for the existing hook capability, no new capability (no dependency, no new code module >=50/80 LOC, no new subdir). * chore(install-sh): re-capture byte-identical baselines (marker edit + staging inject-* staleness) The @file-content-gate marker added to the shipped check-doc-authority-header.sh shifts its install fingerprint. Re-capturing (snapshot.sh, SNAPSHOT_MODE=capture) also corrects three baselines that origin/staging left STALE — inject-matching-rule / inject-output-language / inject-project-digest — hooks staging changed via zcode-parity but never re-captured (my source for them is byte-identical to staging's; only staging's recorded baseline was old). byte-identical is an all-or-nothing gate → a full re-capture is mandatory to green it; there is no way to refresh only the marker-touched hook. Captured hashes match the live source (verified via shasum). See decisions doc F2. Framework-internal hooks (check-hook-marker, check-doc-authority, validate-prompt) ship to 0 consumers → untouched here. Prior-art: skipped — regenerated install snapshot baselines after a shipped-hook marker edit, no new capability. * fix(hooks): @matcher-parity extraction strips comment lines (cold-review MINOR) The case-arm extraction matched `case "$TOOL" in …` on ANY line, including comments — and this hook's own @matcher-parity doc block now contains that pattern in prose. Current population was safe only because the real code arm precedes the comment mentions and head -1 takes the first; one reorder would mis-extract the prose arm → false parity verdict. Strip comment lines (^[[:space:]]*#) before extraction, mirroring the ^#-anchored marker check. New test arm PARITY comment-immunity: a prose `case "$TOOL" in Read)` above the real `case "$TOOL" in Write)` with matcher Write → exit 0 (reads the real {Write}, not {Read}). Suite 18/18 green. Prior-art: skipped — hardening the edit-time gate's parser + one test arm, no new capability. * docs(spec): D5 — rebased onto staging trunk-restore (8 inherited reds + F1 cleared); 1147/1147 green * fix(test): annotate runRealHook fullEnv as NodeJS.ProcessEnv (tsc --noEmit) vitest passed but tsc --noEmit (all-workspaces CI job) rejected `delete fullEnv.ZCODE_PROJECT_DIR` — the {...process.env, CLAUDE_PROJECT_DIR} literal lost ProcessEnv's index signature. Explicit annotation restores it, mirroring the sibling runHook helper. Prior-art: skipped — type annotation on a test helper, no new capability. * docs(spec): matcher-widening morning report — PR #1054 green, awaiting operator merge --------- Co-authored-by: t <t@t.co>
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.
Summary
Closes the test-coverage gap surfaced by the ZCode plugin-channel migration. ZCode parses hook stdout against the
HookJSONOutputschema (CCt,.strict()): unknown top-level keys are rejected (hook.run.failed, output discarded). A prior regression shape{hookEventName, additionalContext}at top level was silently rejected. This PR pins the allowed top-level key set for every ZCode-emitting hook via dedicated schema-compliance arms.Also carries the plugin-channel migration itself (emitPlugin backend, relocated hook twins, output-shape fix, baselines regen, research-patch correction) — the full
feat/zcode-plugin-channel-hooksbranch from the prior session + this session's coverage work.What changed
Schema arms added (each asserts an
allowedTopLevelSet +hookEventNameNOT at top level):inject-subagent-context,inject-subagent-digest,validate-prompt(modified — arm added)check-worker-dispatch-channel,inject-project-digest,runtime-bridge-dispatch,session-start(new dedicated test files)end-of-turn-reminder(modified — arm added for unconditional{decision,reason,systemMessage})runtime-bridge-dispatchforwarded-output arm via stubdispatch.ts(pins the hook's forward contract; the JSON is constructed in a separate source location)Plugin channel migration (prior session, in this branch):
emitPluginbackend inscripts/render-harness-config.mjs— rendersplugin/hooks/hooks.jsonfrom.ai-factory/harness-model.jsonSSOT (plugin channel is the only zcode-working hook path; zcode strips project-config hooks viaconfig_project_hooks_ignoredsecurity policy)emitZcodecleaned (hooks branch removed; 4 degradation note-ops declared loudly)plugin/hooks/(T-PLUG-A path-rewrite) +lang/packs_emit_ctxhooks: top-levelhookEventNameremoveddeps-hash-checkSSOT-triad (source/dogfood/plugin byte-identity, 3-way guard)2026-07-04-zcode-harness-visibility.mdcorrected (false "hooks run unchanged once registered" claim removed)Test results
hooks/suite: 748 → 783 PASS (+35 arms: 9 schema-compliance + 26 supporting)node scripts/render-harness-config.mjs --check— greenbash tests/install-sh/byte-identical.test.sh— 2/2 PASSbash tests/plugin/hook-paths.test.sh— 60/60 PASSpackages/core/principles/24-plugin-manifest-integrity.test.ts— 6/6 PASS (fixed: skip subdirs + comment-stripping for V7 path check)§1.7 Forward-check applied
(compliance with existing disciplines)
install.sh/settings.json/CLAUDE.mdchanges (test coverage of existing hooks; pre-push hook confirms viaPrior-art: skippedtrailer).allowedTopLevelSet followsdeps-hash-check.test.ts:164(established pattern across 14 prior test files).§1.7 Backward-check applied
(cold sweep via
agents/backward-sweep-auditor.md, no diff knowledge)Class of this change = ZCode-emitting hooks (JSON output parsed by
CCt.strict()) + paired tests asserting the allowed top-level key set.Surfaces where class-X occurs (enumerated cold, 16 dogfood emitters + 14 plugin twins):
allowedTopLevel).end-of-turn-reminder: unconditional{decision,reason,systemMessage}, no guard → added arm.session-start(plugin-only): no test.ts at all → created dedicated test.runtime-bridge-dispatch: forwarded output fromdispatch.tsunguarded → added stub arm.inject-project-digest,check-doc-authority-header,inject-output-language) are NOT in.ai-factory/harness-model.json→emitPlugindoes not render them → plugin channel does not deliver them to ZCode consumers. Adding consumer-only hooks changes the SSOT model semantics (needs framework-vs-consumer categorization in hooks entries).inject-project-digestandcheck-doc-authority-headergot schema arms anyway (defensive — the JSON shape must be valid for any harness/manual replay);inject-output-languageis not ZCode-emitting (out of scope).review-sidecar (external cold review): 0 BLOCKER, 4 CONCERN, 4 NIT
runtime-bridge.test.ts:85-121contradicts hook behavior): pre-existing, noted.allowedTopLevelSet duplicated in 17 files —#two-prompts-driftrisk): deferred to follow-up cleanup PR — refactoring 17 passing files is out of scope here; Sets are currently identical (verified, no drift).Pre-push notes
Prior-art: skippedsubstance arm — warn-only calibration window (perpre-push.ts:340).