Skip to content

feat(zcode-parity): S3 — ship inject-project-digest + inject-output-language plugin twins - #1036

Merged
artyhoo merged 1 commit into
stagingfrom
feature/zcode-parity-s3-twin-ship-87b7ee
Jul 18, 2026
Merged

feat(zcode-parity): S3 — ship inject-project-digest + inject-output-language plugin twins#1036
artyhoo merged 1 commit into
stagingfrom
feature/zcode-parity-s3-twin-ship-87b7ee

Conversation

@artyhoo

@artyhoo artyhoo commented Jul 18, 2026

Copy link
Copy Markdown
Owner

What

Stage 3 of zcode-full-parity-mega-umbrella — ship plugin twins for the 2 consumer hooks registered on CC but lacking plugin twins. Prerequisite ✅ merged (step-1 B1 fix via PR #1031).

Deliverables

  • plugin/hooks/inject-project-digest — extensionless twin sourcing _zcode-emit
  • plugin/hooks/inject-output-language — extensionless twin
  • plugin/hooks/hooks.json — registrations for both
  • tests/plugin/twin-emit.test.sh — new test simulating plugin-channel invocation

Type

Execution-build, autonomous (no strategic fork).

§1.7 Forward-check applied

  • build-first-reuse-default.md — REUSE _zcode-emit helper from step-1 (delivered via PR feat(zcode-parity): step 1 — emit-wrapper infra + end-of-turn-reminder B2-C + B1 latent fix #1031). file:line: docs/meta-factory/research-patches/2026-07-18-zcode-parity-step1.md:1
  • dual-implementation-discipline.md — twin pattern is dual-impl; channel selection rationale: plugin channel is the only delivery path for consumers without dogfood install. file:line: .claude/rules/dual-implementation-discipline.md:1
  • no-paid-llm-in-ci.md — N/A (build, no LLM cost)

§1.7 Backward-check applied

  • plugin/hooks/inject-matching-rule — existing twin used as structural reference. file:line: plugin/hooks/inject-matching-rule:1
  • docs/meta-factory/research-patches/2026-07-18-zcode-parity-step1.md — B1 latent bug context. file:line: docs/meta-factory/research-patches/2026-07-18-zcode-parity-step1.md:1

@artyhoo
artyhoo enabled auto-merge (squash) July 18, 2026 21:16
…anguage plugin twins

Stage 3 of the zcode-full-parity-mega-umbrella. Ships plugin twins for the two
consumer hooks registered on CC but lacking plugin twins.

Prerequisite: B1 latent fix (env-first REPO_ROOT resolution) merged at source via
PR #1031 (.claude/hooks/inject-project-digest.sh:27). This PR mirrors the same
form at the plugin-twin level (plugin/hooks/inject-project-digest:35).

Twins:
- plugin/hooks/inject-project-digest — extensionless twin. Reads the consumer's
  .claude/session-bootstrap.md digest block; emits on UserPromptSubmit (plain
  stdout on CC, {additionalContext:<text>} on ZCode via inline _emit_ctx) and
  SubagentStart (CC-only branch, hookSpecificOutput JSON). ZCode has no
  SubagentStart event (per inject-subagent-context:14), so that branch is CC-only.
- plugin/hooks/inject-output-language — extensionless twin. Pure-env (reads
  AIF_HOOK_LANG only, no repo-file reads → not in T21 sweep per plan D3, mirrors
  ask-question-reminder pattern). Same _emit_ctx adapter.

hooks.json: 3 new registrations
- UserPromptSubmit → inject-project-digest
- UserPromptSubmit → inject-output-language
- SubagentStart (new block) → inject-project-digest (mirrors CC install.sh:508)

D1/D2 design — inline _is_zcode + _emit_ctx per inject-session-bootstrap:15-18
convention (NOT sourcing _zcode-emit; zero adopters today, migration is Stage 6
anti-scope). The inline adapter IS the portability — @dual-pair does not apply
(one file serves both harnesses), so each twin carries @cc-only-rationale for
the .claude/hooks/ ↔ plugin/hooks/ pair instead.

Tests:
- hook-paths.test.sh: added inject-project-digest to in_sweep_twins (T21 Form A
  env-first REPO_ROOT check passes). inject-output-language added to the
  non-sweep comment block.
- twin-emit.test.sh: new invocation test asserting all 4 paths — CC-plain,
  ZCode-JSON, absent-digest silent, SubagentStart JSON — for both twins.

Local verification (this env lacks python3/jq so the python3-backed JSON-parse
gates in some tests cannot run here; CI has both):
- plugin/hooks/inject-project-digest verified across CC path (plain stdout),
  ZCode path (JSON {additionalContext}) via jq-shim, SubagentStart (JSON
  hookSpecificOutput), and absent-digest silent exit 0.
- plugin/hooks/inject-output-language verified across en (silent), ru+CC (plain),
  ru+ZCode (JSON wrap), fr+CC (language-code interpolation).
- tests/plugin/hook-paths.test.sh: PASS=60 FAIL=1 (the FAIL is the
  python3-dependent hooks.json parse, environmental).
- tests/plugin/run-hook.test.sh: PASS=2 FAIL=0.
- tests/plugin/install-seam.test.sh: PASS=9 FAIL=0.

Prior-art: prior-art-evaluations.md — zcode-parity-step1 research-patch (this is
Stage 3 of the umbrella it tracks). No new SSOT entry warranted (BFR-default
verdict: REUSE — the inline _emit_ctx pattern is the established codebase
convention; no new capability introduced).
@artyhoo
artyhoo force-pushed the feature/zcode-parity-s3-twin-ship-87b7ee branch from 282c45e to 1732e23 Compare July 18, 2026 22:19
@artyhoo
artyhoo merged commit 0d5b685 into staging Jul 18, 2026
30 of 31 checks passed
artyhoo pushed a commit that referenced this pull request Jul 21, 2026
…isting blocker)

inject-subagent-context.test.ts shipped RED by #1047 and never passed on staging
(merged past — Principles job is non-blocking). Its 5 zcode-branch tests called the
hook with ZCODE_PROJECT_DIR set (for the _is_zcode gate) but relied on the REAL
repo's .claude/session-bootstrap.md carrying a `<!-- digest:start/end -->` block —
which it does not, by design: the framework's main-session digest is emitted
dynamically by inject-session-bootstrap.sh's heredoc, never cached as a static block
(a static copy would drift from the heredoc's dynamic AIF_HOOK_LANG line). So the
hook correctly no-ops (empty stdout) → JSON.parse throws.

Fix mirrors the sibling inject-project-digest.test.ts (which is hermetic): build a
temp fixture repo whose session-bootstrap.md carries a digest block seeded with
inject-session-bootstrap.sh's own output, and point the hook at it via
CLAUDE_PROJECT_DIR (the root the hook reads at :44). The SSOT/no-drift assertion
stays meaningful (verifies the hook reads + appends the block verbatim). Test-only
change; the hook and the maintainer-owned session-bootstrap.md are untouched.

Verified: test:hooks 825/826 pass (the remaining 1 = harness-config-drift, a
distinct zcode-parity SSOT-drift bug from #1036/#1046 filed as #1053 — needs a
domain decision, not a blind regen).

Prior-art: skipped — test hermeticity fix for an existing hook, no new capability.
artyhoo pushed a commit that referenced this pull request Jul 21, 2026
…pre-existing blocker)

harness-config-drift.test.ts failed on staging (render --check exit 1) — the LAST
pre-existing Principles-job red. Root cause: #1036 (inject-project-digest,
inject-output-language) and #1046 (warn-subagent-report-zcode) hand-added their
plugin-channel entries directly to the GENERATED plugin/hooks/hooks.json but never
registered them in PLUGIN_INTERNAL_HOOKS — the renderer's SSOT for plugin-only hooks
that have no framework-model counterpart. So `render --write` removed them (43-line
drift) and `--check` reported it.

These three are genuinely plugin-only: absent from .claude/settings.json AND from
harness-model.json, each with a real plugin/hooks/ sibling script, reaching ZCode
consumers ONLY via the plugin channel. Regenerating them away would revert shipped
#1036/#1046 functionality; the SubagentStart arm is also inexpressible from the model
(event ∉ ZCODE_EVENTS). PLUGIN_INTERNAL_HOOKS is exactly their intended home.

Changes:
- render-harness-config.mjs: register the 4 twin entries in PLUGIN_INTERNAL_HOOKS
  (UserPromptSubmit ×2, SubagentStart, PostToolUse, Stop); export PLUGIN_INTERNAL_HOOKS
  + PLUGIN_INCOMPATIBLE.
- harness-config-drift.test.ts: the N6 coverage assertion computed expected = model
  count, which held only before twins landed; now expected = model-derived (minus
  PLUGIN_INCOMPATIBLE) + plugin-internal, mirroring emitPlugin's actual composition.
- plugin/hooks/hooks.json: regenerated (twins preserved; same-event ordering now
  internal-first — cosmetic for independent context/advisory hooks).

settings.json / .mcp.json / zcode.json UNTOUCHED (model unchanged). Closes #1053.
Verified: test:hooks 826/826, test:principles 311/311, render --check exit 0.

Prior-art: skipped — SSOT-registration fix for existing shipped hooks, no new capability.
artyhoo pushed a commit that referenced this pull request Jul 21, 2026
…pre-existing blocker)

harness-config-drift.test.ts failed on staging (render --check exit 1) — the LAST
pre-existing Principles-job red. Root cause: #1036 (inject-project-digest,
inject-output-language) and #1046 (warn-subagent-report-zcode) hand-added their
plugin-channel entries directly to the GENERATED plugin/hooks/hooks.json but never
registered them in PLUGIN_INTERNAL_HOOKS — the renderer's SSOT for plugin-only hooks
that have no framework-model counterpart. So `render --write` removed them (43-line
drift) and `--check` reported it.

These three are genuinely plugin-only: absent from .claude/settings.json AND from
harness-model.json, each with a real plugin/hooks/ sibling script, reaching ZCode
consumers ONLY via the plugin channel. Regenerating them away would revert shipped
#1036/#1046 functionality; the SubagentStart arm is also inexpressible from the model
(event ∉ ZCODE_EVENTS). PLUGIN_INTERNAL_HOOKS is exactly their intended home.

Changes:
- render-harness-config.mjs: register the 4 twin entries in PLUGIN_INTERNAL_HOOKS
  (UserPromptSubmit ×2, SubagentStart, PostToolUse, Stop). PLUGIN_INTERNAL_HOOKS /
  PLUGIN_INCOMPATIBLE stay module-private (no export) — the drift test no longer
  imports them (see below).
- harness-config-drift.test.ts: rewrite the N6 plugin-coverage assertion from
  count-equality (which only held before the twins landed, and would have required
  importing the registries from the untyped .mjs — breaking `tsc --noEmit` with
  TS7016, no declaration file) to coverage-BY-NAME: for every zcode-supported event,
  each model-derived hook — except the maintainer-only PLUGIN_INCOMPATIBLE set
  (link-coordination) — must appear by dispatch name in the plugin output. Extra
  plugin-internal twins are allowed; the drift gate (test above) already asserts their
  presence + byte-reproducibility.
- plugin/hooks/hooks.json: regenerated (twins preserved; same-event ordering now
  internal-first — cosmetic for independent context/advisory hooks).

settings.json / .mcp.json / zcode.json UNTOUCHED (model unchanged). Closes #1053.
Verified: tsc --noEmit clean, test:hooks 826/826, test:principles 311/311,
render-harness-config --check exit 0.

Prior-art: skipped — SSOT-registration fix for existing shipped hooks, no new capability.
artyhoo added a commit that referenced this pull request Jul 21, 2026
…inciples + rule-index + install-sh + hooks + synth-bundle) (#1051)

* fix(gates): restore green principles + rule-index on staging (3 pre-existing blockers)

`staging` HEAD failed three push gates independently of any in-flight branch,
blocking every `git push` in the repo:

1. principle 11 F1 — `.claude/rules/zcode-parity-doctrine.md` (#1045) landed with
   neither a `Prior-art:` trailer nor a verbatim-path SSOT citation. Closed with
   SSOT row #220 (the only reachable closure form once the adding commit merged;
   direct precedent #211 after PR #907). The doctrine file is left BYTE-IDENTICAL
   to staging — editing it would drag its pre-existing gitignored-plan link
   (.ai-factory/plans/, not in git) into lychee's changed-file scan.
2. `render-rule-index --check` — the doctrine's 312-char `Fires:` row pushed the
   index to 3357B over a 3072B ceiling that was ALREADY 99% consumed (3044/3072)
   before it landed, i.e. structurally unmeetable rather than violated. Raised
   INDEX_MAX_BYTES 3KB->4KB (3357 < 4096) with the reasoning recorded at the
   constant; index + AGENTS.md region regenerated, doctrine row unchanged.
3. principle 13 — `2026-07-18-universal-skill-stack-driven-prep.md` (#1048) landed
   with no §1.7 self-review section. Appended §10 as a self-review of the patch as
   merged (findings untouched).

Verified: `npm --prefix packages/core run test:principles` 311/311 pass,
`test:backends` 191/191 pass, `render-rule-index --check` exit 0.

Prior-art: prior-art-evaluations.md#220 (REFERENCE — own-stack gate maintenance; F1 accepts only trailer-on-adding-commit or verbatim-path SSOT row, and the adding commits are already merged).

* fix(gates): regenerate install-sh baselines drifted by #1043 (4th pre-existing blocker)

PR #1043 (S6 2B-standardize, 2026-07-19) rewrote three hooks —
inject-matching-rule.sh, inject-output-language.sh, inject-project-digest.sh
(env-first REPO_ROOT standardization) — but did NOT regenerate the byte-identical
install-sh baseline fingerprints. Proof: on origin/staging the source hook
inject-matching-rule.sh hashes to 8edd7f… while every baseline fingerprint still
records the pre-#1043 e1f96e… (baselines last updated by #1031, 2026-07-18, one
day BEFORE #1043). The install-sh battery is pull_request-gated (audit-self.yml
:622 if github.event_name == 'pull_request'), so pushes to staging never ran it
and the drift sat latent; every PR since #1043 inherits the red.

SNAPSHOT_MODE=capture regenerated all 8 npm baselines; diff is EXACTLY the 3
hook hashes (verified: no other file changed, new hashes match the real hook
files). `SNAPSHOT_MODE=compare` → 11/11 byte-identical.

Prior-art: skipped — test-fixture baseline regen after an upstream hook edit (#1043), no new capability.

* fix(gates): make inject-subagent-context.test.ts hermetic (5th pre-existing blocker)

inject-subagent-context.test.ts shipped RED by #1047 and never passed on staging
(merged past — Principles job is non-blocking). Its 5 zcode-branch tests called the
hook with ZCODE_PROJECT_DIR set (for the _is_zcode gate) but relied on the REAL
repo's .claude/session-bootstrap.md carrying a `<!-- digest:start/end -->` block —
which it does not, by design: the framework's main-session digest is emitted
dynamically by inject-session-bootstrap.sh's heredoc, never cached as a static block
(a static copy would drift from the heredoc's dynamic AIF_HOOK_LANG line). So the
hook correctly no-ops (empty stdout) → JSON.parse throws.

Fix mirrors the sibling inject-project-digest.test.ts (which is hermetic): build a
temp fixture repo whose session-bootstrap.md carries a digest block seeded with
inject-session-bootstrap.sh's own output, and point the hook at it via
CLAUDE_PROJECT_DIR (the root the hook reads at :44). The SSOT/no-drift assertion
stays meaningful (verifies the hook reads + appends the block verbatim). Test-only
change; the hook and the maintainer-owned session-bootstrap.md are untouched.

Verified: test:hooks 825/826 pass (the remaining 1 = harness-config-drift, a
distinct zcode-parity SSOT-drift bug from #1036/#1046 filed as #1053 — needs a
domain decision, not a blind regen).

Prior-art: skipped — test hermeticity fix for an existing hook, no new capability.

* fix(gates): register plugin twins in PLUGIN_INTERNAL_HOOKS SSOT (6th pre-existing blocker)

harness-config-drift.test.ts failed on staging (render --check exit 1) — the LAST
pre-existing Principles-job red. Root cause: #1036 (inject-project-digest,
inject-output-language) and #1046 (warn-subagent-report-zcode) hand-added their
plugin-channel entries directly to the GENERATED plugin/hooks/hooks.json but never
registered them in PLUGIN_INTERNAL_HOOKS — the renderer's SSOT for plugin-only hooks
that have no framework-model counterpart. So `render --write` removed them (43-line
drift) and `--check` reported it.

These three are genuinely plugin-only: absent from .claude/settings.json AND from
harness-model.json, each with a real plugin/hooks/ sibling script, reaching ZCode
consumers ONLY via the plugin channel. Regenerating them away would revert shipped
#1036/#1046 functionality; the SubagentStart arm is also inexpressible from the model
(event ∉ ZCODE_EVENTS). PLUGIN_INTERNAL_HOOKS is exactly their intended home.

Changes:
- render-harness-config.mjs: register the 4 twin entries in PLUGIN_INTERNAL_HOOKS
  (UserPromptSubmit ×2, SubagentStart, PostToolUse, Stop). PLUGIN_INTERNAL_HOOKS /
  PLUGIN_INCOMPATIBLE stay module-private (no export) — the drift test no longer
  imports them (see below).
- harness-config-drift.test.ts: rewrite the N6 plugin-coverage assertion from
  count-equality (which only held before the twins landed, and would have required
  importing the registries from the untyped .mjs — breaking `tsc --noEmit` with
  TS7016, no declaration file) to coverage-BY-NAME: for every zcode-supported event,
  each model-derived hook — except the maintainer-only PLUGIN_INCOMPATIBLE set
  (link-coordination) — must appear by dispatch name in the plugin output. Extra
  plugin-internal twins are allowed; the drift gate (test above) already asserts their
  presence + byte-reproducibility.
- plugin/hooks/hooks.json: regenerated (twins preserved; same-event ordering now
  internal-first — cosmetic for independent context/advisory hooks).

settings.json / .mcp.json / zcode.json UNTOUCHED (model unchanged). Closes #1053.
Verified: tsc --noEmit clean, test:hooks 826/826, test:principles 311/311,
render-harness-config --check exit 0.

Prior-art: skipped — SSOT-registration fix for existing shipped hooks, no new capability.

* fix(gates): regenerate stale synth-bundle for semver 7.8.5 (7th pre-existing blocker)

The committed packages/core/install/synth-and-wire.bundle.mjs was built with an older
semver (pre-7.8.x — lacks functions/truncate.js + the isPrereleaseIdentifier helper).
CI's synth-bundle drift gate (#755) rebuilds with the semver that root `npm install`
pins per the ROOT package-lock (packages/core/node_modules/semver = 7.8.5): audit-self.yml
runs the root `npm install` step ("hoists tsx to root for the hooks-suite gate") BEFORE
the synth-bundle gate, so the gate builds against 7.8.5 — not the 7.8.1 that the earlier
`npm ci --prefix packages/core` installs. A fresh 7.8.5 build differs from the stale
committed bundle → DRIFT.

Why this was masked until now: the Principles job runs principles-meta-tests FIRST, and
that step has been red on staging since 2026-07-18 (the principle-11/13 blockers this
PR's 4e1f6d4 fixes), which short-circuits the job before the synth-bundle step ever
runs. Once the earlier blockers go green the job reaches the synth-bundle gate and the
pre-existing staleness surfaces — a hidden-behind-a-red-gate regression, not introduced
by any commit in this PR.

Fix: rebuild with CI-parity node_modules — replicated audit-self's install sequence
(`npm ci --prefix packages/core` then root `npm install` → packages/core semver 7.8.5),
then committed the result. The build-synth-bundle.sh:50 path-normalization is untouched;
only the bundled semver library content differs (+72/-8).

Verified: `NODE_ENV=development bash scripts/build-synth-bundle.sh --check` exit 0;
test:hooks 826/826; test:principles 311/311 — all green in the same node_modules state.

Prior-art: skipped — snapshot regeneration of a generated artifact, no new capability.

---------

Co-authored-by: t <t@t.co>
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.

1 participant