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/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')
|
||||
|
||||
Reference in New Issue
Block a user