docs: reserve seam for complete capabilities
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/hooks/hook-protocol/README.md
|
||||
README.md: 797d15c357bd26e0db072b30b61c251005021905
|
||||
README.zh.md: 8a96cd2f1f91ad2d7a12fc15ffc06ea0c2c1d80f
|
||||
README.md: 230f14c2bc053555991e5fb96e2753aef5bbff8a
|
||||
README.zh.md: d83ea99065123c9cf0f85c0abbbeeff6eadb17bc
|
||||
|
||||
@@ -12,7 +12,7 @@ Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claud
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Decode output | `parseHookOutput(exit, stdout, stderr)` → neutral `HookOutput` | maps the neutral `HookOutput` onto an extension-point-specific typed Decision |
|
||||
| Merge N hooks | `mergeHookOutputs(outputs)` → most-restrictive `MergedHookOutcome` | — |
|
||||
| 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 |
|
||||
@@ -23,7 +23,7 @@ Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claud
|
||||
- **`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.
|
||||
- **`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).
|
||||
- **`createDetachedRuns()`** — quiescence tracking for the emit-shaped points, which run detached (no extension point 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).
|
||||
|
||||
## `hook/*` session events
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Claude Code/Codex hook 协议格式(wire format)的**共享核心**。它
|
||||
|---|---|---|
|
||||
| Matcher 校验 + 测试 | `matcherDiagnostic(pattern, mode)` 用于解析时诊断;`matchesMatcher(pattern, query, mode)` 用于隔离的运行时匹配 | 选择自身的 `mode`(`claude` = 字面量或正则,`codex` = 始终使用正则),并拒绝带有诊断的配置组 |
|
||||
| 运行 hook | `runHook(bash, hook, opts, now)`:通过 `ctx.bash` 提供 stdin payload + env,再解码 | 构造每个事件的 stdin **payload** + 该方言的 **env** |
|
||||
| 解码输出 | `parseHookOutput(exit, stdout, stderr)` → 中性 `HookOutput` | 将中性 `HookOutput` 映射到 seam 特定的类型化 Decision |
|
||||
| 解码输出 | `parseHookOutput(exit, stdout, stderr)` → 中性 `HookOutput` | 将中性 `HookOutput` 映射到扩展点特定的类型化 Decision |
|
||||
| 合并 N 个 hook | `mergeHookOutputs(outputs)` → 最严格的 `MergedHookOutcome` | (无) |
|
||||
| 持久记录 | `appendHookInvoked` / `appendHookResult`(`hook/*` 会话事件;结果的 `decision`/`stderrSummary` 从此处的 `HookOutput` 派生) | 在每次调用前后调用它们 |
|
||||
| 脱离运行完全停稳 | `createDetachedRuns()`:跟踪触发后不等待的运行链;`drain()` 先 abort,再等待它们 | 将 `signal` 传给每个脱离的 `runHook`,并将 `drain` 注册为 effect disposer |
|
||||
@@ -23,7 +23,7 @@ Claude Code/Codex hook 协议格式(wire format)的**共享核心**。它
|
||||
- **`runHook(bash, hook, options, now)`**:要求并转发调用方拥有的 `options.signal`,将 `options.payload` 序列化到 hook stdin(当且仅当 `options.trailingNewline` 时添加尾随换行符),在执行器凭证清理后合并 `options.env`(`dsh-bash` 受信任插件接口),遵循 hook 的 `timeoutSec`(否则使用 `options.defaultTimeoutMs`;默认值属于桥接,其配置默认为 lib 的 `DEFAULT_HOOK_TIMEOUT_MS` 10 分钟参考值),再解码结果(将 `options.expectedEventName` 传递给 codec)。因此取消会到达执行器的进程组终止与 join 边界。它绝不抛出异常:执行器拒绝(基础设施故障)会变为 `HookOutput`,其 `exitCode: undefined`(非阻塞错误)。`now` 会被注入,以便测试持续时间。
|
||||
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** 解码退出状态与结构化 stdout。退出码为 2 时,会以 stderr 内容阻止执行;其他失败不阻塞。匹配的 hook 特定权限决策会覆盖遗留顶层决策;事件判别字段不匹配或缺失只会抑制事件特定字段。顶层字段仍与事件无关,成功但非 JSON 的输出会留给桥接处理。
|
||||
- **`mergeHookOutputs(outputs)`**:折叠在一个点上匹配的每个 hook 结果:权限优先级为 **deny > ask > allow**,从首个 `continue:false` 起,halt 状态保持不变,阻塞原因用 `\n\n` 连接,`additionalContext`/`systemMessages` 按顺序累积。
|
||||
- **`createDetachedRuns()`**:跟踪以 emit 形式脱离运行的点是否完全停稳(没有 seam 等待它们)。桥接会跟踪每条运行链,包括 hook 运行及其 continuation,并将 `drain()` 注册为 effect disposer。drain 会触发 tracker 的 abort `signal`(因此仍在运行的 hook 进程会通过 `runHook` 终止,而不是等待到超时),随后在所有已跟踪链结算后 resolve。因此 `fiber.dispose()` resolve 时,不会遗留任何可能作用于已 dispose(资源释放)的上下文的脱离 hook 工作(见 [防御模式](../../../docs/defensive-patterns.md):dispose 必须达到完全停稳)。
|
||||
- **`createDetachedRuns()`**:跟踪以 emit 形式脱离运行的点是否完全停稳(没有扩展点等待它们)。桥接会跟踪每条运行链,包括 hook 运行及其 continuation,并将 `drain()` 注册为 effect disposer。drain 会触发 tracker 的 abort `signal`(因此仍在运行的 hook 进程会通过 `runHook` 终止,而不是等待到超时),随后在所有已跟踪链结算后 resolve。因此 `fiber.dispose()` resolve 时,不会遗留任何可能作用于已 dispose(资源释放)的上下文的脱离 hook 工作(见 [防御模式](../../../docs/defensive-patterns.md):dispose 必须达到完全停稳)。
|
||||
|
||||
## `hook/*` 会话事件
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Quiescence tracking for emit-shaped hook runs that no seam awaits. Bridges
|
||||
* Quiescence tracking for emit-shaped hook runs that no extension point 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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 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.
|
||||
* environment rules, matcher mode, and typed extension-point mappings.
|
||||
* @module @deepseek-ai/dsh-hook-protocol
|
||||
*/
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ function decisionForRank(maxRank: number): MergedDecision {
|
||||
* yields a neutral outcome (`decision: 'none'`, no stop, empty context) — the
|
||||
* caller treats that as "no hook had anything to say".
|
||||
* @param outputs - every matched hook's decoded output, in hook order.
|
||||
* @returns the single folded outcome the bridge maps onto its seam.
|
||||
* @returns the single folded outcome the bridge maps onto its extension point.
|
||||
*/
|
||||
export function mergeHookOutputs(outputs: HookOutput[]): MergedHookOutcome {
|
||||
let maxRank = 0
|
||||
|
||||
@@ -58,7 +58,7 @@ export interface RunHookResult {
|
||||
* 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 bash - The executor service 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.
|
||||
* @param now - millisecond clock used for the reported duration.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 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.
|
||||
* and extension-point-specific decision mapping remain owned by each bridge.
|
||||
* @module @deepseek-ai/dsh-hook-protocol/types
|
||||
*/
|
||||
|
||||
@@ -42,8 +42,8 @@ declare module '@deepseek-ai/dsh-session' {
|
||||
|
||||
/**
|
||||
* The bridge that ran a hook — the CC bridge stamps `'claude'`, the Codex
|
||||
* bridge `'codex'`. A native plugin on the interception seams is not a bridge
|
||||
* and writes no `hook/*` invocation/result records (see the interception-seams Agent Note).
|
||||
* bridge `'codex'`. A native plugin at the interception points is not a bridge
|
||||
* and writes no `hook/*` invocation/result records (see the interception extension-points Agent Note).
|
||||
*/
|
||||
export type HookDialect = 'claude' | 'codex'
|
||||
|
||||
@@ -81,7 +81,7 @@ export type MatcherMode = 'claude' | 'codex'
|
||||
/**
|
||||
* The dialect-neutral OUTCOME a hook produced, parsed from its exit code +
|
||||
* stdout JSON + stderr by {@link parseHookOutput}. A bridge maps this onto a
|
||||
* seam-specific typed Decision (PreToolDecision, PreStepDecision, …). Every field
|
||||
* extension-point-specific typed Decision (PreToolDecision, PreStepDecision, …). Every field
|
||||
* is OPTIONAL because a hook may exercise any subset; the bridge decides which
|
||||
* fields are meaningful for its hook point and which it ignores (faithful-but-
|
||||
* degraded — e.g. Codex ignores `allow`/`ask`).
|
||||
@@ -130,7 +130,7 @@ export interface HookOutput {
|
||||
systemMessage?: string
|
||||
/**
|
||||
* A tool-input rewrite a hook requested (CC `updatedInput`). PARSED but NOT
|
||||
* honored — input rewrite is deferred (see the interception-seams Agent Note); a
|
||||
* honored — input rewrite is deferred (see the interception extension-points Agent Note); a
|
||||
* bridge logs + warns when this is present.
|
||||
*/
|
||||
updatedInput?: Record<string, unknown>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { RunHookOptions } from '@deepseek-ai/dsh-hook-protocol'
|
||||
/**
|
||||
* A minimal stand-in for the bits of {@link BashExecutor} that {@link runHook}
|
||||
* actually calls (`resolve` then `run`). `runHook` is pure plumbing over those
|
||||
* two methods, so a duck-typed recorder is the right test seam — the REAL
|
||||
* two methods, so a duck-typed recorder is the right test hook — the REAL
|
||||
* executor (dsh-bash-local) is exercised end-to-end by the hook-bridge plugins
|
||||
* that consume this library, not here.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user