The branch now bases on the hooks stack (PR #138's head), so every 'in-flight' reference to stack content became a current-state fact. Audited all nine RFCs + three supplements claim-by-claim against the merged tree (none invalidated; several strengthened): - prune-dead-core-spine-surface: describe the landed tools/pre-execute → dispatch → tools/post-execute pipeline — listeners return Decisions, the registry builds every result and snapshots it to protect callId, and a second (mutation-guard) test pins the field; drop the resolved wait-for-interception hedge; scope the additionalContext ferry out. - prune-producerless-vocabulary-variants: the ui-stdio fixture migrated off the continuation trigger (llm-replay is the sole writer now); the stack's own additions (rejected, prompt/blocked, hook/invoked+result) all arrived with producers — the admission policy demonstrated live. - prune-unimplemented-subagent-vocabulary: enrichment landed as lastAssistantMessage only (agentType was dropped in the stack's own review — the same judgment this RFC extends); the seam RFC now names tools/pre-execute deny, which exists, sharpening the re-add path. - drop-inert-request-knobs / drop-image-content-block / trim-acp-bridge-unreachable-surface: current-state rewordings (shipped bridges set no request fields; only compact-basic has explicit image arms; 13 hook goldens also pin agentInfo). - generic-long-running-tool-runtime census: second production seam consumer (hook-protocol runHook: resolve+run, stdin/env, foreground only — background machinery stays single-consumer); scrub-duplication blast radius. - discover-package-inventory: identical 54-entry tsconfig reference sets; the comparesLog scenario knob (fixture-derivable, like recorded). - unify-agent-and-session-id: third divergence site (in-process subagent children mint two UUIDs), the hooks bridge id-lookups, and ui-stdio's labelBySession map as a consumer that deletes under unification. New RFCs from the post-stack survey: - remove-agent-steering-mirror: the last mirror-of-durable event; zero production listeners; both retention RFCs deferred its fate, and the 'no durable twin' rationale is contradicted by the adjacent append. - tighten-hook-protocol-contract: producer-less 'native' dialect, parsed-and-discarded suppressOutput, and hook/result semantics (truncation + decision-string) defined twice in the bridges instead of the lib that owns the event. - single-source-acp-replay-config: cordis.yml/cordis.snapshot.yml differ by exactly one plugin entry, with no gate on the forced symmetry.
4.9 KiB
RFC: Tighten the hook-protocol contract — the native dialect, suppressOutput, and lib-owned hook/result semantics
Status: proposed
Problem
Three pieces of the freshly-landed dsh-hook-protocol contract miss the discipline the hooks stack itself applied elsewhere (its review dropped a subagent/end agentType field for lacking a consumer — the same test these fail):
HookDialect's'native'variant (packages/hooks/hook-protocol/src/types.ts) has zero producers — the bridges stamp'claude'and'codex'; the only'native'constructor anywhere is the lib's own unit test. The field's own JSDoc definesdialectas "the bridge that ran it", and native is not a bridge: the interception-seams RFC records that native hooks are not a package and that "a native plugin can already use the typed Decisions" without the durable hook log, and the flagship native-plugin worked example asserts exactly that (nohook/*events at all).HookOutput.suppressOutput(same file) is parsed by the codec and discarded on every path: no bridge branch, no merge fold, no warn, no deferred-list row — uniquely among its parsed-but-unhonored siblings, each of which carries a stated deferral (updatedInput→ a logged warn plus the pre-tool-input-rewrite proposal;systemMessage→ a logged warn plus a README deferred row;continue/stopReason→ aTODO(hook-continue-false)anchor plus the'stop'decision record). Structurally there is nothing to suppress: hook stdout never enters any transcript (context flows only viaadditionalContext; the log records onlydecision/stderrSummary), so a hook author settingsuppressOutput: truegets silent nothing with no warn.- The
hook/resultsemantics live in the bridges, twice, not in the lib that owns the event.summarize()— the 500-character stderr truncation rule — is byte-identical inpackages/hooks/hooks-claude/src/index.tsandpackages/hooks/hooks-codex/src/index.ts, and so is the decision-string ruleoutput.decision ?? (output.continue === false ? 'stop' : 'pass'); yetdsh-hook-protocoldeclareshook/result, documentsstderrSummaryas "truncated" without owning the truncation, and documents the decision values without owning the mapping. If one bridge drifts (a different cap, a different fallback), the shared durable event's semantics fork silently.
Proposal
Narrow HookDialect to 'claude' | 'codex' and fix its JSDoc; retarget the lib's one 'native' test. Drop suppressOutput from HookOutput, the codec's parse lines, its codec-test assertions, and the parsed-superset lists in the lib README and hook-protocol-lib RFC (amended per implemented/AGENTS.md). Move the hook/result semantics into the lib: appendHookResult (or a helper it exposes) derives stderrSummary and the decision string from the HookOutput + exit outcome, and both bridges delete their private copies. Rider: un-export BLOCKING_EXIT_CODE (zero importers; even the codec tests spell the literal 2).
Why not keep them?
The hook-protocol-lib RFC deliberately records "parses the full CC superset", and 'native'/suppressOutput are days old — the strongest counterargument is that this re-litigates fresh decisions. But parsing a field whose value can never influence anything is not protocol faithfulness, it is a reader trap; and a dialect variant that the design's own thesis says will never be stamped is vocabulary without an interpreter — the exact bar the stack's own review enforced when it dropped agentType. Both return trivially with their first real producer (a transcript surface that has hook stdout to suppress; a native-provenance feature that logs hook events). On item 3, the lib RFC chose per-bridge explicitness over a parameterized engine — but that choice governed payload construction and Decision mapping; the semantics of the SHARED durable event are precisely the "primitives where duplication would actually be dangerous" that the same RFC assigns to the lib.
Acceptance criteria
HookDialectis two-valued;rg "'native'"in the hooks packages returns only this RFC's amended references.suppressOutputappears nowhere in source, tests, or parsed-field doc lists.- One definition each of the truncation rule and the decision-string rule, in
dsh-hook-protocol, exercised by both bridges' suites; the hook-matrix snapshot goldens are byte-identical.
Risks
All three changes are invisible on the wire and in the goldens (dialect values emitted today are claude/codex; suppressOutput influences nothing; the folded semantics are the same rules). The cost is touching a week-old package — cheap now, per the pre-release stance, and cheaper than letting two copies of a durable event's semantics age apart.