Skip to content

feat(zcode-parity): step 1 — emit-wrapper infra + end-of-turn-reminder B2-C + B1 latent fix - #1031

Merged
artyhoo merged 5 commits into
stagingfrom
feat/zcode-parity-step1-emit-wrapper
Jul 18, 2026
Merged

feat(zcode-parity): step 1 — emit-wrapper infra + end-of-turn-reminder B2-C + B1 latent fix#1031
artyhoo merged 5 commits into
stagingfrom
feat/zcode-parity-step1-emit-wrapper

Conversation

@artyhoo

@artyhoo artyhoo commented Jul 18, 2026

Copy link
Copy Markdown
Owner

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.

  1. Mechanism 1 — _zcode-emit additive helper (NEW infrastructure, zero adopters — declared follow-up umbrella for migration). Sourced _ze_classify + _ze_emit functions providing a universal adapter between plain-stdout CC semantics and strict-JSON ZCode semantics.
  2. Bespoke feat: audit fixes Phase 1+2+3 (R2/R11 drift, manifest SSOT, depcruise integration) #1end-of-turn-reminder B2-C fix. The only runtime-DEAD Stop hook on ZCode (pre-fix: L52 grep '"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, NOT additionalContext).
  3. B1 — inject-project-digest.sh env-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/24 parallel-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)

  • All 9 plan-v3 scope files created/edited + 2 scope additions (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-audit final run = GREEN, 311+ tests across 34+ files.
  • _zcode-emit.test.ts unit tests pass (Mechanism 1 forward+backward). Evidence: npx vitest run hooks/_zcode-emit.test.ts = 17/17 pass.
  • end-of-turn-reminder.test.ts Part A both arms tested in isolation + Part B reason field 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_context all pass.
  • inject-project-digest.test.ts B1 env-first resolution + fallback. Evidence: npx vitest run hooks/inject-project-digest.test.ts = 10/10 pass; mutation-gate proof (reverting L27 → only env_var_first_resolution_reads_consumer_root fails) documented in research-patch.
  • tests/plugin/hook-paths.test.sh extended with _zcode-emit skip-list + case (h) repo_root_resolution_form sweep 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.sh baselines 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.md written with all required sections.
  • §1.7 forward + backward tables copied verbatim into PR body (below) + §1.7 applied sections with file:line citations (below).
  • PR base = 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)

# Item Rationale
1 Added packages/core/principles/24-plugin-manifest-integrity.test.ts V8 skip-list extension (10th file vs plan-v3's 9). Plan-v3 scope named only hook-paths.test.sh as gate-compat edit, but the repo runs a SECOND parallel marker gate in principles/24 V8 with the same skip-set. Without extending it, [V8] hook _zcode-emit: missing marker fails. Same T-ZP-C rationale. Stage 1 self-audit caught it.
2 _ze_classify uses jq -r 'keys[0]' + bash case for allowed-key check instead of regex on serialized form. Plan-v3 did not pin implementation. A regex anchored on ^" would miss {"key":…} (leading {); keys[0] extraction is robust to pretty-printing + key order. Same external behaviour.
3 Restored §7 active-AI-traps enumeration in kickoff.md (was stashed pre-branch). Required by principles/12-ai-laziness-traps.test.ts gate. Stash was a branch-creation artifact; restoration is bookkeeping.
4 (scope) Regenerated tests/install-sh/baselines/{react-native,react-next,react-spa,ts-server}/{greenfield,brownfield}.fingerprint (8 files). These baselines fingerprint the installed consumer tree, which ships .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 via SNAPSHOT_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 (per tests/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)

  • Mechanism 2 (twin auto-derivation) — DROPPED, conflicts with hand-maintained-twin SSOT at render-harness-config.mjs:333.
  • Bespoke feat: self-application + meta-factory foundation (Phase 0.5–3) #2 (warn-subagent-report ZCode variant) — REJECTED, SubagentStop not in ZCODE_EVENTS, already declared CC-only.
  • Migration of 9 existing plugin twins to source _zcode-emit — declared follow-up umbrella.
  • inject-project-digest plugin twin shipping — declared follow-up umbrella.
  • L43/L45 anchor-extraction grep fixes — documented as honest degradation (Part C).
  • SubagentStart loud-declaration renderer-side — documented in research-patch only.

§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,230 grep -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-124 emits decision:"block" + reason: field ↔ thin_recap_emits_reason_not_additional_context (asserts reason: key present, additionalContext: key absent); (d) .claude/hooks/inject-project-digest.sh:27 env-first REPO_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:107 case (h) repo_root_resolution_form sweeps 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:213 if (f.startsWith('_zcode-')) continue skip-list extension ensures the sourced-helper _zcode-emit is exempt from the @dual-pair/@cc-only-rationale marker 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)

Fix Test (file:name) Asserts
Mechanism 1 wrapper pass-through _zcode-emit.test.ts :: passthrough_valid_json_allowed_key stdin {"system":[…]} → stdout byte-identical
Mechanism 1 wrapper plain-text wrap _zcode-emit.test.ts :: wrap_plain_text stdin hello → stdout {"additionalContext":"hello"}
Mechanism 1 wrapper empty silent _zcode-emit.test.ts :: empty_silent_exit0 empty stdin → exit 0, no stdout
Mechanism 1 wrapper edge cases _zcode-emit.test.ts :: multiline_trailing_newline / zero_allowed_keys / double_wrap_prevention correct classification, no double-wrap
Mechanism 1 non-ZCode gate _zcode-emit.test.ts :: non_zcode_passthrough_cat ZCODE_PROJECT_DIR unset → _ze_emit cats stdin unchanged (no-op)
Mechanism 1 gate-compat tests/plugin/hook-paths.test.sh (existing) _zcode-emit extensionless + in skip-list; gate green
Bespoke #1 Part A ZCode arm end-of-turn-reminder.test :: zcode_synthetic_transcript_last_line_extracted_via_role synthetic {role:assistant,type:text,…}last_line non-empty (matched via role arm)
Bespoke #1 Part A CC arm end-of-turn-reminder.test :: cc_transcript_last_line_extracted_via_type CC fixture (outer "type":"assistant") → last_line non-empty (matched via type arm)
Bespoke #1 Part B thin-recap end-of-turn-reminder.test :: zcode_long_markdown_emits_block_decision_with_reason_field >500 chars markdown under _is_zcode{decision:"block", reason:"<nudge>"} (NOT additionalContext)
Bespoke #1 Part B gate end-of-turn-reminder.test :: non_zcode_skips_thin_recap under non-ZCode env, thin-recap branch does not fire
B1 source-level rewrite inject-project-digest.test :: env_var_first_resolution_reads_consumer_root under CLAUDE_PROJECT_DIR=/consumer, fixture at /consumer/.claude/session-bootstrap.md IS read
B1 source-level fallback inject-project-digest.test :: dogfood_fallback_when_env_unset CLAUDE_PROJECT_DIR unset → $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):

Fix removed → Test that catches the regression
Mechanism 1 wrapper absent (helper deleted) _zcode-emit.test.ts :: passthrough_valid_json_allowed_key etc. — all unit tests fail (function undefined)
Mechanism 1 helper not sourced by adopter n/a (no adopter in v3 — but integration test in follow-up umbrella will cover)
Gate skip-list not extended tests/plugin/hook-paths.test.sh fails: _zcode-emit swept, no marker → check (b) fails
Bespoke #1 Part A role arm dropped (keep type only) end-of-turn-reminder.test :: zcode_synthetic_transcript_last_line_extracted_via_role fails: synthetic line has no outer type → last_line empty (current runtime-DEAD state). Strengthened vs v2 (reviewer-B MAJOR): both arms tested in isolation.
Bespoke #1 Part A type arm dropped (keep role only) end-of-turn-reminder.test :: cc_transcript_last_line_extracted_via_type fails: CC line's outer type is the matchable field; role-only misses it on CC
Bespoke #1 Part A both arms dropped either test fails (current runtime-DEAD state)
Bespoke #1 Part B branch absent end-of-turn-reminder.test :: zcode_long_markdown_emits_block_decision_with_reason_field fails: no {decision:"block"} emitted
Bespoke #1 Part B uses additionalContext instead of reason NEW (reviewer-A NM-2): assert emitted JSON contains reason: key, NOT additionalContext: key. Test: end-of-turn-reminder.test :: thin_recap_emits_reason_not_additional_context — fails if field swapped
Bespoke #1 Part B _is_zcode gate absent end-of-turn-reminder.test :: non_zcode_skips_thin_recap fails: branch fires on CC dogfood → regression
B1 rewrite absent (env-first arm removed) inject-project-digest.test :: env_var_first_resolution_reads_consumer_root fails: $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.
B1 fallback absent (env-first unconditional) inject-project-digest.test :: dogfood_fallback_when_env_unset fails: dogfood CC env without env-var can't resolve $0-relative path

Sibling-sweep assertions (T21):

  • B1 sweep — env-first form invariant: all 9 plugin twins that read repo files must carry REPO_ROOT="${CLAUDE_PROJECT_DIR:-…}" (or PROJECT_DIR rename). Extend tests/plugin/hook-paths.test.sh with 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.
  • B2 sweep — warn-subagent-report CC-only containment: no test added — the existing render-harness-config.mjs:256 declaration 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.sh check (b) catches it (would need @dual-pair or @cc-only-rationale marker; the existing @cc-only-rationale at .claude/hooks/warn-subagent-report.sh:7 documents the design intent).

…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.
@artyhoo
artyhoo merged commit 95f0b48 into staging Jul 18, 2026
40 checks passed
@artyhoo
artyhoo deleted the feat/zcode-parity-step1-emit-wrapper branch July 18, 2026 12:19
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>
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