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/README.md
|
||||
README.md: 35989292313c6cef47c02041eb671a73ff7c5786
|
||||
README.zh.md: 3d169d1286f38658d98f4c5f8d016ef77e1b6517
|
||||
README.md: fe743705ea8c07a2422847606cdfa42227be962f
|
||||
README.zh.md: 909ecf8e5414c382603beb7ee3fd0acbecdcfc47
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The hooks subsystem lets users extend the agent at lifecycle points the way Claude Code and Codex do — by pointing a bridge plugin at an existing `hooks.json` (or settings) so those external shell hooks run faithfully. The canonical extension surface itself is the harness's typed interception seams ([the interception-seams Agent Note](../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md)); a "native hook" is just an ordinary cordis plugin on those seams. These packages are the **bridges** that translate the external shell-hook protocol onto that same surface, plus the shared wire-protocol library they build on.
|
||||
The hooks subsystem lets users extend the agent at lifecycle points the way Claude Code and Codex do — by pointing a bridge plugin at an existing `hooks.json` (or settings) so those external shell hooks run faithfully. The canonical extension surface itself is the harness's typed interception points ([the interception extension-points Agent Note](../../.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md)); a "native hook" is just an ordinary Cordis plugin on those extension points. These packages are the **bridges** that translate the external shell-hook protocol onto that same surface, plus the shared wire-protocol library they build on.
|
||||
|
||||
| Package | Role | Shape |
|
||||
|---|---|---|
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
hooks 子系统让用户像使用 Claude Code 和 Codex 一样,在生命周期节点扩展 agent(智能体):把桥接插件指向现有 `hooks.json`(或 settings),即可忠实运行这些外部 shell 钩子。规范扩展 surface 本身是 harness 的类型化拦截 seam(参见[拦截 seam Agent Note](../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md));“原生钩子”只是这些 seam 上的普通 Cordis 插件。这些包是把外部 shell 钩子协议转换到同一 surface 的**桥接**,也包括它们共同依赖的共享协议格式库。
|
||||
hooks 子系统让用户像使用 Claude Code 和 Codex 一样,在生命周期节点扩展 agent(智能体):把桥接插件指向现有 `hooks.json`(或 settings),即可忠实运行这些外部 shell 钩子。规范扩展 surface 本身是 harness 的类型化拦截点(参见[拦截扩展点 Agent Note](../../.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md));“原生钩子”只是这些扩展点上的普通 Cordis 插件。这些包是把外部 shell 钩子协议转换到同一 surface 的**桥接**,也包括它们共同依赖的共享协议格式库。
|
||||
|
||||
| 包 | 职责 | 形态 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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/hooks-claude/README.md
|
||||
README.md: c97643832821746b816d80d498e8a66fbb9db895
|
||||
README.zh.md: 1562c99fc9d5c24ea3f303ea034a13efec022f77
|
||||
README.md: 77806514a9ae928ec0e6a233b0760016cad1eded
|
||||
README.zh.md: 7ba8f269eedb8791042a5cc8cff1affe35fd2920
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
A cordis plugin that runs the supported command-hook subset of a user's existing **Claude Code** hook config (a `hooks.json`, or a settings file's `hooks` key) on the harness's canonical interception seams. It is the **CC dialect** half of the hooks subsystem: it owns the bridge's CC-shaped per-event stdin payloads, CC's env + `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` substitution, and the mapping from a hook's neutral outcome onto the harness's typed Decisions. The dialect-agnostic primitives (matcher, exit-code/stdout codec, `ctx.bash` execution, most-restrictive merge, the `hook/*` events) come from [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md).
|
||||
A cordis plugin that runs the supported command-hook subset of a user's existing **Claude Code** hook config (a `hooks.json`, or a settings file's `hooks` key) on the harness's canonical interception points. It is the **CC dialect** half of the hooks subsystem: it owns the bridge's CC-shaped per-event stdin payloads, CC's env + `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` substitution, and the mapping from a hook's neutral outcome onto the harness's typed Decisions. The dialect-agnostic primitives (matcher, exit-code/stdout codec, `ctx.bash` execution, most-restrictive merge, the `hook/*` events) come from [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md).
|
||||
|
||||
A native cordis plugin could do everything this bridge does — more powerfully, with typed returns and no serialization boundary. **The bridge exists only as a compatibility path for the mapped CC command-hook subset**; anything bespoke should be a native plugin on the same seams (see [the interception-seams Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md)).
|
||||
A native cordis plugin could do everything this bridge does — more powerfully, with typed returns and no serialization boundary. **The bridge exists only as a compatibility path for the mapped CC command-hook subset**; anything bespoke should be a native plugin on the same extension points (see [the interception extension-points Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md)).
|
||||
|
||||
## Config
|
||||
|
||||
@@ -32,9 +32,9 @@ The config is parsed **once** at load. `configPath` is **process-level**: a rela
|
||||
|
||||
The hooks **themselves** run in the agent's session workspace: for the agent-scoped points the bridge passes the session's `cwd` (the `session/new.cwd`) as the hook process's working directory, so a hook's `pwd`/relative-path/marker operates in the user's project tree, not the server launch dir.
|
||||
|
||||
## Hook points → seam Decisions
|
||||
## Hook points → typed Decisions
|
||||
|
||||
| CC hook | Harness seam | Mapping |
|
||||
| CC hook | Harness point | Mapping |
|
||||
|---|---|---|
|
||||
| `SessionStart` | `agent/session-start` (emit) | additionalContext → `agent.inject()` into the new session (cannot block) |
|
||||
| `UserPromptSubmit` | `agent/pre-step` (waterfall) | `deny` → `PreStepDecision.reject`; additionalContext-only → delegate via `next()` then append a separately sourced message to a downstream `enter` decision (a later outer listener can still reject/rewrite) |
|
||||
@@ -44,7 +44,7 @@ The hooks **themselves** run in the agent's session workspace: for the agent-sco
|
||||
| `SubagentStart` | `subagent/start` (emit) | additionalContext → `agent.inject()` into a live in-process child; a remote child has no local injection target |
|
||||
| `SubagentStop` | `subagent/end` (emit) | observe-only |
|
||||
|
||||
The three emit points run detached — no seam awaits a `SessionStart`/`SubagentStart`/`SubagentStop` hook. Each run chain is tracked, and disposing the bridge aborts still-running hook processes, then drains the continuations before the dispose resolves (`createDetachedRuns` in `dsh-hook-protocol`).
|
||||
The three emit points run detached — no extension point awaits a `SessionStart`/`SubagentStart`/`SubagentStop` hook. Each run chain is tracked, and disposing the bridge aborts still-running hook processes, then drains the continuations before the dispose resolves (`createDetachedRuns` in `dsh-hook-protocol`).
|
||||
|
||||
The matcher subject is the tool name (`PreToolUse`/`PostToolUse`), the session source (`SessionStart`), or a constant `agent_type` of `general-purpose` (`SubagentStart`/`SubagentStop` — the harness subagent seam carries no per-kind label, so the bridge reports Claude Code's own Task-tool default; a default/`*`/empty `agent_type` matcher fires, a specific-kind matcher does not); `UserPromptSubmit`/`Stop` ignore matchers. Multiple file-configured hooks on one point run **serially, in config order**, and fold most-restrictively (`deny > ask > allow`, see `dsh-hook-protocol`); serial keeps each hook's `hook/invoked`/`hook/result` pair adjacent in the log, and the fold is order-independent for the decision (see the Agent Note's "run serially, not concurrently" note).
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
一个 Cordis 插件,在 harness 的规范拦截 seam 上运行用户现有 **Claude Code** hook 配置(`hooks.json` 或 settings 文件的 `hooks` key)中受支持的 command hook 子集。它是 hooks 子系统的 **CC 方言**部分,负责桥接中 CC 格式的逐事件 stdin payload、CC 的 env 和 `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` 替换,以及将 hook 的中性结果映射为 harness 的类型化 Decision。方言无关原语(matcher、退出码/stdout codec、`ctx.bash` 执行、最严格合并、`hook/*` 事件)来自 [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md)。
|
||||
一个 Cordis 插件,在 harness 的规范拦截点上运行用户现有 **Claude Code** hook 配置(`hooks.json` 或 settings 文件的 `hooks` key)中受支持的 command hook 子集。它是 hooks 子系统的 **CC 方言**部分,负责桥接中 CC 格式的逐事件 stdin payload、CC 的 env 和 `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` 替换,以及将 hook 的中性结果映射为 harness 的类型化 Decision。方言无关原语(matcher、退出码/stdout codec、`ctx.bash` 执行、最严格合并、`hook/*` 事件)来自 [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md)。
|
||||
|
||||
原生 Cordis 插件可以完成此桥接的所有工作,功能更强,且具有类型化返回,没有序列化边界。**该桥接只是已映射 CC command hook 子集的兼容路径**;所有定制行为都应当使用相同 seam 上的原生插件(见 [拦截 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md))。
|
||||
原生 Cordis 插件可以完成此桥接的所有工作,功能更强,且具有类型化返回,没有序列化边界。**该桥接只是已映射 CC command hook 子集的兼容路径**;所有定制行为都应当使用相同扩展点上的原生插件(见 [拦截扩展点 Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md))。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -32,9 +32,9 @@ const config: Config = {
|
||||
|
||||
hook **本身**会在 agent 的会话工作区中运行:对 agent scope 点,桥接会将会话 `cwd`(`session/new.cwd`)作为 hook 进程工作目录,因此 hook 的 `pwd`/相对路径/marker 作用于用户项目树,而非服务器启动目录。
|
||||
|
||||
## Hook 点 → seam Decision
|
||||
## Hook 点 → 类型化 Decision
|
||||
|
||||
| CC hook | Harness seam | 映射 |
|
||||
| CC hook | Harness 点 | 映射 |
|
||||
|---|---|---|
|
||||
| `SessionStart` | `agent/session-start`(emit) | additionalContext → `agent.inject()` 到新会话(无法阻塞) |
|
||||
| `UserPromptSubmit` | `agent/pre-step`(waterfall,瀑布式事件) | `deny` → `PreStepDecision.reject`;仅 additionalContext → 通过 `next()` 委托,再向下游 `enter` 决策追加一条单独标记来源的消息(后续外层 listener 仍可 reject/改写) |
|
||||
@@ -44,7 +44,7 @@ hook **本身**会在 agent 的会话工作区中运行:对 agent scope 点,
|
||||
| `SubagentStart` | `subagent/start`(emit) | additionalContext → `agent.inject()` 到仍在运行的同进程 child;远程 child 没有本地注入目标 |
|
||||
| `SubagentStop` | `subagent/end`(emit) | 只观测 |
|
||||
|
||||
三个 emit 点都以分离方式运行:没有 seam 会等待 `SessionStart`/`SubagentStart`/`SubagentStop` hook。每条运行链都会被跟踪;对桥接执行 dispose(资源释放)时,会中止仍在运行的 hook 进程,并在 dispose 完成前排空 continuation(`createDetachedRuns`,位于 `dsh-hook-protocol`)。
|
||||
三个 emit 点都以分离方式运行:没有扩展点会等待 `SessionStart`/`SubagentStart`/`SubagentStop` hook。每条运行链都会被跟踪;对桥接执行 dispose(资源释放)时,会中止仍在运行的 hook 进程,并在 dispose 完成前排空 continuation(`createDetachedRuns`,位于 `dsh-hook-protocol`)。
|
||||
|
||||
matcher subject 是工具名称(`PreToolUse`/`PostToolUse`)、会话源(`SessionStart`),或常量 `agent_type`,其值为 `general-purpose`(`SubagentStart`/`SubagentStop`)。harness subagent seam 不携带每 kind label,因此桥接报告 Claude Code 自身 Task 工具默认值;默认/`*`/空 `agent_type` matcher 会触发,特定 kind matcher 不会触发。`UserPromptSubmit`/`Stop` 忽略 matcher。一个点上文件配置的多个 hook 会**按配置顺序串行运行**,并按最严格方式折叠(`deny > ask > allow`,见 `dsh-hook-protocol`)。串行使每个 hook 的 `hook/invoked`/`hook/result` 对在日志中相邻,决策折叠与顺序无关(见 Agent Note 的「run serially, not concurrently」说明)。
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Bridge for unmodified Claude Code command hooks on harness interception
|
||||
* seams. It supports SessionStart, prompt/tool pre/post, Stop, and subagent
|
||||
* extension points. It supports SessionStart, prompt/tool pre/post, Stop, and subagent
|
||||
* start/stop. It owns Claude payloads, environment, substitution, and decision
|
||||
* mapping; shared execution and parsing live in `dsh-hook-protocol`.
|
||||
* `updatedInput` is logged and warned but not honored. Bespoke behavior should
|
||||
* use typed native plugins on the same seams; see the
|
||||
* use typed native plugins on the same extension points; see the
|
||||
* [hook-bridges Agent Note](../../../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md).
|
||||
* @module @deepseek-ai/dsh-hooks-claude
|
||||
*/
|
||||
@@ -38,7 +38,7 @@ import { parseClaudeConfig, type ClaudeHookConfig } from './config.ts'
|
||||
|
||||
export const name = 'hooks-claude'
|
||||
// `bash` is required to run hooks; the rest are read opportunistically via
|
||||
// ctx.get so a deployment can load this bridge without every seam present.
|
||||
// ctx.get so a deployment can load this bridge without every extension point present.
|
||||
export const inject = ['bash']
|
||||
|
||||
/** Plugin config: where the CC hook config lives + substitution roots. */
|
||||
@@ -130,7 +130,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
* `matchQuery`, with the per-event `payload` on stdin, and fold the results.
|
||||
* Writes a `hook/invoked`/`hook/result` pair per hook when `opts.turn` names
|
||||
* an open turn. Detached lifecycle points omit the pair. Returns the merged outcome (a neutral,
|
||||
* already-most-restrictive view) for the caller to map onto its seam
|
||||
* already-most-restrictive view) for the caller to map onto its extension point
|
||||
* decision. `matchQuery` is the event's matcher subject (tool name, session
|
||||
* source, …); `''` for events that ignore matchers.
|
||||
*/
|
||||
@@ -186,7 +186,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
return mergeHookOutputs(outputs)
|
||||
}
|
||||
|
||||
// TODO(hook-continue-false): `merged.stop` is logged but needs a run-level halt seam.
|
||||
// TODO(hook-continue-false): `merged.stop` is logged but needs a run-level halt mechanism.
|
||||
|
||||
/** Build additional model context from hook output, or return undefined when empty. */
|
||||
function contextFrom(merged: MergedHookOutcome): UserMessage | undefined {
|
||||
@@ -310,7 +310,7 @@ const SUBAGENT_TYPE = 'general-purpose'
|
||||
function lastTurn(agent: Agent | undefined): number {
|
||||
if (!agent) return 0
|
||||
const last = [...agent.session.events].findLast(e => e.type === 'turn/start')
|
||||
/* v8 ignore next -- agent-present callers are tool/stop seams inside an open turn. */
|
||||
/* v8 ignore next -- agent-present callers are tool/stop extension points inside an open turn. */
|
||||
return last?.type === 'turn/start' ? last.data.turn : 0
|
||||
}
|
||||
|
||||
|
||||
@@ -416,7 +416,7 @@ export function defineCoverageCases(group: CoverageGroup): void {
|
||||
|
||||
if (group === 'context') describe('hooks-claude coverage — continue:false, context arm, no-cwd', () => {
|
||||
it('a {"continue":false} hook is RECORDED as decision "stop" but does not halt the run (TODO(hook-continue-false))', async () => {
|
||||
// The seams cannot yet honor `continue:false` as a hard halt. The log must still record the
|
||||
// The extension points cannot yet honor `continue:false` as a hard halt. The log must still record the
|
||||
// stop decision while execution and the turn continue normally.
|
||||
const d = dir()
|
||||
const s = sh(d, 'stop.sh', '#!/usr/bin/env bash\necho \'{"continue":false,"stopReason":"halt"}\'\n')
|
||||
|
||||
@@ -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/hooks-codex/README.md
|
||||
README.md: 60dd1d098966aad6ccdb0957ee223b9843db499f
|
||||
README.zh.md: 80162e9fb82eb0f9e358c3d14bd24bf4bfeddaca
|
||||
README.md: f8e0684e0bf4eafadcb31d8c82dde177411db2bf
|
||||
README.zh.md: d278e78204c1bd320980420b506135997cd3a4cc
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
A cordis plugin that runs the supported subset of a user's existing **Codex** hook config on the harness's canonical interception seams. The **Codex dialect** half of the hooks subsystem. The dialect-agnostic primitives come from [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md); this bridge owns the Codex-shaped payloads, matcher mode, and decision mapping.
|
||||
A cordis plugin that runs the supported subset of a user's existing **Codex** hook config on the harness's canonical interception points. The **Codex dialect** half of the hooks subsystem. The dialect-agnostic primitives come from [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md); this bridge owns the Codex-shaped payloads, matcher mode, and decision mapping.
|
||||
|
||||
This bridge implements a deliberate subset of Codex's current hook protocol:
|
||||
|
||||
@@ -12,7 +12,7 @@ This bridge implements a deliberate subset of Codex's current hook protocol:
|
||||
- **No Codex plugin env injection and no config-time placeholder substitution** (the command still receives the executor's environment and runs through its shell).
|
||||
- **No pre-tool approval or rewrite path** — a hook can block, but the bridge does not pre-approve or replace tool input.
|
||||
|
||||
A native cordis plugin could do everything this bridge does, more powerfully; the bridge exists only as a compatibility path for the mapped Codex subset (see [the interception-seams Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md)).
|
||||
A native cordis plugin could do everything this bridge does, more powerfully; the bridge exists only as a compatibility path for the mapped Codex subset (see [the interception extension-points Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md)).
|
||||
|
||||
## Config
|
||||
|
||||
@@ -38,9 +38,9 @@ The config is parsed **once** at load. `configPath` is **process-level** — a r
|
||||
|
||||
The hooks themselves run in the agent's session workspace: for the agent-scoped points the bridge passes the session's `cwd` as the hook process's working directory, so a hook operates in the user's project tree, not the server launch dir.
|
||||
|
||||
## Hook points → seam Decisions
|
||||
## Hook points → typed Decisions
|
||||
|
||||
| Codex hook | Harness seam | Mapping |
|
||||
| Codex hook | Harness point | Mapping |
|
||||
|---|---|---|
|
||||
| `SessionStart` | `agent/session-start` (emit) | a plain-stdout hook's output → additionalContext → `agent.inject()` |
|
||||
| `UserPromptSubmit` | `agent/pre-step` (waterfall) | `block` (exit 2) → `PreStepDecision.reject`; additionalContext-only → delegate via `next()` then append a separately sourced message to a downstream `enter` decision |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
一个 Cordis 插件,在 harness 的规范拦截 seam 上运行用户现有 **Codex** hook 配置的受支持子集。它是 hooks 子系统中采用 **Codex 方言** 的一侧。方言无关原语来自 [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md);该桥接负责处理 Codex 形状的 payload、matcher 模式和决策映射。
|
||||
一个 Cordis 插件,在 harness 的规范拦截点上运行用户现有 **Codex** hook 配置的受支持子集。它是 hooks 子系统中采用 **Codex 方言** 的一侧。方言无关原语来自 [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md);该桥接负责处理 Codex 形状的 payload、matcher 模式和决策映射。
|
||||
|
||||
该桥接实现 Codex 当前 hook 协议的一个明确子集:
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- **没有 Codex 插件 env 注入,也没有配置时 placeholder 替换**(命令仍会接收执行器环境,并通过其 shell 运行)。
|
||||
- **没有工具前审批或改写路径**:hook 可以阻塞,但桥接不会预审批或替换工具输入。
|
||||
|
||||
原生 Cordis 插件可以完成此桥接的所有工作,并且功能更强;该桥接只是已映射 Codex 子集的兼容路径(见 [拦截 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md))。
|
||||
原生 Cordis 插件可以完成此桥接的所有工作,并且功能更强;该桥接只是已映射 Codex 子集的兼容路径(见 [拦截扩展点 Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md))。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -38,9 +38,9 @@ const config: Config = {
|
||||
|
||||
hook 本身会在 agent(智能体)的会话工作区中运行:对 agent scope 点,桥接会将会话 `cwd` 作为 hook 进程工作目录,因此 hook 作用于用户项目树,而非服务器启动目录。
|
||||
|
||||
## Hook 点 → seam Decision
|
||||
## Hook 点 → 类型化 Decision
|
||||
|
||||
| Codex hook | Harness seam | 映射 |
|
||||
| Codex hook | Harness 点 | 映射 |
|
||||
|---|---|---|
|
||||
| `SessionStart` | `agent/session-start`(emit) | 纯 stdout hook 的输出 → additionalContext → `agent.inject()` |
|
||||
| `UserPromptSubmit` | `agent/pre-step`(waterfall,瀑布式事件) | `block`(退出码 2)→ `PreStepDecision.reject`;仅 additionalContext → 通过 `next()` 委托,再向下游 `enter` 决策追加一条单独标记来源的消息 |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Bridge for unmodified Codex command hooks on harness interception seams. It
|
||||
* Bridge for unmodified Codex command hooks on harness interception points. It
|
||||
* supports five points (SessionStart, prompt/tool pre/post, Stop), regex-only
|
||||
* matchers, snake_case payloads without a trailing newline, no hook environment
|
||||
* or command substitution, and no pre-tool approval or rewrite path; only
|
||||
@@ -156,7 +156,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
}
|
||||
outputs.push(output)
|
||||
// Execution and decision mapping remain in each bridge so dialect
|
||||
// differences stay explicit at their owning seam.
|
||||
// differences stay explicit at their owning extension point.
|
||||
/* jscpd:ignore-start */
|
||||
if (output.systemMessage !== undefined) {
|
||||
ctx.logger.warn(`hooks-codex: ${point} hook emitted a systemMessage, which is not yet surfaced (ignored)`)
|
||||
@@ -169,7 +169,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
return mergeHookOutputs(outputs)
|
||||
}
|
||||
|
||||
// TODO(hook-continue-false): `merged.stop` is logged but needs a run-level halt seam.
|
||||
// TODO(hook-continue-false): `merged.stop` is logged but needs a run-level halt mechanism.
|
||||
|
||||
function contextFrom(merged: MergedHookOutcome): UserMessage | undefined {
|
||||
if (merged.additionalContext.length === 0) return undefined
|
||||
@@ -279,7 +279,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
function lastTurn(agent: Agent | undefined): number {
|
||||
if (!agent) return 0
|
||||
const last = [...agent.session.events].findLast(e => e.type === 'turn/start')
|
||||
/* v8 ignore next -- agent-present turnBase callers are tool/stop seams inside an open turn. */
|
||||
/* v8 ignore next -- agent-present turnBase callers are tool/stop extension points inside an open turn. */
|
||||
return last?.type === 'turn/start' ? last.data.turn : 0
|
||||
}
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
|
||||
})
|
||||
|
||||
it('a {"continue":false} hook is RECORDED as "stop" but does not halt the run (TODO(hook-continue-false))', async () => {
|
||||
// Honoring `continue:false` is deferred — the seams have no hard-halt
|
||||
// Honoring `continue:false` is deferred — the extension points have no hard-halt
|
||||
// primitive. Assert the LOG records the halt request AND that the run is not
|
||||
// actually halted (the tool still runs, the turn completes).
|
||||
const d = dir()
|
||||
|
||||
Reference in New Issue
Block a user