Merge origin/master into codex/truncated-design

This commit is contained in:
Dudu-0223
2026-07-17 18:21:54 +08:00
1057 changed files with 42961 additions and 18526 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?)`** — the exit-code + structured-stdout codec. Exit `0` → parse JSON stdout (lenient: non-JSON is left for the bridge); exit `2` → blocking error, `stderr` is the block reason (surfaced as `decision: 'block'`); other → non-blocking error. `hookSpecificOutput.permissionDecision` (allow/deny/ask) overrides a legacy top-level `decision`; `additionalContext`/`updatedInput`/`systemMessage`/`continue`/`stopReason` are parsed too. The schemas key the `hookSpecificOutput` block by `hookEventName`, so passing `expectedEventName` (the firing event) DISCARDS a block whose `hookEventName` names a different event — or omits it entirely — its event-scoped fields don't take effect (a `PreToolUse` block on a `Stop` hook is malformed, and so is a discriminator-less block that would otherwise apply to any event), while the event-agnostic top-level fields still apply. Pure and total.
- **`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).
@@ -29,6 +29,11 @@ Declaration-merged into `SessionEventMap` (log-only, like `compact/*` — NOT a
Like every event they must sit inside an open turn. The mid-turn points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn by construction; `SessionStart` gets no `hook/*` record (its injected `context/message` is the durable evidence) — see the hooks RFC.
## Input rewrite is parsed but not honored
## Model Experience
`HookOutput.updatedInput` carries a hook's requested tool-input rewrite (CC `updatedInput`), but the harness does not honor it yet — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite RFC](../../../docs/rfc/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.
Indirectly, through `dsh-hooks-claude` and `dsh-hooks-codex`, which can turn parsed hook output into prompt context, blocked outcomes, or continuation feedback.
## Known Limitations and Deferred Work
- **`HookOutput.updatedInput` is parsed but not honored** — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite RFC](../../../docs/rfc/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.
- **An invalid matcher regex matches nothing, silently** — `matchesMatcher` never throws; surfacing the error needs a diagnostic-returning variant or parse-time validation (`TODO(matcher-diagnostics)`).

View File

@@ -24,11 +24,11 @@
"peerDependencies": {
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"cordis": "^4.0.0-rc.6"
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"cordis": "^4.0.0-rc.6"
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -1,20 +1,7 @@
/**
* 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`).
*
* 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
*/
@@ -58,49 +45,30 @@ 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 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 - the event the hook is firing for; omit to apply a `hookSpecificOutput` block as-is.
* @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 {
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.
@@ -150,13 +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 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).
// A missing or mismatched discriminator cannot affect the firing event.
if (expectedEventName !== undefined && eventName !== expectedEventName) {
return
}

View File

@@ -1,16 +1,7 @@
/**
* 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 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,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 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
*/
@@ -92,12 +83,9 @@ export function appendHookInvoked(session: Session, invocation: HookInvocation):
}
/**
* Append a `hook/result` outcome event to `session` (pairs with a prior
* `hook/invoked`). Owns the durable event's semantics: `decision` is the hook's
* parsed decision, else `'stop'` when it asked to halt (`continue: false`),
* else `'pass'`; `stderrSummary` is the trimmed stderr truncated to
* `record.stderrSummaryMaxChars` characters (omitted when empty); `exitCode`
* is omitted when the hook never ran.
* 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,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.
*
* 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,20 +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, …).
*
* 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)`).
*
* 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
*/
@@ -29,15 +17,14 @@ function isMatchAll(matcher: string | undefined): boolean {
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).
* 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.
* @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,8 @@
/**
* 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.
*
* 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
*/
@@ -76,10 +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). An `allow`'s reason is never an
// objection the model needs, so rank 1 collects none.
// 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,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.
*
* 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
*/
@@ -61,15 +54,10 @@ 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` 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,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 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
*/
@@ -32,15 +24,9 @@ declare module '@deepseek-ai/dsh-session' {
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 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
@@ -134,14 +120,8 @@ export interface HookOutput {
/** The reason/explanation accompanying {@link decision}. */
reason?: string
/**
* The `hookSpecificOutput.hookEventName` discriminator, when the hook emitted
* a `hookSpecificOutput` block. The reference schemas key that block by event,
* so a block whose `hookEventName` names a DIFFERENT event than the one firing
* is malformed: {@link parseHookOutput} DISCARDS its event-scoped fields when
* given the firing event's `expectedEventName` (a hook claiming `PreToolUse`
* output on a `Stop` event does not affect the `Stop`). This field is still
* surfaced even on a mismatch — the record shows what the block claimed. Absent
* when the hook emitted no `hookSpecificOutput`.
* Event discriminator claimed by `hookSpecificOutput`. On mismatch,
* {@link parseHookOutput} preserves this value but discards event-scoped fields.
*/
hookEventName?: string
/** Extra context to inject for the next model request (CC `additionalContext`). */

View File

@@ -26,7 +26,6 @@ function recordingBash(run: (spec: BashExecSpec) => Promise<BashRunResult>): {
...request.signal ? { signal: request.signal } : {},
...request.stdin !== undefined ? { stdin: request.stdin } : {},
...request.env !== undefined ? { env: request.env } : {},
owner: request.owner,
sandboxMode: request.sandboxMode,
}
},