deepseek-harness/packages/hooks/hook-protocol/README.md

45 lines
6.2 KiB
Markdown
Raw Normal View History

feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
# @deepseek-ai/dsh-hook-protocol
2026-07-26 05:03:53 +08:00
English | [中文](README.zh.md)
feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
The **shared core** of the Claude Code / Codex hook wire protocol. NOT a cordis plugin — it registers nothing and injects nothing. It is a **library** of dialect-neutral primitives the two bridge plugins (`@deepseek-ai/dsh-hooks-claude`, `@deepseek-ai/dsh-hooks-codex`) import so neither re-implements the identical halves of the protocol.
Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claude Code hook protocol — the same `hooks.json` matcher-group shape, the same exit-code/stdout output contract, the same command-hook execution model. The genuinely-shared parts live here; each bridge owns only what differs.
## What's shared (here) vs. per-dialect (the bridges)
| Concern | Here (`dsh-hook-protocol`) | The bridge (`dsh-hooks-claude` / `-codex`) |
|---|---|---|
| Matcher validation + test | `matcherDiagnostic(pattern, mode)` for parse-time diagnostics; `matchesMatcher(pattern, query, mode)` for contained runtime matching | picks its `mode` (`claude` = literal-or-regex, `codex` = always regex) and rejects a config group carrying a diagnostic |
feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
| Run a hook | `runHook(bash, hook, opts, now)` — stdin payload + env via `ctx.bash`, decode | builds the per-event stdin **payload** + the dialect's **env** |
| Decode output | `parseHookOutput(exit, stdout, stderr)` → neutral `HookOutput` | maps the neutral `HookOutput` onto a seam-specific typed Decision |
| Merge N hooks | `mergeHookOutputs(outputs)` → most-restrictive `MergedHookOutcome` | — |
refactor(hooks): tighten the hook-protocol contract surface Implement the tighten-hook-protocol-contract RFC (moved to implemented/): - HookDialect narrows to 'claude' | 'codex': the 'native' variant had zero producers (native plugins on the seams write no hook/* provenance), and the dialect is defined as the bridge that ran the hook. - HookOutput.suppressOutput is gone: the codec parsed it and every path discarded it with no warn and no deferral — hook stdout never enters a transcript, so there is nothing to suppress. - hook/result.durationMs is gone: durable timing telemetry with no reader that the snapshot normalizer had to scrub as replay noise. With no duration to measure, runHook loses its injected now clock and the single-field RunHookResult wrapper — it returns the HookOutput directly. The committed hook fixtures had the field stripped mechanically (field-only diff); the stdout goldens never carried it. - The bridges' double-defaulted defaultTimeoutMs config knob is replaced by one reference-default constant, DEFAULT_HOOK_TIMEOUT_MS, exported from the lib's runner and applied inside runHook; per-hook timeoutSec stays the override surface. - The hook/result semantics move into the lib that declares the event: HookResultRecord now carries the decoded HookOutput and appendHookResult derives the decision string (decision ?? stop-on-continue:false ?? pass) and the 500-char stderrSummary truncation; both bridges delete their byte-identical private copies. The snapshot suite passes against the existing goldens, proving the derived values are unchanged. - Rider: BLOCKING_EXIT_CODE is codec-internal again (zero importers). Amend the hook-protocol-lib and hook-snapshot-matrix RFCs to the new facts, update the lib/bridge READMEs and the session.md event tables, and retarget the affected unit tests (including new lib-level coverage of the derivation rules).
2026-07-04 15:44:26 +08:00
| Durable record | `appendHookInvoked` / `appendHookResult` (`hook/*` session events; the result's `decision`/`stderrSummary` derive from the `HookOutput` here) | calls them around each invocation |
| Detached-run quiescence | `createDetachedRuns()` — track fire-and-forget run chains; `drain()` aborts, then awaits them | passes `signal` to each detached `runHook`, registers `drain` as its effect disposer |
feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
## Primitives
- **`matcherDiagnostic(matcher, mode)` / `matchesMatcher(matcher, query, mode)`** — match-all on absent/`''`/`'*'`; `claude` mode treats a pure `[A-Za-z0-9_|]+` pattern as a literal (pipe = exact-match alternation) and anything else as a regex; `codex` mode is always an unanchored regex. Bridge parsers discard matcher fields for events without matcher subjects, then use `matcherDiagnostic` to reject an invalid consumed regex with a stable diagnostic before registering any hooks. The runtime predicate still contains an invalid pattern as a non-match, so a direct library caller cannot throw into the agent loop.
- **`runHook(bash, hook, options, now)`** — require and forward the caller-owned `options.signal`, serialize `options.payload` to the hook's stdin (with a trailing newline iff `options.trailingNewline`), merge `options.env` after the executor's credential scrub (the `dsh-bash` trusted-plugin surface), honor the hook's `timeoutSec` (else `options.defaultTimeoutMs` — the bridge owns the default, its config defaulting to the lib's `DEFAULT_HOOK_TIMEOUT_MS` 10-minute reference), and decode the result (threading `options.expectedEventName` to the codec). Cancellation therefore reaches the executor's process-group kill and join boundary. Never throws: an executor rejection (infra fault) becomes a `HookOutput` with `exitCode: undefined` (a non-blocking error). `now` is injected for testable durations.
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** decodes exit status and structured stdout. Exit 2 blocks with stderr; other failures are non-blocking. A matching hook-specific permission decision overrides the legacy top-level decision; mismatched or missing event discriminators suppress only event-specific fields. Top-level fields remain event-agnostic, and successful non-JSON output is left to the bridge.
feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
- **`mergeHookOutputs(outputs)`** — fold the results of every hook that matched one point: permission precedence **deny > ask > allow**, halt sticky on the first `continue:false`, block reasons joined with `\n\n`, `additionalContext`/`systemMessages` accumulated in order.
- **`createDetachedRuns()`** — quiescence tracking for the emit-shaped points, which run detached (no seam awaits them). The bridge tracks each run chain — the hook run PLUS its continuation — and registers `drain()` as its effect disposer: drain fires the tracker's abort `signal` (so a still-running hook process is killed via `runHook`, not awaited out to its timeout), then resolves once every tracked chain has settled. `fiber.dispose()` resolving therefore means no detached hook work is left to fire into a disposed context ([defensive patterns](../../../docs/defensive-patterns.md): dispose must reach quiescence).
feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
## `hook/*` session events
Declaration-merged into `SessionEventMap` (log-only, like `compact/*` — NOT a `SurfaceEventType`, no `surfaceOp`): `hook/invoked` (a hook command ran) and `hook/result` (its outcome, paired by `handlerId`, with `appendHookResult` owning the decision rule). Payloads and per-event JSDoc are in the generated [persistence log event catalog](../../../docs/persistence-catalog.md); `stderrSummary` is truncated to the record's `stderrSummaryMaxChars` (the bridge's config, reference default `DEFAULT_STDERR_SUMMARY_MAX_CHARS` = 500; omitted when empty).
feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
Hook provenance records must sit inside an open turn. `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` satisfy that owner-defined relation by construction. `SessionStart` runs before turn 1 and gets no `hook/*` record; its allowed context remains pending in the inbox until a waking delivery opens a turn — see the hooks Agent Note.
feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
2026-07-12 02:55:26 +08:00
## Model Experience
feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
Indirectly, through `dsh-hooks-claude` and `dsh-hooks-codex`, which can turn parsed hook output into prompt context, blocked outcomes, or continuation feedback.
2026-07-12 02:55:26 +08:00
#### KV Cache effect
No direct invalidation; the named consumer owns any request-prefix changes.
Add a gated Known Limitations and Deferred Work section to every package README Every packages/*/* README now carries a canonical '## Known Limitations and Deferred Work' section: condensed, evidence-backed bullets for consumer-visible gaps (unimplemented features, platform caveats, MVP cuts) and consciously postponed work (TODO/FIXME/XXX markers, RFC deferrals still open). The ten pre-existing ad-hoc variants ('What is NOT here (TODO)', 'Deferred', 'Limitations (MVP)', 'Known limitations (tracked TODOs)', ...) are normalized into the canonical heading. A new doc-sync gate, scripts/verify-readme-limitations.ts, enforces the shape: exactly one limitations-like heading per package README, byte-equal to the canonical h2, with at least one bullet; near-miss headings fail so variants cannot creep back. Packages with genuinely nothing to declare (dsh-brand, dsh-timeout, dsh-subagent-mock, dsh-app-boot) are whitelisted in the script and must NOT carry the section; whitelist entries are validated against the scanned package set so a rename fails loud. Wired into the doc-sync chain (package.json) and the run-gates doc-sync leaf set; the standing rule lands in packages/AGENTS.md and the adding-a-package cookbook; decision record in docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md (RFC index regenerated). Also fixes two stale '(deferred)' markers claiming dsh-compact-basic is unimplemented (the dsh-compact seam README's package table and the seam's module doc comment).
2026-07-10 01:51:50 +08:00
## Known Limitations and Deferred Work
feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook model). This library holds the genuinely-identical primitives; each bridge owns only what differs (per-event stdin payload, env/substitution, decision mapping). New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects nothing): - matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to a mode param (claude = literal-or-regex with pipe alternation; codex = always unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing. - codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0 → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as decision:'block'); other → non-blocking. Parses the CC superset (continue/stopReason/decision/hookSpecificOutput.{permissionDecision, additionalContext,updatedInput}/systemMessage); permissionDecision overrides the legacy top-level decision. - runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin payload + trusted-plugin env), honors timeoutSec, never throws (executor reject → non-blocking-error HookOutput). Injected clock for testable durations. - merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop, block reasons joined, context/system-messages accumulated). - hook/* session events (declaration-merged into SessionEventMap, log-only like compact/*) + appendHookInvoked/appendHookResult helpers. updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/* helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:38:06 +08:00
2026-07-19 22:50:49 +08:00
- **`HookOutput.updatedInput` is parsed but not honored** — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)); a bridge logs + warns when a hook sets it. See `src/types.ts` for the full contracts.