docs: rebalance prose cleanup and add trimming skill

This commit is contained in:
Tianyi Cui
2026-07-13 23:27:00 +08:00
parent fcdc318dda
commit 148046b9c8
392 changed files with 2801 additions and 1754 deletions

View File

@@ -19,7 +19,7 @@ Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claud
- **`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. An invalid regex matches nothing (never throws).
- **`runHook(bash, hook, options, now)`** — 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). 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. Event-specific output applies only when its discriminator matches the firing event, while top-level fields remain event-agnostic. The parser is total and leaves successful non-JSON output to the bridge.
- **`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.
- **`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).

View File

@@ -1,6 +1,7 @@
/**
* Parse a finished hook command's process outcome (exit code + stdout + stderr) into the
* dialect-neutral {@link HookOutput} both bridges map from.
* Decode hook process outcomes for both dialects. Exit 0 may carry structured
* JSON or plain stdout; exit 2 blocks with stderr as the reason; every other
* exit is a non-blocking error. Bridges decide which recognized fields apply.
* @module @deepseek-ai/dsh-hook-protocol/codec
*/
@@ -44,11 +45,15 @@ function permissionDecisionOf(value: string | undefined): HookOutput['decision']
}
/**
* Decode process output into the dialect-neutral hook outcome.
* Decode process output into a dialect-neutral hook outcome. This function is
* total: malformed JSON remains plain stdout. When `expectedEventName` is set,
* a missing or different `hookSpecificOutput.hookEventName` discards only its
* event-scoped fields; top-level fields and the claimed discriminator remain.
* Omitting the guard applies the block as-is.
* @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 - optional event guard for hook-specific output.
* @param expectedEventName - firing event used to guard hook-specific fields; omit to disable the guard.
* @returns the dialect-neutral decoded outcome.
*/
export function parseHookOutput(exitCode: number | undefined, stdout: string, stderr: string, expectedEventName?: string): HookOutput {
@@ -113,8 +118,7 @@ function applyStructured(output: HookOutput, parsed: Record<string, unknown>, ex
// Always surface the discriminator (for the log/diagnostics), even on a
// 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 missing or mismatched discriminator cannot affect the firing event.
if (expectedEventName !== undefined && eventName !== expectedEventName) {
return
}

View File

@@ -1,5 +1,7 @@
/**
* Quiescence tracking for a bridge's DETACHED hook runs.
* Quiescence tracking for emit-shaped hook runs that no seam awaits. Bridges
* track the run plus its continuation, pass the tracker signal into execution,
* and drain on disposal so no process or late callback outlives the fiber.
* @module @deepseek-ai/dsh-hook-protocol/detached
*/

View File

@@ -1,8 +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).
* Append helpers for durable, log-only hook events. They carry no surface
* intent and must remain turn-enclosed and invoked/result paired. Mid-turn hook
* points satisfy that boundary; SessionStart records injected context instead
* and does not append `hook/*` outside a turn.
* @module @deepseek-ai/dsh-hook-protocol/events
*/
@@ -83,8 +83,9 @@ export function appendHookInvoked(session: Session, invocation: HookInvocation):
}
/**
* Append the durable result paired with `hook/invoked`, normalizing its decision,
* bounded stderr summary, and optional exit code.
* Append the durable result paired with `hook/invoked`. The recorded decision
* is the parsed decision, then `stop` for `continue:false`, else `pass`; stderr
* is trimmed and capped, and an absent process exit stays omitted.
* @param session - the session whose open turn records the event.
* @param record - the outcome to record: the decoded output plus the summary cap and duration.
*/

View File

@@ -1,8 +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.
* Shared, non-plugin hook protocol library: matching, command execution and
* decoding, restrictive outcome merging, durable event helpers, and detached
* run quiescence. Claude Code and Codex bridges own their distinct payloads,
* environment rules, matcher mode, and typed seam mappings.
* @module @deepseek-ai/dsh-hook-protocol
*/

View File

@@ -1,6 +1,8 @@
/**
* The matcher primitive shared by both hook dialects: decide whether a matcher pattern selects
* a given query (a tool name, a session source, …).
* Matcher shared by both hook dialects. Claude treats alphanumeric/underscore/
* pipe patterns as literal alternatives and other patterns as regex; Codex
* treats every non-empty pattern as an unanchored regex. Missing, empty, and
* `*` match all; invalid regexes silently match nothing.
* @module @deepseek-ai/dsh-hook-protocol/matcher
*/
@@ -15,8 +17,9 @@ function isMatchAll(matcher: string | undefined): boolean {
const CLAUDE_LITERAL = /^[A-Za-z0-9_|]+$/
/**
* Whether `matcher` selects `query` under the given dialect {@link MatcherMode}.
*
* Whether `matcher` selects `query` under the given dialect. Claude literal
* patterns exact-match pipe-separated alternatives; all other patterns are
* unanchored regexes. Invalid regexes return `false` rather than throwing.
* @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.

View File

@@ -1,6 +1,8 @@
/**
* Merge the outcomes of MULTIPLE hooks that matched one hook point into a single
* most-restrictive {@link MergedHookOutcome}.
* Merge matched hooks into one most-restrictive outcome. Permission precedence
* is `deny > ask > allow`; the first `continue:false` stop is sticky; reasons
* for the winning rank are joined; and context and system messages accumulate
* in hook order.
* @module @deepseek-ai/dsh-hook-protocol/merge
*/
@@ -59,9 +61,7 @@ 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).
// Keep reasons per rank so only objections explaining the winning decision surface.
const reasonsByRank = new Map<number, string[]>()
let stop = false
let stopReason: string | undefined

View File

@@ -1,8 +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.
* Execute command hooks through `ctx.bash`, using its credential scrub,
* process-group cancellation, and timeout machinery. The bridge supplies the
* trusted stdin payload and dialect environment, then this module decodes the
* captured outcome.
* @module @deepseek-ai/dsh-hook-protocol/runner
*/
@@ -54,9 +54,10 @@ export interface RunHookResult {
}
/**
* Run `hook` via `bash` with `options.payload` serialized to its stdin, then decode the result
* into a {@link HookOutput}.
*
* Run `hook` with serialized stdin and decode its outcome. A hook-specific
* timeout in seconds overrides the default; trusted environment entries merge
* after the executor scrub. Infrastructure rejection becomes an outcome with
* no exit code, so this function never throws or crashes the calling turn.
* @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,7 +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`).
* Dialect-neutral vocabulary and log-only events shared by the Claude Code and
* Codex hook bridges. Payload construction, matching differences, environment,
* and seam-specific decision mapping remain owned by each bridge.
* @module @deepseek-ai/dsh-hook-protocol/types
*/
@@ -23,7 +23,11 @@ declare module '@deepseek-ai/dsh-session' {
matcher?: string
handlerId: string
}
/** Log-only hook outcome paired to `hook/invoked` by `handlerId`. */
/**
* Log-only outcome paired to `hook/invoked` by `handlerId`. Decision is the
* parsed permission result, `stop` for `continue:false`, or `pass`; exit code
* may be absent, stderr is bounded, and duration is wall-clock runtime.
*/
'hook/result': {
turn: number
point: string