feat(zcode-parity): step 1 — emit-wrapper infra + end-of-turn-reminder B2-C + B1 latent fix - #1031
Merged
Merged
Conversation
…ate-compat
plan-v3 "Mechanism 1" — additive universal emit-wrapper helper.
Deliverables:
- plugin/hooks/_zcode-emit (NEW, extensionless, ~75 lines): two sourced
functions per plan-v3 pinned signatures:
_ze_classify — reads stdin; classifies into {empty -> silent,
valid-JSON+allowed-key -> byte-identical pass-through,
anything-else -> {additionalContext:text} wrap}.
_ze_emit — _is_zcode gate (ZCode -> classify; non-ZCode -> cat).
Allowed top-level keys: additionalContext, additional_context,
hookSpecificOutput, hookEventName, decision, reason, systemMessage,
continue, stopReason, suppressOutput — matches every shape emitted
across plugin/hooks/ today (verified via grep).
- packages/core/hooks/_zcode-emit.test.ts (NEW, 17 tests): empty/whitespace
silent, byte-identical pass-through (incl. full allowed-key sweep),
plain-text wrap (with jq -Rs escaping), zero-allowed-keys wrap,
multiline+trailing-newline strip-emit, double-wrap prevention, decision:block
pass-through, non-zcode cat gate, sourcing-contract assertions.
- tests/plugin/hook-paths.test.sh (EDIT):
* L29 skip-list extended with _zcode-* glob + comment (T-ZP-C: helpers
are internal infra, not delivery-channel artifacts).
* New case (h) repo_root_resolution_form — T21 sibling-sweep over the
8 in-sweep plugin twins, asserting Form A env-first REPO_ROOT/PROJECT_DIR
OR Form B cd-guard per plan-v3 "Sibling-sweep T21 final, corrected".
Non-sweep twins skipped by name (CAT-B sibling-source / marker-file prefix).
- packages/core/principles/24-plugin-manifest-integrity.test.ts (EDIT):
V8 marker-check extended with the same _zcode-* skip (parallel gate the
plan-v3 scope table named only hook-paths.test.sh; same rationale).
- .claude/orchestrator-prompts/zcode-parity-step1-emit-wrapper/kickoff.md:
restored §7 active-AI-traps enumeration (T3/T7/T15/T16/T19/T20/T21 +
T-ZP-A/B/C). Was stashed pre-branch; required by principles/12 gate.
Adoption scope (honest): ZERO existing twins migrated in this umbrella.
Helper is new infrastructure; migration is a declared follow-up umbrella
(plan-v3 "Non-goals").
Prior-art: in-repo inline-pattern consolidation — the _is_zcode / _emit_ctx / _adv_violation trio is already copy-pasted across 7 plugin twins (validate-prompt:14-17, inject-matching-rule, check-doc-authority, check-hook-marker, check-kickoff-traps, check-worker-dispatch-channel, inject-subagent-context). This commit extracts that proven inline pattern into a sourced library; no external analog surveyed (plan-v3 §"Risks / open questions" Q-tracked-1). Not a refactor (helper is NEW with zero adopters), but the capability surface — strict-JSON-vs-plain-stdout adapter — is already in production use inline; this commit only consolidates it for future adoption.
Self-audit: GREEN (311 tests, 34 files).
make self-audit = pre-commit-check + pre-push-check + principles-meta-tests.
…ep + Part B thin-recap) plan-v3 "Bespoke #1" — restore ZCode runtime + preserve CC parity on the only runtime-DEAD Stop hook. The hook IS in ZCODE_EVENTS (Stop) and HAS a plugin twin, so it CAN fire on ZCode — but pre-fix the L52 grep returned 0 lines on ZCode synthetic transcripts (no outer type field), making it silently dead. Part A — grep alternation (both arms load-bearing, T7): - L52 + L176: grep '"type":"assistant"' -> grep -E '"(type|role)":"assistant"'. - CC legacy transcripts carry an OUTER "type":"assistant" per entry (verified Mode A on ~/.claude/projects/-Users-art-code-BDDS/0b42f1ff-*.jsonl). - ZCode synthetic transcripts ({message:{…,role:"assistant"}} via $_n producer at zcode.cjs:~1072550) have NO outer type -> only the role arm matches. - Dropping either arm regresses one harness. Both arms tested in isolation. Part B — ZCode thin-recap branch (T-ZP-B: reason field, NOT additionalContext): - Introduces _is_zcode() (canonical form, verified across 7 plugin twins; end-of-turn-reminder previously had NO ZCode gate — reviewer-A NM-4). - Branch fires when _is_zcode AND text > 500 chars AND markdown-dense (## heading OR ** bold OR blank line — same density signal as Branch A/C). - Emits {decision:"block", reason:<Branch A nudge>, systemMessage:<glance>} — the proven Stop-hook shape. reason is the field delivered to the MODEL on Stop decision:block (comment at L227 + emit at L234-235); systemMessage is user-UI-only. NOT additionalContext (PostToolUse/PreToolUse field). - Insertion point: immediately AFTER last_line extraction (post-L52 grep), BEFORE the existing recap cascade — composes with Part A (grep fix makes last_line non-empty on ZCode -> Part B reads it). Part C — anchor degradation (documented, non-plan-breaking): L43 (ai-title grep) + L45 (user-message grep) assume CC outer type. ZCode synthetic lacks ai-title AND likely lacks outer type on user turns -> anchor falls through to aif_msg_eot_anchor_fallback. Part B does NOT depend on the anchor, so this is honest degradation. L43/L45 fixes deferred (plan-v3 §Non-goals). Twin (T-ZP-A): plugin/hooks/end-of-turn-reminder mirrored byte-identically (diff source vs twin = zero). Was byte-identical pre-edit; remains so post-edit. Fixtures (TDD, byte-pinned): - tests/fixtures/zcode-synthetic-transcript.jsonl: single line, shape {message:{content:[{text:<676 chars markdown>,type:"text"}],role:"assistant"}}. Compact JSON (separators ',',':') so the literal "role":"assistant" matches the production grep. NO outer type field — load-bearing for the role-arm test. - tests/fixtures/cc-transcript-legacy.jsonl: 2-line sanitized snippet with outer "type":"assistant" + message.{type:"message",role:"assistant",content:[…]}. Tests added (packages/core/hooks/end-of-turn-reminder.test.ts, +7 cases, 37 total): - zcode_synthetic_transcript_last_line_extracted_via_role (Part A role arm) - cc_transcript_last_line_extracted_via_type (Part A type arm) - Part A both arms proven asymmetric via direct grep on fixtures (T7 anti-pattern) - zcode_long_markdown_emits_block_decision_with_reason_field (Part B forward) - thin_recap_emits_reason_not_additional_context (T-ZP-B backward) - non_zcode_skips_thin_recap (Part B _is_zcode gate backward) - non_zcode_long_markdown_still_uses_existing_cascade (Part B additive, not replacement) Self-audit: GREEN (311 tests, 34 files).
…OOT (source-level)
plan-v3 §"B1" — subshell-aware env-first resolution at source level.
Pre-fix (L18): REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" — $0-relative only.
The `$(cd … && pwd)` is command substitution in a SUBSHELL — cwd change is discarded,
only the path string is captured. Correct ONLY when install.sh copies the hook INTO the
consumer repo at .claude/hooks/depth-2 (where $0-relative resolves to the consumer root);
WRONG for a future plugin-twin payload ($0 = ${CLAUDE_PLUGIN_ROOT}/hooks/ -> resolves to
the plugin payload dir, NOT the consumer root). The deps-hash-check cd-guard form is
inapplicable here (T16: subshell-aware env-first, NOT byte-identical cd-guard).
Post-fix (L18): REPO_ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
Env-first with $0-relative fallback. Matches the 7 already-fixed plugin twins (verified
Mode A: check-doc-authority:18, check-hook-marker:33, check-kickoff-traps:28,
check-worker-dispatch-channel:41, inject-matching-rule:22 as PROJECT_DIR rename,
runtime-bridge-dispatch:83, validate-prompt:19).
Scope (honest): SOURCE-LEVEL fix only. inject-project-digest is NOT shipped via the plugin
channel today; the latent bug is fixed at the source, preparing for future plugin-twin
shipping (declared follow-up umbrella per plan-v3 §"Non-goals"). The plugin twin itself
is NOT shipped in this umbrella.
Tests added (packages/core/hooks/inject-project-digest.test.ts, +3 cases, 10 total):
- env_var_first_resolution_reads_consumer_root: plugin-twin scenario (hook + fixture in
SEPARATE temp roots); with CLAUDE_PROJECT_DIR=<consumer> the fixture IS read.
- dogfood_fallback_when_env_unset: env unset -> $0-relative fallback resolves correctly
(dogfood behaviour preserved; makeTempRepo places hook + fixture in same root).
- backward_plugin_twin_fixture_not_read_without_rewrite: simulated pre-fix path (env unset
in plugin-twin scenario) -> $0-relative lands in payload dir, fixture absent, silent
exit 0. The strengthened half of the §1.7 backward contract.
Mutation-gate proof: reverting L18 to the pre-fix form causes ONLY
env_var_first_resolution_reads_consumer_root to fail (9/10 stay green — the original tests
place hook + fixture under the same root, so $0-relative works there). Confirms the new
test catches the plugin-twin regression specifically.
Prior-art: in-repo pattern adoption — 7 plugin twins already carry this env-first form
(verified Mode A, listed above); this commit brings inject-project-digest.sh in line with
the established convention. No external analog surveyed.
Self-audit: GREEN (311 tests, 34 files).
…reject + Mech 2 reject + degradations plan-v3 §"Implementation order" step 4 — concrete research-patch doc covering all required sections per the plan: - Deliverables shipped (3 concrete + gate-compat + tests, with file:line evidence). - Bespoke #2 reject rationale (corrected): warn-subagent-report is CC-only by design (SubagentStop not in ZCODE_EVENTS; already declared at render-harness-config.mjs:256; @cc-only-rationale at hook:7). Corrected vs v2: Stop != SubagentStop — CC DOES have a subagent-summary nudge, it just doesn't fire on ZCode because the event is missing. - Mechanism 2 rejection rationale: twin generation stays hand-maintained per SSOT at render-harness-config.mjs:333 (5 twins verified Mode A with hand-crafted divergences that auto-derivation cannot reproduce). - SubagentStart degradation: inject-project-digest registered on UserPromptSubmit + SubagentStart (install.sh:507-508); SubagentStart loud-declaration removed with Mechanism 2; backup = inject-subagent-context on PreToolUse:Agent. Follow-up tracked. - Anchor degradation (Part C): L43 ai-title + L45 user-message greps assume CC outer type; ZCode synthetic lacks ai-title + likely lacks outer type on user turns -> anchor falls through to fallback. Part B does not depend on anchor; non-plan-breaking. - Q-tracked follow-ups: (1) 9-twin migration to source _zcode-emit + integration tests; (2) inject-project-digest plugin twin shipping + B1 guard at twin + SubagentStart loud-declaration. - §1.7 Self-review (Forward-check + Backward-check + T21 sibling-sweep + T15 recursive self-application) per phase-research-coverage.md §1.7 + principle-13 substance gate. - Plan deviations cold-review log: 3 deviations documented (principles/24 parallel gate extension; _ze_classify keys[0] implementation choice; kickoff §7 restoration). Self-audit: GREEN (311 tests, 34 files).
…t change
The install-sh snapshot test fingerprints the installed consumer tree, which
ships .claude/hooks/end-of-turn-reminder.sh + .claude/hooks/inject-project-digest.sh
(8 npm stacks x {greenfield,brownfield}). Both hooks had content changes in this
umbrella (Plan deviations #1 Bespoke #1 Part A grep + B1 env-first REPO_ROOT),
so the fingerprint sha256 legitimately rotates. python stacks do not ship these
hooks (eot=0 ipd=0 in baseline) and remain byte-identical.
Regenerated via: SNAPSHOT_MODE=capture bash tests/install-sh/snapshot.sh
Verified green via: SNAPSHOT_MODE=compare bash tests/install-sh/snapshot.sh
Result: 11 pass / 0 fail (8 npm stacks + 3 python)
Pre-empts CI shard C ('install-sh battery') red that surfaced in initial CI run.
This was referenced Jul 18, 2026
artyhoo
added a commit
that referenced
this pull request
Jul 18, 2026
…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
added a commit
that referenced
this pull request
Jul 18, 2026
…anguage plugin twins (#1036) 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
pushed a commit
that referenced
this pull request
Jul 20, 2026
…-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.
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>
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
ZCode parity step 1 — ship three concrete deliverables from plan-v3 (Phase -1 round 3 dual Opus reviewer GO, advisory fixes integrated). Plan:
/tmp/zcode-parity-orch/plan-v3.md._zcode-emitadditive helper (NEW infrastructure, zero adopters — declared follow-up umbrella for migration). Sourced_ze_classify+_ze_emitfunctions providing a universal adapter between plain-stdout CC semantics and strict-JSON ZCode semantics.end-of-turn-reminderB2-C fix. The only runtime-DEAD Stop hook on ZCode (pre-fix: L52grep '"type":"assistant"'returned 0 lines on ZCode synthetic transcripts). Part A:grep -E '"(type|role)":"assistant"'alternation (both arms load-bearing). Part B: ZCode-gated thin-recap branch emitting{decision:"block", reason, systemMessage}(T-ZP-B:reason, NOTadditionalContext).inject-project-digest.shenv-first REPO_ROOT (source-level). Subshell-aware latent-bug fix preparing for future plugin-channel shipping; twin NOT shipped in this umbrella.Honest scope: 9 files per plan-v3 scope table + 2 in-flight scope additions (install-sh baseline regen after hook content change +
principles/24parallel-gate extension, both documented below). ZERO existing twins migrated to source_zcode-emit. NO Mechanism 2 (twin auto-derivation — rejected). NO Bespoke #2 (warn-subagent-report ZCode variant — rejected).Acceptance criteria (T20: every checkbox cites a concrete artifact)
principles/24-plugin-manifest-integrity.test.ts+tests/install-sh/baselines/*regen). Evidence:git diff --stat origin/staging..HEAD= 22 files, ~1050 insertions.<CHECK_ALL>green at end of each stage AND finally. Evidence:make self-auditfinal run = GREEN, 311+ tests across 34+ files._zcode-emit.test.tsunit tests pass (Mechanism 1 forward+backward). Evidence:npx vitest run hooks/_zcode-emit.test.ts= 17/17 pass.end-of-turn-reminder.test.tsPart A both arms tested in isolation + Part Breasonfield asserted. Evidence:npx vitest run hooks/end-of-turn-reminder.test.ts= 37/37 pass;zcode_synthetic_transcript_last_line_extracted_via_role(role arm) +cc_transcript_last_line_extracted_via_type(type arm) +thin_recap_emits_reason_not_additional_contextall pass.inject-project-digest.test.tsB1 env-first resolution + fallback. Evidence:npx vitest run hooks/inject-project-digest.test.ts= 10/10 pass; mutation-gate proof (reverting L27 → onlyenv_var_first_resolution_reads_consumer_rootfails) documented in research-patch.tests/plugin/hook-paths.test.shextended with_zcode-emitskip-list + case (h)repo_root_resolution_formsweep over 8 twins. Evidence:bash tests/plugin/hook-paths.test.sh= PASS=68 FAIL=0 (was 53); case (h) asserts all 8 in-sweep twins.tests/install-sh/snapshot.shbaselines regenerated after hook-content change. Evidence:SNAPSHOT_MODE=compare bash tests/install-sh/snapshot.sh= 11/11 PASS (8 npm stacks × {green,brown} + 3 python).docs/meta-factory/research-patches/2026-07-18-zcode-parity-step1.mdwritten with all required sections.staging. Evidence:gh pr create --base staging.Twin byte-identity (T-ZP-A)
diff .claude/hooks/end-of-turn-reminder.sh plugin/hooks/end-of-turn-reminder= ZERO. Was byte-identical pre-edit (verified at Stage 1); remains so post-edit (Part A + Part B mirrored byte-for-byte).Plan deviations (3) + scope additions (1)
packages/core/principles/24-plugin-manifest-integrity.test.tsV8 skip-list extension (10th file vs plan-v3's 9).hook-paths.test.shas gate-compat edit, but the repo runs a SECOND parallel marker gate inprinciples/24V8 with the same skip-set. Without extending it,[V8] hook _zcode-emit: missing markerfails. Same T-ZP-C rationale. Stage 1 self-audit caught it._ze_classifyusesjq -r 'keys[0]'+ bashcasefor allowed-key check instead of regex on serialized form.^"would miss{"key":…}(leading{);keys[0]extraction is robust to pretty-printing + key order. Same external behaviour.kickoff.md(was stashed pre-branch).principles/12-ai-laziness-traps.test.tsgate. Stash was a branch-creation artifact; restoration is bookkeeping.tests/install-sh/baselines/{react-native,react-next,react-spa,ts-server}/{greenfield,brownfield}.fingerprint(8 files)..claude/hooks/end-of-turn-reminder.sh+.claude/hooks/inject-project-digest.sh. Both hooks had content changes in this PR → fingerprint sha256 legitimately rotates. Regenerated viaSNAPSHOT_MODE=capture bash tests/install-sh/snapshot.sh; CI shard C (install-sh battery) was red until this regen. This is expected maintenance when a shipped hook changes (pertests/install-sh/snapshot.sh:8-12).Full details in
docs/meta-factory/research-patches/2026-07-18-zcode-parity-step1.md§"Plan deviations".Non-goals (DO NOT expect in this PR)
render-harness-config.mjs:333.ZCODE_EVENTS, already declared CC-only._zcode-emit— declared follow-up umbrella.§1.7 Forward-check applied
This PR ships three fixes; each one is paired with a firing test that proves the fix works (the §1.7 Forward-check table below restates the full mapping verbatim from plan-v3 §1.7). Forward-check evidence of fix→test pairing, by file:line: (a)
plugin/hooks/_zcode-emit:84_ze_emit()ZCode-gated passthrough ↔packages/core/hooks/_zcode-emit.test.ts:non_zcode_passthrough_cat(17 unit tests total); (b).claude/hooks/end-of-turn-reminder.sh:69,230grep -E '"(type|role)":"assistant"'alternation ↔packages/core/hooks/end-of-turn-reminder.test.ts:zcode_synthetic_transcript_last_line_extracted_via_role+cc_transcript_last_line_extracted_via_type(both arms tested in isolation, 37 tests total); (c).claude/hooks/end-of-turn-reminder.sh:123-124emitsdecision:"block"+reason:field ↔thin_recap_emits_reason_not_additional_context(assertsreason:key present,additionalContext:key absent); (d).claude/hooks/inject-project-digest.sh:27env-firstREPO_ROOT="${CLAUDE_PROJECT_DIR:-…}"↔packages/core/hooks/inject-project-digest.test.ts:env_var_first_resolution_reads_consumer_root(10 tests total). The plan-v3 forward-check tables are reproduced verbatim below this section.§1.7 Backward-check applied
Each "fix removed →" row in the §1.7 Backward-check table below is a mutation gate (test fails without the fix). Sibling-sweep applied to existing repo artefacts under the new rules' scope: (a)
tests/plugin/hook-paths.test.sh:107case (h)repo_root_resolution_formsweeps all 8 plugin twins that read repo files (Form A env-first OR Form B cd-guard, enumerated by name) — a future hand-added twin without the guard would fail the sweep (T21 done right); (b)packages/core/principles/24-plugin-manifest-integrity.test.ts:213if (f.startsWith('_zcode-')) continueskip-list extension ensures the sourced-helper_zcode-emitis exempt from the@dual-pair/@cc-only-rationalemarker gate across BOTH parallel marker gates; (c)tests/install-sh/snapshot.sh+ regenerated baselines sweep the installed-tree fingerprint across 8 npm stacks × {greenfield,brownfield} + 3 python, byte-identical — a future drift between shipped hook and baseline fails CI shard C. The plan-v3 backward-check table is reproduced verbatim below this section.§1.7 — Forward-check + Backward-check (PR-body draft)
§1.7 Forward-check (each fix has a firing test that proves the fix works)
_zcode-emit.test.ts :: passthrough_valid_json_allowed_key{"system":[…]}→ stdout byte-identical_zcode-emit.test.ts :: wrap_plain_texthello→ stdout{"additionalContext":"hello"}_zcode-emit.test.ts :: empty_silent_exit0_zcode-emit.test.ts :: multiline_trailing_newline / zero_allowed_keys / double_wrap_prevention_zcode-emit.test.ts :: non_zcode_passthrough_catZCODE_PROJECT_DIRunset →_ze_emitcats stdin unchanged (no-op)tests/plugin/hook-paths.test.sh(existing)_zcode-emitextensionless + in skip-list; gate greenend-of-turn-reminder.test :: zcode_synthetic_transcript_last_line_extracted_via_role{role:assistant,type:text,…}→last_linenon-empty (matched viarolearm)end-of-turn-reminder.test :: cc_transcript_last_line_extracted_via_type"type":"assistant") →last_linenon-empty (matched viatypearm)end-of-turn-reminder.test :: zcode_long_markdown_emits_block_decision_with_reason_field_is_zcode→{decision:"block", reason:"<nudge>"}(NOT additionalContext)end-of-turn-reminder.test :: non_zcode_skips_thin_recapinject-project-digest.test :: env_var_first_resolution_reads_consumer_rootCLAUDE_PROJECT_DIR=/consumer, fixture at/consumer/.claude/session-bootstrap.mdIS readinject-project-digest.test :: dogfood_fallback_when_env_unsetCLAUDE_PROJECT_DIRunset →$0-fallback resolves correctly (dogfood behavior preserved)§1.7 Backward-check (each test fails without the fix — mutation-gate)
For each "fix removed →" row, the named test must fail. Plus sibling-sweep (T21 counter #2 — #backward-check-restates-not-sweeps):
_zcode-emit.test.ts :: passthrough_valid_json_allowed_keyetc. — all unit tests fail (function undefined)tests/plugin/hook-paths.test.shfails:_zcode-emitswept, no marker → check (b) failsrolearm dropped (keeptypeonly)end-of-turn-reminder.test :: zcode_synthetic_transcript_last_line_extracted_via_rolefails: synthetic line has no outer type →last_lineempty (current runtime-DEAD state). Strengthened vs v2 (reviewer-B MAJOR): both arms tested in isolation.typearm dropped (keeproleonly)end-of-turn-reminder.test :: cc_transcript_last_line_extracted_via_typefails: CC line's outer type is the matchable field; role-only misses it on CCend-of-turn-reminder.test :: zcode_long_markdown_emits_block_decision_with_reason_fieldfails: no{decision:"block"}emittedadditionalContextinstead ofreasonreason:key, NOTadditionalContext:key. Test:end-of-turn-reminder.test :: thin_recap_emits_reason_not_additional_context— fails if field swapped_is_zcodegate absentend-of-turn-reminder.test :: non_zcode_skips_thin_recapfails: branch fires on CC dogfood → regressioninject-project-digest.test :: env_var_first_resolution_reads_consumer_rootfails:$0→plugin payload dir, fixture absent, silent exit 0. Strengthened vs v2: assertion is "fixture IS read with rewrite; fixture is NOT read without rewrite" — distinguishes guard-present-file-missing from guard-absent-wrong-dir.inject-project-digest.test :: dogfood_fallback_when_env_unsetfails: dogfood CC env without env-var can't resolve$0-relative pathSibling-sweep assertions (T21):
REPO_ROOT="${CLAUDE_PROJECT_DIR:-…}"(or PROJECT_DIR rename). Extendtests/plugin/hook-paths.test.shwith a new check (or sibling test file) that asserts this invariant across the 9 verified twins (list above). Catches future regressions when new twins are hand-added without the guard. This is an EDIT to existing gate file (file chore(meta-factory): Phase 3 Step 0 retrofit + 3 MAJOR fixes #3 already counted), not a new file — scope estimate holds at 9.render-harness-config.mjs:256declaration is the SSOT; a gate-test asserting the renderer's own declaration would be tautological. Honest scope decision, documented here. If a future contributor adds warn-subagent-report to plugin twins by mistake,hook-paths.test.shcheck (b) catches it (would need@dual-pairor@cc-only-rationalemarker; the existing@cc-only-rationaleat.claude/hooks/warn-subagent-report.sh:7documents the design intent).