docs: trim generated prose

This commit is contained in:
Tianyi Cui
2026-07-12 03:36:43 +08:00
parent 3dca90261c
commit 75838e10b5
323 changed files with 2857 additions and 11833 deletions

View File

@@ -1,20 +1,6 @@
/**
* Parse a finished hook command's process outcome (exit code + stdout + stderr)
* into the dialect-neutral {@link HookOutput} both bridges map from.
*
* The exit-code contract is shared by Claude Code and Codex:
* - exit 0 → success; if stdout is structured JSON, parse it; else the plain
* stdout is available to the bridge (some events treat it as `additionalContext`).
* - exit 2 → BLOCKING error; stderr is the block reason fed back to the model.
* We surface this as `decision: 'block'` with `reason = stderr` so a bridge
* needs no separate exit-code branch — the neutral output already says "block".
* - other → non-blocking error; recorded (exitCode + stderr) but no decision.
*
* Structured-stdout fields are a SUPERSET across dialects (CC is richest); we
* parse every field we recognize and leave it to the bridge to honor only the
* subset meaningful for its dialect/hook point (Codex, e.g., ignores
* `allow`/`ask`/`updatedInput`).
*
* Parse a finished hook command's process outcome (exit code + stdout + stderr) into the
* dialect-neutral {@link HookOutput} both bridges map from.
* @module @deepseek-ai/dsh-hook-protocol/codec
*/
@@ -58,49 +44,26 @@ function permissionDecisionOf(value: string | undefined): HookOutput['decision']
}
/**
* Parse one finished hook command into a {@link HookOutput}. `stdout`/`stderr`
* are the captured streams; `exitCode` is the process exit (`undefined` when the
* hook could not be spawned at all). Pure and total — never throws; malformed
* JSON on a 0 exit is treated as "no structured output" (the plain stdout is
* still on the bridge to use), matching both reference engines' lenient parse of
* non-JSON stdout.
*
* `expectedEventName` is the event the hook is FIRING for (e.g. `'PreToolUse'`).
* The reference schemas key the `hookSpecificOutput` block by `hookEventName`,
* so a block whose `hookEventName` names a DIFFERENT event is malformed and its
* event-scoped fields (`permissionDecision`/`permissionDecisionReason`/
* `additionalContext`/`updatedInput`) are DISCARDED — a `PreToolUse` block on a
* `Stop` hook must not deny the `Stop`. The block's `hookEventName` is still
* surfaced (for the log/diagnostics), and the event-agnostic top-level fields
* (`decision`/`reason`/`continue`/`stopReason`/`systemMessage`)
* are unaffected. Omit `expectedEventName` (or pass a matching one) to apply the
* block as-is — a caller that doesn't key by event opts out of the check.
*
* @param exitCode - the process exit code; `undefined` when the hook could not be spawned at all.
* @param stdout - the captured stdout stream; consulted for structured JSON only on a 0 exit.
* Decode process output into the dialect-neutral hook outcome.
* @param exitCode - process exit, or `undefined` when spawn failed.
* @param stdout - output parsed as structured JSON only on exit 0.
* @param stderr - the captured stderr stream; becomes the blocking `reason` on exit 2.
* @param expectedEventName - the event the hook is firing for; omit to apply a `hookSpecificOutput` block as-is.
* @param expectedEventName - optional event guard for hook-specific output.
* @returns the dialect-neutral decoded outcome.
*/
export function parseHookOutput(exitCode: number | undefined, stdout: string, stderr: string, expectedEventName?: string): HookOutput {
const trimmedErr = stderr.trim()
const trimmedOut = stdout.trim()
// Keep the raw stdout verbatim: a clean-exit hook may emit PLAIN text the
// protocol renders/uses (CC output; Codex SessionStart/UserPromptSubmit
// additionalContext), so the bridge needs it even when there's no JSON.
// Plain stdout remains available even when it is not JSON.
const output: HookOutput = { exitCode, stderr: trimmedErr, stdout: trimmedOut }
// Exit 2 is a blocking error in both dialects: stderr is the reason. Surface
// it as a `block` decision so the bridge maps it uniformly with a structured
// `decision:'block'` — the exit code and the JSON channel converge here.
// Both dialects treat exit 2 as a block with stderr as its reason.
if (exitCode === BLOCKING_EXIT_CODE) {
output.decision = 'block'
if (trimmedErr.length > 0) output.reason = trimmedErr
}
// Structured stdout is only consulted on a clean (0) exit; on a blocking exit
// the stderr channel is authoritative. A non-zero/undefined exit other than 2
// carries no decision (the bridge records it as a non-blocking error).
// Structured stdout is valid only for a clean exit.
if (exitCode === 0) {
// Only attempt JSON when stdout looks like a JSON object — matches the
// reference engines, which treat other stdout as plain text, not an error.
@@ -151,12 +114,7 @@ function applyStructured(output: HookOutput, parsed: Record<string, unknown>, ex
// mismatch — the record should show what the malformed block claimed.
if (eventName !== undefined) output.hookEventName = eventName
// The schemas key this block by event: when a caller passes the firing event
// (`expectedEventName`), the block's `hookEventName` MUST name it. A different
// name — or a MISSING one — is malformed under the keyed schema, so discard the
// event-scoped fields (a PreToolUse block must not deny a Stop hook; nor may a
// discriminator-less block silently apply PreToolUse-scoped permission fields to
// whatever event is firing). A caller that passes no expectedEventName opts out
// of the check (applies the block as-is).
// (`expectedEventName`), the block's `hookEventName` must name it.
if (expectedEventName !== undefined && eventName !== expectedEventName) {
return
}

View File

@@ -1,16 +1,5 @@
/**
* Quiescence tracking for a bridge's DETACHED hook runs. The waterfall-shaped
* hook points (`UserPromptSubmit`, `PreToolUse`, …) are awaited by their seams,
* but the emit-shaped points (`SessionStart`, `SubagentStart`, `SubagentStop`)
* run fire-and-forget: no seam awaits them, so without tracking a bridge's
* disposal could strand a live hook process and let a late continuation fire
* into a disposed context (docs/defensive-patterns.md: dispose must reach
* quiescence). A bridge creates one tracker in `apply()`, passes
* {@link DetachedRuns.signal} to each detached {@link runHook} call, wraps the
* full run chain (the hook run PLUS its `.then` continuation) in
* {@link DetachedRuns.track}, and registers {@link DetachedRuns.drain} as its
* disposer.
*
* Quiescence tracking for a bridge's DETACHED hook runs.
* @module @deepseek-ai/dsh-hook-protocol/detached
*/

View File

@@ -1,17 +1,8 @@
/**
* Append helpers for the log-only `hook/*` session events — the durable record
* that a hook ran and what it decided. Thin wrappers over `session.append` so a
* bridge does not hand-build the payloads (and so the `turn`-enclosure +
* invoked/result pairing stay consistent across both bridges).
*
* `hook/*` events are log-only (not {@link SurfaceEventType}), so they carry no
* `surfaceOp` and append with no surface intent — but, like every event, they
* must sit inside an OPEN turn (the invariants oracle rejects an un-enclosed
* event). The mid-turn hook points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/
* `Stop`) fire inside the loop's open turn by construction; `SessionStart` is the
* exception (its injected `context/message` is the durable evidence instead), so
* a bridge does NOT write `hook/*` for session-start — see the hooks RFC.
*
* Append helpers for the log-only `hook/*` session events — the durable record that a hook ran
* and what it decided. Thin wrappers over `session.append` so a bridge does not hand-build the
* payloads (and so the `turn`-enclosure + invoked/result pairing stay consistent across both
* bridges).
* @module @deepseek-ai/dsh-hook-protocol/events
*/

View File

@@ -1,27 +1,8 @@
/**
* `@deepseek-ai/dsh-hook-protocol` — 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
* (`dsh-hooks-claude`, `dsh-hooks-codex`) import to avoid re-implementing the
* identical halves of the protocol:
*
* - {@link matchesMatcher} — the matcher primitive (literal-or-regex by dialect).
* - {@link runHook} + {@link parseHookOutput} — run a command hook via `ctx.bash`
* (stdin payload + env) and decode its exit-code/stdout/stderr into a neutral
* {@link HookOutput}.
* - {@link mergeHookOutputs} — fold multiple matched hooks into one
* most-restrictive {@link MergedHookOutcome} (deny > ask > allow).
* - {@link appendHookInvoked} / {@link appendHookResult} — the log-only `hook/*`
* session-event helpers (declaration-merged into `SessionEventMap`);
* `appendHookResult` derives the durable `decision`/`stderrSummary` from the
* {@link HookOutput} so the shared event's semantics live in one place.
* - {@link createDetachedRuns} — quiescence tracking for the fire-and-forget
* hook points: disposal aborts and drains a bridge's detached runs.
*
* Each bridge owns what genuinely DIFFERS: building the per-event stdin payload
* (CC vs Codex field sets), the dialect's env/substitution, and mapping the
* neutral outcome onto the harness's seam-specific typed Decisions.
*
* `@deepseek-ai/dsh-hook-protocol` — 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 (`dsh-hooks-claude`, `dsh-hooks-codex`)
* import to avoid re-implementing the identical halves of the protocol.
* @module @deepseek-ai/dsh-hook-protocol
*/

View File

@@ -1,20 +1,6 @@
/**
* The matcher primitive shared by both hook dialects: decide whether a matcher
* pattern selects a given query (a tool name, a session source, …).
*
* The two dialects differ ONLY in how a non-empty pattern is interpreted, so
* that single axis is the {@link MatcherMode} parameter:
* - `claude`: a pattern of purely `[A-Za-z0-9_|]+` is a LITERAL (pipe =
* exact-match alternation, e.g. `Edit|Write`); anything else is a regex.
* - `codex`: every pattern is an unanchored regex (no literal fast path).
*
* Both treat an absent / empty / `'*'` pattern as match-all, and both treat an
* invalid regex as a non-match: a broken matcher selects nothing rather than
* throwing into the loop. This is SILENT — the boolean return cannot distinguish
* "did not match" from "failed to compile", so a typo'd pattern (e.g. `[`)
* quietly disables that matcher with no warning. Surfacing bad config would need
* a diagnostic-returning variant or parse-time validation (`TODO(matcher-diagnostics)`).
*
* The matcher primitive shared by both hook dialects: decide whether a matcher pattern selects
* a given query (a tool name, a session source, …).
* @module @deepseek-ai/dsh-hook-protocol/matcher
*/
@@ -30,14 +16,12 @@ const CLAUDE_LITERAL = /^[A-Za-z0-9_|]+$/
/**
* Whether `matcher` selects `query` under the given dialect {@link MatcherMode}.
* Match-all sentinels (absent/`''`/`'*'`) always match. A `claude` literal
* pattern exact-matches the query (splitting `|` into alternatives); every other
* `claude` pattern and ALL `codex` patterns are tested as an unanchored regex.
* An invalid regex matches nothing (never throws).
*
* @param matcher - the configured pattern; absent/empty/`'*'` are the match-all sentinels.
* @param query - the candidate value (a tool name, a session source, …).
* @param mode - the dialect deciding literal-vs-regex interpretation of the pattern.
* @returns `true` when the pattern selects the query; `false` on a non-match or an invalid regex.
* @returns `true` when the pattern selects the query; `false` on a non-match or an invalid
* regex.
*/
export function matchesMatcher(matcher: string | undefined, query: string, mode: MatcherMode): boolean {
if (isMatchAll(matcher)) return true

View File

@@ -1,23 +1,6 @@
/**
* Merge the outcomes of MULTIPLE hooks that matched one hook point into a single
* most-restrictive {@link MergedHookOutcome}. Both reference engines run matched
* hooks concurrently and fold their results; the precedence rules here are the
* intersection both dialects agree on (and the strictest interpretation where
* they differ), so a bridge gets one decision to map onto its seam:
*
* - **permission precedence `deny > ask > allow`**: any `deny`/`block` wins; an
* `ask` overrides `allow`; `allow`/`approve` only stands if nothing stricter
* appeared. (Claude Code's explicit precedence; Codex only ever blocks, so the
* rule degenerates correctly for it.)
* - **halt is sticky**: the first hook with `continue:false` sets `stop` and its
* `stopReason`.
* - **reasons accumulate**: block/deny reasons are joined with `\n\n` (Codex's
* `join_text_chunks`), so the model sees every objection, not just the first.
* - **context accumulates**: `additionalContext` from every hook is collected in
* order (CC concatenates; Codex keeps them as separate developer messages —
* either way the bridge gets the ordered list).
* - **systemMessages accumulate** likewise.
*
* most-restrictive {@link MergedHookOutcome}.
* @module @deepseek-ai/dsh-hook-protocol/merge
*/
@@ -76,10 +59,9 @@ function decisionForRank(maxRank: number): MergedDecision {
*/
export function mergeHookOutputs(outputs: HookOutput[]): MergedHookOutcome {
let maxRank = 0
// Reasons collected PER RANK, so the merged reason can be the one explaining
// the WINNING decision (a deny-winning outcome surfaces deny reasons; an
// ask-winning outcome surfaces ask reasons). An `allow`'s reason is never an
// objection the model needs, so rank 1 collects none.
// Reasons collected per RANK, so the merged reason can be the one explaining the WINNING
// decision (a deny-winning outcome surfaces deny reasons; an ask-winning outcome surfaces ask
// reasons).
const reasonsByRank = new Map<number, string[]>()
let stop = false
let stopReason: string | undefined

View File

@@ -1,15 +1,8 @@
/**
* Run one configured command hook through the `ctx.bash` executor seam and parse
* its outcome into a {@link HookOutput}. This is where the wire protocol's
* EXECUTION half lives: feed the hook its JSON payload on stdin, hand it the
* dialect's env vars, honor its timeout, capture stdout/stderr/exit, and decode.
*
* It runs hooks through `ctx.bash` (not a bespoke `spawn`) deliberately — the
* bash seam already provides the scrubbed-but-overridable env, process-group
* kills, and timeout the protocol needs, and `dsh-bash`'s `stdin`/`env` fields
* are the trusted-plugin surface (added for exactly this) that a hook bridge —
* an in-process plugin, not model output — is allowed to use.
*
* Run one configured command hook through the `ctx.bash` executor seam and parse its outcome
* into a {@link HookOutput}. This is where the wire protocol's EXECUTION half lives: feed the
* hook its JSON payload on stdin, hand it the dialect's env vars, honor its timeout, capture
* stdout/stderr/exit, and decode.
* @module @deepseek-ai/dsh-hook-protocol/runner
*/
@@ -61,15 +54,9 @@ export interface RunHookResult {
}
/**
* Run `hook` via `bash` with `options.payload` serialized to its stdin, then
* decode the result into a {@link HookOutput}. The hook's configured
* `timeoutSec` (wire unit: seconds) overrides `options.defaultTimeoutMs`.
* The command runs with the dialect's `env` merged after the executor's
* credential scrub (the trusted-plugin path). NEVER throws: an infrastructure
* failure (the executor rejecting) is surfaced as a {@link HookOutput} with
* `exitCode: undefined`, so the caller's merge logic treats it as a
* non-blocking error rather than crashing the turn. `now` is injected for
* testable durations.
* Run `hook` via `bash` with `options.payload` serialized to its stdin, then decode the result
* into a {@link HookOutput}.
*
* @param bash - the executor seam the command runs through.
* @param hook - the configured command; its `timeoutSec` (wire unit: seconds) overrides the default timeout.
* @param options - the invocation's payload, env, cwd, signal, stdin framing, and default timeout.

View File

@@ -1,15 +1,7 @@
/**
* Dialect-neutral vocabulary for the Claude Code / Codex hook wire protocol,
* plus the log-only `hook/*` session events. Types only — runtime helpers live
* in the sibling modules (`matcher`, `codec`, `runner`, `merge`, `events`).
*
* This package is the SHARED CORE: the truly-identical primitives both the
* `dsh-hooks-claude` and `dsh-hooks-codex` bridges build on. Each bridge owns
* its own per-dialect stdin-payload construction and decision mapping on top of
* these primitives — the divergences (which events exist, literal-vs-regex
* matching, env/substitution, snake_case extras, allow/ask support) are the
* BRIDGE's concern, not this lib's.
*
* Dialect-neutral vocabulary for the Claude Code / Codex hook wire protocol, plus the log-only
* `hook/*` session events. Types only — runtime helpers live in the sibling modules
* (`matcher`, `codec`, `runner`, `merge`, `events`).
* @module @deepseek-ai/dsh-hook-protocol/types
*/
@@ -31,17 +23,7 @@ declare module '@deepseek-ai/dsh-session' {
matcher?: string
handlerId: string
}
/**
* A hook command's outcome — log-only, paired with a prior `hook/invoked`
* (same `handlerId`). `decision` is the dialect-neutral outcome derived by
* `appendHookResult` (which owns the rule): the hook's parsed decision
* (`approve`/`allow`/`block`/`deny`/`ask`), else `'stop'` when it asked to
* halt via `continue:false`, else `'pass'`. `exitCode` is the process exit
* (absent if it never ran), `stderrSummary` the trimmed stderr truncated to
* the bridge's configured cap (the block reason source on exit 2),
* `durationMs` the wall-clock runtime (audit timing; snapshot replay
* normalizes it). `turn` matches the `hook/invoked`.
*/
/** Log-only hook outcome paired to `hook/invoked` by `handlerId`. */
'hook/result': {
turn: number
point: string