docs: rebalance prose cleanup and add trimming skill
This commit is contained in:
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user