feat(dispatcher): park-chips for strategic forks, pointer-only and re-verified at click - #1426
Merged
Conversation
…-verified at click F1 ratified park-chips (D3-D5) on 2026-08-17, which was the only thing holding S3; S1 landed in #1419. Both ADR §6 preconditions met, so this ships the stage on its declared surface — `dispatcher/SKILL.md` §3 — and nothing else. Type 2 (strategic fork) gains a park-chip emission step plus the contract and the decision-session protocol D3/D4 specify: capability-gated on an invocable `spawn_task`; title carries the question + umbrella and MAY batch a burst, which is the observed shape; tldr carries the recommended seat class, because `spawn_task` cannot name a model and the operator's pick at click IS the seat control; prompt carries POINTERS ONLY, never the park payload body, since a chip outlives the state it was minted from. The clicked session re-fetches the park from aif and reports-and-stops when it is already answered — that re-verification is the park-chip's analogue of the dispatch chip's click-time stage gate — then assembles a decision package, applies it with `answer.ts` itself, and records to the task comment + PR `## Parked questions`. Composition, stated rather than assumed: the routing-seats table added by the advisor pattern (#1374) still moves first — when the advisor is reachable, the ask + ASK is the opening move, and the chip is what materialises a decision venue when no live seat holds the question. Deliberately NOT extended to principle 18. The ADR scopes S3 to this one surface, and park-chips carry no stage gate; the assertion question is recorded as a follow-up rather than smuggled into the stage. ADR §6/§7 updated to record the ratification and the shipped state. §1.7: Forward — capability-gate posture reuses D2 verbatim (runtime roster probe, never allowed-tools) per .claude/skills/pipeline/references/output-format.md:521, and the CC-absent path follows dual-implementation-discipline.md:4 capability-check-not-brand-name at .claude/skills/dispatcher/SKILL.md:359. Backward — class = «a session-bound emitter gains an ephemeral chip channel»; swept the sibling emitters: pipeline §10 and arch §3 emit DISPATCH chips only and are untouched (.claude/skills/arch/SKILL.md:109), the §2.2 park detector is unchanged so no new park class appears, and the morning sweep backstop in §3 stays the mechanical layer — SWEPT-CLEAN each; no surface superseded. Prior-art: skipped — prose stage on an existing shipped skill, implementing decisions D3/D4 already recorded in the merged ADR; no new capability, dependency, or module.
artyhoo
added a commit
that referenced
this pull request
Aug 17, 2026
…as wired (D8) (#1434) ## What and why Stage **S2b** of the pipeline-chips ADR — the D8 PreCompact residue writer. **F8 closed 2026-08-17** (operator GO on the SSOT #108 PreCompact item, given in a live session), so the stage is unblocked; the bench ran first, per D8's own ordering. A compacted session left nothing behind. The D7 context-arm's debounce flag stays spent across an auto-compact, so a session re-climbing after one gets no second nudge — and no record of what it had been doing. D8's shape is the one already registered in SSOT #108: the hook itself **writes** the state, deterministically. Round-1's alternative («remind the model to write a handoff before compaction») is undeliverable as specified — a non-blocking PreCompact hook gives the model no execution window. ## The bench, and what it did not prove D8 makes a liveness bench the gate and orders it **before** the writer. It ran, and its outcome is split three ways rather than summed — [`docs/meta-factory/research-patches/2026-08-17-precompact-liveness-bench.md`](docs/meta-factory/research-patches/2026-08-17-precompact-liveness-bench.md): - **Contract — PROVEN.** 15 cases green; the suite is not vacuous (seeding `select(.isSidechain != true)` → `select(true)` flipped exactly one case, then restored); run against one real 202-line CC transcript, not only fixtures; and end-to-end **reader/writer agreement** — the hook wrote to `.claude/orchestrator-prompts/_residue-e2e-proof.md` (gitignored, `.gitignore:7`) and the `/pipeline` §1 fence, run verbatim, printed it back. - **Manual delivery — NOT FIRED.** Registration needs `.claude/settings.json`, whose own deny-list carries `Write(.claude/settings.json)`; hooks snapshot at session start; `/compact` is an operator action. - **Auto delivery — NOT OBSERVED**, and not observable from history either: 0 files matched `"isCompactSummary":true` across the 60 most-recent transcripts. Consistent with a 1M window, but per T14 that is «coverage insufficient», never «works». D8 pre-accepted this split («an honest *auto path unverified, manual path proven* is an acceptable bench outcome»). What ships is one notch below even that — manual is proven at the CONTRACT level, unfired at the DELIVERY level — so it is stated rather than rounded up. ## Two deviations from D8-as-written (recorded in the ADR, not silent) - **The `auto` matcher is dropped; the hook registers on all triggers.** With `auto` alone the only half a human can trigger on demand is unfireable *by construction*, and a manual `/compact` discards the same context. Bench finding B-2; the writer is matcher-independent (asserted). - **The renderer surface is `emitPlugin`, not `emitClaude`.** `emitClaude` already passes any event through, so there was nothing to change there. The real gap: `emitPlugin` `continue`d silently past every event outside `ZCODE_EVENTS`, so a hook on an inexpressible event vanished with no trace — the `#warning-nobody-reads` shape inverted. It now names each dropped event in a note op (fires today on `SubagentStart`/`SubagentStop`). ## Changes - `.claude/hooks/precompact-residue.sh` — new. Extracts the ai-title anchor (fallback: first user turn) and the **last** recap block, marker sourced from the lang pack; fallback is the last main-thread assistant excerpt. `select(.isSidechain != true)` is load-bearing (subagent turns share the transcript). Writes even with no transcript — «a session existed here and was compacted» is itself the fact the reader needs. Every path exits 0 and stdout stays empty: PreCompact *can* block on exit 2, and blocking compaction on a residue-write failure would strand a session at a full window. - `packages/core/hooks/precompact-residue.test.ts` — 15 cases. Every case pins `AIF_HOOK_LANG`: the same fixture scored `recap` under `en` and `excerpt` under the ambient `ru`, caught by a `bash -x` trace, so an unpinned suite is a coin-flip. - `scripts/render-harness-config.mjs` — the off-`ZCODE_EVENTS` skip becomes a note op. - `.claude/rules/zcode-parity-doctrine.md` — census row 21 (`cc-only`), rollup `Total = 20 → 21` and `cc-only 2 → 3`, §4 rationale, §5 tier-table counts. - `.claude/skills/pipeline/SKILL.md` — §1 Step-1 injection extended **inside the existing line** (the file sits at 599/600). The line still opens `head -200 "$(bash …print-orch-home.sh…)` because principle 39's paired-negative seeds its hardcode by matching that prefix — reshaping the line broke the test, so the shape was kept rather than the test edited. - `docs/meta-factory/prior-art-evaluations.md` — #108 `Last reviewed` → 2026-08-17, PreCompact item marked BUILT with both deviations from its sketch. ## Registration is NOT in this PR — and why it cannot be `.claude/settings.json` is tracked **and** rendered from `.ai-factory/harness-model.json`, with the drift gate checking the real tree every CI run. Editing only the SSOT was tried in a sandbox copy of both files: `✗ harness-config drift: - .claude/settings.json: drift vs SSOT`, exit 1. So the two files move in ONE commit — and one of them is agent-blocked. The hand-off block is below. ## Verification - `npx vitest run packages/core/hooks/precompact-residue.test.ts` → 15/15; seeded-break → `1 failed | 14 passed`; restored → 15/15. - `npx vitest run packages/core/principles/` → 402 (401 pass, 1 skip), 42 files. Principle 39 caught the fence reshape and is green after the fix. - `node scripts/render-harness-config.mjs --check` → `✓ harness-config up-to-date`, with the new note firing. - `bash tests/plugin/hook-paths.test.sh` → PASS=82 FAIL=0. `SNAPSHOT_MODE=compare bash tests/install-sh/snapshot.sh` → 15 pass / 0 fail (byte-identical; the hook is framework-internal, not installer-delivered). `npm run format:check`, `npm run typecheck`, `npm run check:skill-drift` → green. - Worth flagging for the next session in this worktree: `node_modules` was **empty** on arrival (the unprovisioned-worktree class), which made three unrelated suites fail with spawn `-1` before `bash scripts/worktree-node-modules.sh --apply` fixed them. ## Operator hand-off — registration (agent-blocked, per CLAUDE.md «settings.json uncommittable») Idempotent, backs up first, validates before moving, and touches **both** files so the drift gate stays green. Paste as one block: ```bash ( set -euo pipefail cd /Users/art/code/rules-as-tests-aif || exit 1 H='bash "$CLAUDE_PROJECT_DIR/.claude/hooks/precompact-residue.sh"' test -f .claude/hooks/precompact-residue.sh || { echo "hook missing on this checkout — merge the S2b PR first"; exit 1; } if jq -e --arg c "$H" 'any(.hooks.PreCompact[]?.hooks[]?; .command == $c)' .claude/settings.json >/dev/null; then echo "already registered — nothing to do"; exit 0 fi cp .claude/settings.json ".claude/settings.json.bak.$(date +%Y%m%dT%H%M%S)" cp .ai-factory/harness-model.json ".ai-factory/harness-model.json.bak.$(date +%Y%m%dT%H%M%S)" jq --arg c "$H" '.hooks.PreCompact = [{hooks:[{type:"command",command:$c}]}]' .claude/settings.json > /tmp/s.json jq -e . /tmp/s.json >/dev/null && mv /tmp/s.json .claude/settings.json jq --arg c "$H" '.hooks.PreCompact = [{command:$c}]' .ai-factory/harness-model.json > /tmp/m.json jq -e . /tmp/m.json >/dev/null && mv /tmp/m.json .ai-factory/harness-model.json node scripts/render-harness-config.mjs --check && echo "OK — registered, drift-clean. Commit BOTH files together." ) ``` **No matcher on purpose** (bench finding B-2 — an `auto`-only matcher makes the manual half unfireable). Then the residual bench, which only you can run: start a **fresh** session in this repo, run `/compact`, and check that `_residue-<session>.md` appeared under `.claude/orchestrator-prompts/`. The auto half closes itself the first time a session reaches the ceiling. Until that fire is observed, D8's delivery leg is honestly **unverified**; the bench file is where the observation gets recorded. Prior-art: prior-art-evaluations.md#108 (Claude Code orchestrator mechanical-gate hooks, verdict ADOPT) — this is that row's PreCompact item, built rather than re-derived: the «save wave-state before compaction» shape is taken from the entry, and the two places this implementation departs from its sketch (per-session file in the resolved orch-home instead of one clobbered `.claude/session-state.md`; the hook writes rather than reminds) are recorded in the row itself, whose `Last reviewed` date is updated in the same commit. The row's other four items are untouched, and its DECISION-NEEDED note is why registration stays operator-landed. ## Fidelity verdict FIDELITY: skipped — spec-homed contour with no orchestrator-prompts umbrella; an in-session stage authored against the merged ADR, not a deliverable against a kickoff. Same basis as S3 (#1426). ### §1.7 Forward-check applied `attention-is-not-a-mechanism.md:1` — the residue has a NAMED reader (`.claude/skills/pipeline/SKILL.md:85`), not «someone will find the file»; and the renderer change exists precisely because a silent skip is `#warning-nobody-reads` (`scripts/render-harness-config.mjs:421`). `dual-implementation-discipline.md:6` — the hook carries `@cc-only-rationale` with a real reason (PreCompact ∉ `ZCODE_EVENTS`, `scripts/render-harness-config.mjs:46`) plus `@dual-pair: hook-lang-i18n` for the pack it sources; §3 audience triage is done explicitly (framework-internal, not installer-delivered) rather than defaulted. `no-paid-llm-in-ci.md:1` — deterministic bash + jq, zero inference. `language-discipline.md:1` — the residue's own prose is English (category 1); the recap marker stays lang-pack-sourced (category 3), and one test asserts the ru/en split in both directions. `build-first-reuse-default.md:1` — the capability is REUSE-of-verdict: SSOT #108 already carries ADOPT for this hook class, so no new BUILD verdict was minted; the two departures from its sketch are recorded in the row. `doc-authority-hierarchy.md:2` — the new research-patch carries a Type/Owner/Feeds header. ### §1.7 Backward-check applied Class of this change = **«a CC hook is added on an event ZCode cannot express»**. Surfaces where that class occurs, enumerated from `.ai-factory/harness-model.json` + `ZCODE_EVENTS` rather than from this PR's own diff: (a) `SubagentStart` → `inject-subagent-digest`, census row 16 — already classified `cc-only` with a §4 rationale and a documented `PreToolUse:Agent` fallback, **SWEPT-CLEAN**, and it is now also named by the new renderer note; (b) `SubagentStop` → `warn-subagent-report`, row 19 — has a 4D-hybrid parity variant (#1046), so the note naming it is informational, not a gap, **SWEPT-CLEAN**; (c) `WorktreeCreate` → `worktree-setup`, row 20 — `cc-only`, not in default settings, **SWEPT-CLEAN**; (d) the rollup arithmetic in `.claude/rules/zcode-parity-doctrine.md:105` and the tier-table counts at `:114`/`:115` — both restate the census totals and both were stale the moment row 21 landed, **GAP-FOUND → fixed in this PR** (`Total 20→21`, `cc-only 2→3`, «all 20 hooks»→21, «Two rows»→«Three rows», three CC-only events→four); (e) the plugin-twin population, `tests/plugin/hook-paths.test.sh` — generation correctly produced **no** twin for the new hook (`ls plugin/hooks/ | grep -i precompact` → empty), which is the right answer for an event the plugin channel cannot carry, **SWEPT-CLEAN**; (f) the install-sh baselines — the hook is framework-internal, so the fingerprints stayed byte-identical (15/15), **SWEPT-CLEAN**. No surface superseded. ## Parked **Should the residue writer ship to consumers?** It is framework-internal today (audience triage recorded in the hook header). The consumer-facing consequence is that the `/pipeline` §1 line, which IS shipped, reads a residue a consumer never gets — inert and harmless (`2>/dev/null`), but asymmetric. Widening is a separate decision with its own installer + baseline consequences, not a side effect of this stage. **Still open in the contour:** the first live chip click has still never happened — it would close F3 and F9 by observation alone. F4 and F7 remain open.
artyhoo
added a commit
that referenced
this pull request
Aug 17, 2026
… trigger had already fired (#1438) Decides the two chip-assertion questions parked as a follow-up in #1426. (i) CHIP_SURFACES scope — DECIDED: the /dispatcher surface joins principle 18, but with its OWN family (PARK_CHIP_REQUIRED_SUBSTRINGS), not CHIP_REQUIRED_SUBSTRINGS. Park-chips deliberately carry none of the dispatch gates — no isolation step, no stage gate, because a decision session reads and answers rather than implements — so asserting the dispatch family there would have pinned the wrong contract. Pinned instead: the D4 trio plus the block anchor and the capability gate. #1426's reasoning (the rest of §3 is equally unasserted, so the omission is consistent) is a fact about §3's neighbourhood, not an argument that these invariants are unimportant — that shape is #hope-as-gate. (ii) Names-not-semantics — DECIDED: acceptable as-is, no PreToolUse gate built. The deferral's surviving ground is zero live chip clicks; a gate for a never-exercised surface is #type1-process-on-type2. But the backward sweep found the ADR's own census stale: five live emitting clauses across three gate profiles, not three — the night-end and continuation chips landed 2026-08-09 (#1346), eight days before the 2026-08-17 paragraph that claimed three. The "fourth emitting surface" clause is retired as fired; the remaining trigger is the first click with a gate missing from its prompt. Zero shipped-file churn by construction: the assertion pins text that already exists in dispatcher/SKILL.md, so no snapshot capture and no prettier delta on the shipped surface. Verification: principles suite 43 files / 412 passed; live mutation of the real dispatcher/SKILL.md (dropping "Re-verify at click time") turns the new check RED, then restores clean — the gate is non-tautological against the actual file, not only against the synthetic paired-negatives. Prior-art: skipped — extends an existing principle test with a second substring family plus paired-negatives, and corrects a stale census in an existing ADR; no new dependency, no new file, no new capability. Co-authored-by: Test <test@example.com>
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.
What and why
Stage S3 of the pipeline-chips ADR. Both preconditions from §6 are now met — S1 merged (#1419) and F1 ratified as park-chips (D3–D5) by the operator on 2026-08-17 — so this ships the stage on its declared surface,
.claude/skills/dispatcher/SKILL.md§3, and on nothing else./dispatcher§3 Type 2 (strategic fork) gains the park-chip emission step plus the contract and decision-session protocol that D3/D4 specify:spawn_taskis invocable in the session (runtime roster probe, never a version-sniff, neverallowed-tools). Same posture as D2, reused verbatim rather than restated (.claude/skills/pipeline/references/output-format.md:521).titlecarries the question + umbrella and MAY batch a burst (Decide 6 parked questions [<umbrella>]) — bursts are the observed shape, not the exception.tldrcarries the recommended seat class, becausespawn_taskcannot name a model and the operator's model pick at click IS the seat control (.claude/skills/arch/SKILL.md:1§4 routing).promptcarries pointers only — task-id, kickoff path, routing-seat context — never the park payload body, since a chip outlives the state it was minted from.answer.tsfrom that session; record to the aif task comment + PR## Parked questions.spawn_task→ skip the step silently and surface the park as before. Nothing downstream depends on a chip existing, so the degraded path loses latency, never coverage.Composition, stated rather than assumed. The routing-seats table that the advisor pattern added (#1374) still moves first: when the advisor is reachable, the ask +
ASKis the opening move, and the chip is what materialises a decision venue when no live seat holds the question. At night the chip simply waits, inert, until morning.Scope discipline
Deliberately not extended to principle 18. ADR §6 scopes S3 to
dispatcher/SKILL.md§3, and park-chips carry no stage gate — the assertion question is recorded below as a follow-up rather than smuggled into the stage. ADR §6/§7 are updated to record the ratification and the shipped state.Verification
npx vitest run packages/core/principles/18-meta-orchestrator-output-format.test.ts→ 12/12 (dispatcher is correctly absent fromCHIP_SURFACES; the S1 assertion is untouched).npx vitest run packages/core/principles packages/core/skills→ 226/226 across 23 files.dispatcher/SKILL.mdwas prettier-clean onstagingbefore the edit, so the table was reformatted withprettier --write(verified against the pristine file first, to avoid the reverse mistake of reformatting an already-unclean file).Fidelity verdict
FIDELITY: skipped — spec-homed contour with no orchestrator-prompts umbrella; an in-session stage authored against the merged ADR, not a deliverable against a kickoff.
Prior-art: skipped — prose stage on an existing shipped skill, implementing decisions D3/D4 already recorded in the merged ADR; no new capability, dependency, or module.
§1.7 Forward-check applied
build-first-reuse-default.md:1— REUSE throughout: the chip channel, the capability-gate posture, andanswer.tsall pre-exist; the only new artefact is prose.dual-implementation-discipline.md:4— the degradation path branches on capability, not on a harness brand string, matching the existing probe at.claude/skills/dispatcher/SKILL.md:359.attention-is-not-a-mechanism.md:1— the chip is explicitly not made load-bearing: the morning sweep (questions.ts --project) stays the mechanical backstop and the block states that a park raised with no live/dispatcherproduces no chip at all.no-paid-llm-in-ci.md:1— session-bound, zero CI inference.doc-authority-hierarchy.md:2— the skill's Authoritative-for line is extended to name the new scope it now owns.§1.7 Backward-check applied
Class of this change = «a session-bound emitter gains an ephemeral chip channel». Surfaces where that class occurs: (a)
.claude/skills/pipeline/SKILL.md:522— dispatch-chip emitter, emits per Stage 1-liner, untouched and unaffected by a park-class chip, SWEPT-CLEAN; (b).claude/skills/arch/SKILL.md:109— exit-chip emitter with thebridge: autocarve-out, likewise untouched, SWEPT-CLEAN; (c) the §2.2 park detector and the §3 taxonomy table (.claude/skills/dispatcher/SKILL.md:307) — unchanged, so no new park class or detection path appears and Type 1 / Type 3 routing is unaffected, SWEPT-CLEAN; (d).claude/rules/seat-lifecycle.md:1— binds seat birth to a spawn prompt assigning the role; a park-chip spawns a decision venue, not a registry seat, so no lifecycle phase is implicated, SWEPT-CLEAN; (e) the night policy in.claude/skills/night-mode/SKILL.md:1— the chip is inert until clicked, so it adds no unattended turn, which is the property D3 chose it for, SWEPT-CLEAN. No surface superseded.Parked
Should park-chip prose be asserted the way dispatch-chip prose is? Principle 18's chip family pins the three dispatch gates as literal substrings on three surfaces. The park-chip invariants (pointer-only, re-verify-at-click, report-and-stop) are the same shape of load-bearing instruction and are currently unasserted — but so is the rest of
/dispatcher§3 (the Type 1/2/3 discrimination discipline has never been gated), so this is consistent with the surrounding surface rather than a new hole. Out of ADR scope for S3; worth a decision before any fourth emitting surface appears.Still open in the contour: S2b gated on F8 (operator GO on SSOT #108 + the D8 liveness bench), and the first live chip click has still never happened — it would close F3 and F9 by observation alone.