Skip to content

feat(zcode-parity): schema-compliance coverage for all ZCode-emitting hooks + plugin channel - #1030

Merged
artyhoo merged 2 commits into
stagingfrom
feat/zcode-plugin-channel-hooks
Jul 18, 2026
Merged

feat(zcode-parity): schema-compliance coverage for all ZCode-emitting hooks + plugin channel#1030
artyhoo merged 2 commits into
stagingfrom
feat/zcode-plugin-channel-hooks

Conversation

@artyhoo

@artyhoo artyhoo commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the test-coverage gap surfaced by the ZCode 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 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-hooks branch from the prior session + this session's coverage work.

What changed

Schema arms added (each asserts an allowedTopLevel Set + hookEventName NOT 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-dispatch forwarded-output arm via stub dispatch.ts (pins the hook's forward contract; the JSON is constructed in a separate source location)

Plugin channel migration (prior session, in this branch):

  • emitPlugin backend in scripts/render-harness-config.mjs — renders plugin/hooks/hooks.json from .ai-factory/harness-model.json SSOT (plugin channel is the only zcode-working hook path; zcode strips project-config hooks via config_project_hooks_ignored 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) + lang/ packs
  • Output-shape fix in ALL _emit_ctx hooks: top-level hookEventName removed
  • deps-hash-check SSOT-triad (source/dogfood/plugin byte-identity, 3-way guard)
  • research-patch 2026-07-04-zcode-harness-visibility.md corrected (false "hooks run unchanged once registered" 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)
  • node scripts/render-harness-config.mjs --check — green
  • bash tests/install-sh/byte-identical.test.sh — 2/2 PASS
  • bash tests/plugin/hook-paths.test.sh — 60/60 PASS
  • packages/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)

  • Capability-commit gate: N/A — no install.sh/settings.json/CLAUDE.md changes (test coverage of existing hooks; pre-push hook confirms via Prior-art: skipped trailer).
  • 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 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):

  • 14/16 dogfood emitters SWEPT-CLEAN (schema arm present — verified by auditor with null grep for allowedTopLevel).
  • 3 GAP-FOUND by cold auditor (closed in this PR):
    • 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.jsonemitPlugin 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). inject-project-digest and check-doc-authority-header got schema arms anyway (defensive — the JSON shape must be valid for any harness/manual replay); inject-output-language is not ZCode-emitting (out of scope).

review-sidecar (external cold review): 0 BLOCKER, 4 CONCERN, 4 NIT

  • CONCERN-high (false honest-scoping claim that forward-output was covered by dispatch.ts tests): FIXED — stub-dispatch arm now tests the forward path; header comment updated to match.
  • CONCERN-medium-1 (whitespace test passed for the wrong reason — sed trim made indented markers match): 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 the stub arm exercising the END of the hook path (forwarded output), not its bails.
  • CONCERN-low (runtime-bridge.test.ts:85-121 contradicts hook behavior): pre-existing, noted.
  • NIT-2 (allowedTopLevel Set duplicated in 17 files — #two-prompts-drift risk): 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

  • One WARN (not error) on Prior-art: skipped substance arm — warn-only calibration window (per pre-push.ts:340).

… 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
artyhoo merged commit ba04984 into staging Jul 18, 2026
74 of 77 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants