@deepseek-ai/dsh-hook-protocol 0.2.0-rc.2 → 0.2.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +13 -13
- package/README.md +7 -6
- package/README.zh.md +7 -6
- package/lib/index.js +13 -3
- package/lib/types/events.d.ts +3 -3
- package/lib/types/index.d.ts +1 -1
- package/lib/types/matcher.d.ts +10 -2
- package/package.json +8 -15
- package/lib/invariant.js +0 -97
- package/lib/types/invariant.d.ts +0 -13
package/README.i18n.yaml
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
2
|
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
3
|
# After editing either side, bring the other along and re-record with:
|
|
4
|
-
# pnpm run verify-translation-pairing --write packages/
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/experimental/hook-protocol/README.md
|
|
5
5
|
/:
|
|
6
6
|
en: 7cfe3761eedff848
|
|
7
7
|
zh: 80b0bed5743d0924
|
|
8
8
|
/deepseek-ai-dsh-hook-protocol:
|
|
9
|
-
en:
|
|
10
|
-
zh:
|
|
9
|
+
en: 3092627c127b688d
|
|
10
|
+
zh: bbdb083e0a5dbc25
|
|
11
11
|
/deepseek-ai-dsh-hook-protocol/summary:
|
|
12
12
|
en: e8ab847d0db16043
|
|
13
13
|
zh: 5c1e72f4b4a54fe1
|
|
@@ -30,20 +30,20 @@
|
|
|
30
30
|
en: 08b3960150e9c5e2
|
|
31
31
|
zh: 6bb2f236d697de01
|
|
32
32
|
/deepseek-ai-dsh-hook-protocol/understand-the-implementation/processing-pipeline:
|
|
33
|
-
en:
|
|
34
|
-
zh:
|
|
33
|
+
en: f71878539d98392e
|
|
34
|
+
zh: 826a0dd95475fff3
|
|
35
35
|
/deepseek-ai-dsh-hook-protocol/understand-the-implementation/hook-session-events:
|
|
36
|
-
en:
|
|
37
|
-
zh:
|
|
36
|
+
en: f01ab4ab98f052b3
|
|
37
|
+
zh: 1da25fa50378bd19
|
|
38
38
|
/deepseek-ai-dsh-hook-protocol/understand-the-implementation/design-philosophy:
|
|
39
|
-
en:
|
|
40
|
-
zh:
|
|
39
|
+
en: 7f9239eaea5a945c
|
|
40
|
+
zh: 9ba6a2ea955d4934
|
|
41
41
|
/deepseek-ai-dsh-hook-protocol/understand-the-implementation/source-map:
|
|
42
|
-
en:
|
|
43
|
-
zh:
|
|
42
|
+
en: 0d8956a6d8608ee0
|
|
43
|
+
zh: 9f554284c4c0a36e
|
|
44
44
|
/deepseek-ai-dsh-hook-protocol/further-exploration:
|
|
45
|
-
en:
|
|
46
|
-
zh:
|
|
45
|
+
en: 8b585ce73c2be2cc
|
|
46
|
+
zh: c0270fd001e1352b
|
|
47
47
|
/deepseek-ai-dsh-hook-protocol/model-experience:
|
|
48
48
|
en: 53d2f59435a4a88f
|
|
49
49
|
zh: 79c8fc2849b20718
|
package/README.md
CHANGED
|
@@ -60,20 +60,22 @@ This section explains the design decisions behind the library and points at the
|
|
|
60
60
|
|
|
61
61
|
### Processing pipeline
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
`selectHookGroups` selects nonempty matching groups in config order. Each bridge resolves the current working directory only when this selection contains commands to run. `appendHookInvoked` omits absent or undefined matchers from the recorded event.
|
|
64
|
+
|
|
65
|
+
The library is a chain of single-purpose steps, one function each: validate the matcher pattern, run the command through the `dsh-shell` executor, decode the outcome, merge every matched hook's outcome into one most-restrictive result, and record the durable `hook/*` event pair. The matcher's `mode` parameter is the single axis the dialects differ on — `claude-code` interprets a pattern as literal alternatives or a regex, `codex` always as an unanchored regex. Every step degrades to a contained outcome instead of throwing, so a hook can never crash the calling turn: an invalid regex is a non-match, an executor rejection becomes a `HookOutput` with no exit code, exit 2 blocks with stderr as the reason, and every other failure stays non-blocking. Merging applies `deny > ask > allow` precedence, keeps the first `continue: false` stop sticky, and accumulates context in hook order. Detached runs are tracked so `fiber.dispose()` reaches quiescence. The steps live in [`src/matcher.ts`](src/matcher.ts), [`src/runner.ts`](src/runner.ts), [`src/codec.ts`](src/codec.ts), [`src/merge.ts`](src/merge.ts), [`src/events.ts`](src/events.ts), and [`src/detached.ts`](src/detached.ts).
|
|
64
66
|
|
|
65
67
|
### `hook/*` session events
|
|
66
68
|
|
|
67
69
|
The `hook/invoked` and `hook/result` events are declaration-merged into `SessionEventMap` as log-only records: like `compaction/*`, they are not surface events and carry no `surfaceOp`. A `hook/result` pairs with its `hook/invoked` by `handlerId`, and `appendHookResult` owns the decision rule. Payloads and per-event JSDoc live in the generated [persistence log event catalog](../../../docs/persistence-catalog.md).
|
|
68
70
|
|
|
69
|
-
Invocation and result records must sit inside an open turn: `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` satisfy that relation by construction, while `SessionStart` runs before turn 1 and gets no `hook/*` record — its injected context is delivered instead.
|
|
71
|
+
Invocation and result records must sit inside an open turn: `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` satisfy that relation by construction, while `SessionStart` runs before turn 1 and gets no `hook/*` record — its injected context is delivered instead.
|
|
70
72
|
|
|
71
73
|
### Design philosophy
|
|
72
74
|
|
|
73
75
|
- **One axis of difference collapsed into `mode`.** The dialects differ only in how a matcher pattern is interpreted, so the matcher takes the mode as a parameter instead of duplicating the engine.
|
|
74
76
|
- **The executor owns process control.** Commands run through the `dsh-shell` executor rather than a bespoke spawn: the executor already provides the scrubbed-but-overridable environment, process-group cancellation, and timeout the protocol needs.
|
|
75
77
|
- **Never throw into the loop.** Every failure mode — malformed JSON, an invalid regex, an executor rejection — degrades to a contained outcome or a non-match, so a hook can never crash the calling turn.
|
|
76
|
-
- **Log-only, turn-enclosed events.** The `hook/*` records are durable evidence of what ran and what it decided; they are not surface events
|
|
78
|
+
- **Log-only, turn-enclosed events.** The `hook/*` records are durable evidence of what ran and what it decided; they are not surface events.
|
|
77
79
|
|
|
78
80
|
The [hook-protocol-lib Agent Note](../../../.agents/notes/archived/feature/2026-06-30-hook-protocol-lib.md) records the shared-versus-per-dialect split and the alternatives considered.
|
|
79
81
|
|
|
@@ -82,14 +84,13 @@ The [hook-protocol-lib Agent Note](../../../.agents/notes/archived/feature/2026-
|
|
|
82
84
|
| File | Role |
|
|
83
85
|
|---|---|
|
|
84
86
|
| [`src/index.ts`](src/index.ts) | Public exports of every primitive and event helper |
|
|
85
|
-
| [`src/matcher.ts`](src/matcher.ts) |
|
|
87
|
+
| [`src/matcher.ts`](src/matcher.ts) | Matcher validation, dialect matching, and nonempty command-group selection |
|
|
86
88
|
| [`src/runner.ts`](src/runner.ts) | `runHook` execution through `ctx.shell` and `DEFAULT_HOOK_TIMEOUT_MS` |
|
|
87
89
|
| [`src/codec.ts`](src/codec.ts) | Exit-code and structured-stdout decoding into `HookOutput` |
|
|
88
90
|
| [`src/merge.ts`](src/merge.ts) | Most-restrictive merge and the `MergedHookOutcome` type |
|
|
89
91
|
| [`src/events.ts`](src/events.ts) | `hook/*` event declaration, append helpers, stderr summary |
|
|
90
92
|
| [`src/detached.ts`](src/detached.ts) | Detached-run quiescence tracking |
|
|
91
93
|
| [`src/types.ts`](src/types.ts) | `HookOutput`, `MatcherGroup`, `CommandHook`, and the `hook/*` payload types |
|
|
92
|
-
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion: pairing, turn enclosure, dialect, and duration checks |
|
|
93
94
|
|
|
94
95
|
</details>
|
|
95
96
|
|
|
@@ -103,7 +104,7 @@ Read these pages when the package-level contract is not enough. They move from t
|
|
|
103
104
|
- [Hooks group map](../README.md) — the sibling group page and its package table.
|
|
104
105
|
- [Hook protocol library Agent Note](../../../.agents/notes/archived/feature/2026-06-30-hook-protocol-lib.md) — why the protocol core is shared and what each bridge owns.
|
|
105
106
|
- [Hook bridges Agent Note](../../../.agents/notes/archived/feature/2026-06-30-hook-bridges.md) — how the two bridges use these primitives.
|
|
106
|
-
- [Interception extension-points
|
|
107
|
+
- [Interception extension-points reference](../../../docs/tool-execution-pipeline.md) — the typed-Decision surface the bridges map onto.
|
|
107
108
|
- [Generated persistence log event catalog](../../../docs/persistence-catalog.md) — the `hook/*` event payloads and per-event JSDoc.
|
|
108
109
|
|
|
109
110
|
-----
|
package/README.zh.md
CHANGED
|
@@ -60,20 +60,22 @@ kind: "package-library"
|
|
|
60
60
|
|
|
61
61
|
### 处理流水线
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
`selectHookGroups` 按配置顺序选出包含命令的匹配组。仅当所选组中有待运行的命令时,各桥接才解析当前工作目录。`appendHookInvoked` 不会将缺失或值为 undefined 的 matcher 写入事件。
|
|
64
|
+
|
|
65
|
+
本库是一串单一用途的步骤,每个步骤一个函数:校验 matcher pattern、通过 `dsh-shell` 执行器运行命令、解码结果、把每个匹配 hook 的结果合并为最严格的一个结果,并记录持久的 `hook/*` 事件对。matcher 的 `mode` 参数是两个方言唯一的差异轴——`claude-code` 把 pattern 解释为字面量备选或正则,`codex` 始终解释为未锚定正则。每个步骤都会降级为受控结果而不是抛异常,因此钩子永远不会使调用轮次崩溃:无效正则是运行时的不匹配,执行器拒绝会变成没有退出码的 `HookOutput`,退出码 2 以 stderr 作为原因阻塞,其他失败均不阻塞。合并应用 `deny > ask > allow` 优先级,保持首个 `continue: false` 停止的粘性,并按 hook 顺序累积上下文。脱离运行会被跟踪,因此 `fiber.dispose()` 能达到完全停稳。这些步骤位于 [`src/matcher.ts`](src/matcher.ts)、[`src/runner.ts`](src/runner.ts)、[`src/codec.ts`](src/codec.ts)、[`src/merge.ts`](src/merge.ts)、[`src/events.ts`](src/events.ts) 与 [`src/detached.ts`](src/detached.ts)。
|
|
64
66
|
|
|
65
67
|
### `hook/*` 会话事件
|
|
66
68
|
|
|
67
69
|
`hook/invoked` 与 `hook/result` 事件通过 declaration merging 合并进 `SessionEventMap`,作为仅日志记录:与 `compaction/*` 相同,它们不是 surface 事件,也不携带 `surfaceOp`。`hook/result` 按 `handlerId` 与其 `hook/invoked` 配对,决策规则由 `appendHookResult` 负责。载荷与逐事件 JSDoc 位于生成的[持久化日志事件目录](../../../docs/persistence-catalog.zh.md)中。
|
|
68
70
|
|
|
69
|
-
调用与结果记录必须位于尚未结束的轮次内:`UserPromptSubmit`、`PreToolUse`、`PostToolUse` 与 `Stop` 按构造满足该关系,而 `SessionStart` 在轮次 1 之前运行、没有 `hook/*`
|
|
71
|
+
调用与结果记录必须位于尚未结束的轮次内:`UserPromptSubmit`、`PreToolUse`、`PostToolUse` 与 `Stop` 按构造满足该关系,而 `SessionStart` 在轮次 1 之前运行、没有 `hook/*` 记录——改为投递其注入的上下文。
|
|
70
72
|
|
|
71
73
|
### 设计理念
|
|
72
74
|
|
|
73
75
|
- **把唯一差异轴收拢进 `mode`。** 两个方言只在 matcher pattern 的解读方式上不同,因此 matcher 把 mode 作为参数,而不是复制引擎。
|
|
74
76
|
- **执行器拥有进程控制。** 命令通过 `dsh-shell` 执行器运行,而非自建 spawn:执行器已经提供了协议所需的已清理但可覆盖的环境、进程组取消与超时。
|
|
75
77
|
- **绝不向循环抛异常。** 每种失败模式——格式错误的 JSON、无效正则、执行器拒绝——都会降级为受控的结果或不匹配,因此钩子永远不能使调用轮次崩溃。
|
|
76
|
-
- **仅日志、轮次内的事件。** `hook/*` 记录是「运行了什么、决定了什么」的持久证据;它们不是 surface
|
|
78
|
+
- **仅日志、轮次内的事件。** `hook/*` 记录是「运行了什么、决定了什么」的持久证据;它们不是 surface 事件。
|
|
77
79
|
|
|
78
80
|
[hook-protocol-lib Agent Note](../../../.agents/notes/archived/feature/2026-06-30-hook-protocol-lib.md) 记录了共享与逐方言的划分以及备选方案。
|
|
79
81
|
|
|
@@ -82,14 +84,13 @@ kind: "package-library"
|
|
|
82
84
|
| 文件 | 职责 |
|
|
83
85
|
|---|---|
|
|
84
86
|
| [`src/index.ts`](src/index.ts) | 每个原语与事件辅助函数的公开导出 |
|
|
85
|
-
| [`src/matcher.ts`](src/matcher.ts) |
|
|
87
|
+
| [`src/matcher.ts`](src/matcher.ts) | matcher 校验、方言匹配,以及包含命令的匹配组选择 |
|
|
86
88
|
| [`src/runner.ts`](src/runner.ts) | 通过 `ctx.shell` 的 `runHook` 执行与 `DEFAULT_HOOK_TIMEOUT_MS` |
|
|
87
89
|
| [`src/codec.ts`](src/codec.ts) | 退出码与结构化 stdout 解码为 `HookOutput` |
|
|
88
90
|
| [`src/merge.ts`](src/merge.ts) | 最严格合并与 `MergedHookOutcome` 类型 |
|
|
89
91
|
| [`src/events.ts`](src/events.ts) | `hook/*` 事件声明、追加辅助函数、stderr 摘要 |
|
|
90
92
|
| [`src/detached.ts`](src/detached.ts) | 脱离运行的完全停稳跟踪 |
|
|
91
93
|
| [`src/types.ts`](src/types.ts) | `HookOutput`、`MatcherGroup`、`CommandHook` 与 `hook/*` 载荷类型 |
|
|
92
|
-
| [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件:配对、轮次包裹、方言与时长检查 |
|
|
93
94
|
|
|
94
95
|
</details>
|
|
95
96
|
|
|
@@ -103,7 +104,7 @@ kind: "package-library"
|
|
|
103
104
|
- [hooks 组地图](../README.zh.md)——同级组页面及其包表。
|
|
104
105
|
- [hook-protocol-lib Agent Note](../../../.agents/notes/archived/feature/2026-06-30-hook-protocol-lib.md)——协议核心为何共享、各桥接负责什么。
|
|
105
106
|
- [钩子桥接 Agent Note](../../../.agents/notes/archived/feature/2026-06-30-hook-bridges.md)——两个桥接如何使用这些原语。
|
|
106
|
-
- [拦截扩展点
|
|
107
|
+
- [拦截扩展点 参考](../../../docs/tool-execution-pipeline.zh.md)——桥接所映射的类型化 Decision 接口面。
|
|
107
108
|
- [生成的持久化日志事件目录](../../../docs/persistence-catalog.zh.md)——`hook/*` 事件载荷与逐事件 JSDoc。
|
|
108
109
|
|
|
109
110
|
-----
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region lib/types/matcher.js
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Matching and command-group selection shared by both hook dialects. Claude treats alphanumeric/underscore/
|
|
4
4
|
* pipe patterns as literal alternatives and other patterns as regex; Codex
|
|
5
5
|
* treats every non-empty pattern as an unanchored regex. Missing, empty, and
|
|
6
6
|
* `*` match all. Runtime matching contains invalid regexes as non-matches;
|
|
@@ -50,6 +50,16 @@ function matchesMatcher(matcher, query, mode) {
|
|
|
50
50
|
if (mode === "claude-code" && CLAUDE_LITERAL.test(pattern)) return pattern.split("|").includes(query);
|
|
51
51
|
return compileRegex(pattern)?.test(query) ?? false;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Select configured groups that have command hooks and match the event subject.
|
|
55
|
+
* @param groups - groups for one hook point; absent points select no groups.
|
|
56
|
+
* @param query - the event's matcher subject.
|
|
57
|
+
* @param mode - dialect controlling literal or regex matching.
|
|
58
|
+
* @returns Selected groups in config order, retaining their original references.
|
|
59
|
+
*/
|
|
60
|
+
function selectHookGroups(groups, query, mode) {
|
|
61
|
+
return (groups ?? []).filter((group) => group.hooks.length > 0 && matchesMatcher(group.matcher, query, mode));
|
|
62
|
+
}
|
|
53
63
|
//#endregion
|
|
54
64
|
//#region lib/types/codec.js
|
|
55
65
|
/**
|
|
@@ -311,7 +321,7 @@ function summarizeStderr(stderr, maxChars) {
|
|
|
311
321
|
/**
|
|
312
322
|
* Append a `hook/invoked` event naming the handler and hook point to `session`.
|
|
313
323
|
* @param session - the session whose open turn records the event.
|
|
314
|
-
* @param invocation - the invocation identity;
|
|
324
|
+
* @param invocation - the invocation identity; absent or `undefined` matchers are omitted from the payload.
|
|
315
325
|
*/
|
|
316
326
|
function appendHookInvoked(session, invocation) {
|
|
317
327
|
session.append("hook/invoked", {
|
|
@@ -374,4 +384,4 @@ function createDetachedRuns() {
|
|
|
374
384
|
};
|
|
375
385
|
}
|
|
376
386
|
//#endregion
|
|
377
|
-
export { DEFAULT_HOOK_TIMEOUT_MS, DEFAULT_STDERR_SUMMARY_MAX_CHARS, appendHookInvoked, appendHookResult, createDetachedRuns, matcherDiagnostic, matchesMatcher, mergeHookOutputs, parseHookOutput, runHook, summarizeStderr };
|
|
387
|
+
export { DEFAULT_HOOK_TIMEOUT_MS, DEFAULT_STDERR_SUMMARY_MAX_CHARS, appendHookInvoked, appendHookResult, createDetachedRuns, matcherDiagnostic, matchesMatcher, mergeHookOutputs, parseHookOutput, runHook, selectHookGroups, summarizeStderr };
|
package/lib/types/events.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ export interface HookInvocation {
|
|
|
17
17
|
dialect: HookDialect;
|
|
18
18
|
/** A stable id correlating the invoked event with its result. */
|
|
19
19
|
handlerId: string;
|
|
20
|
-
/** The matcher-group pattern
|
|
21
|
-
matcher?: string;
|
|
20
|
+
/** The matcher-group pattern; absent or `undefined` is omitted from the event. */
|
|
21
|
+
matcher?: string | undefined;
|
|
22
22
|
}
|
|
23
23
|
/** The decided outcome half of the pair. */
|
|
24
24
|
export interface HookResultRecord {
|
|
@@ -60,7 +60,7 @@ export declare function summarizeStderr(stderr: string, maxChars: number): strin
|
|
|
60
60
|
/**
|
|
61
61
|
* Append a `hook/invoked` event naming the handler and hook point to `session`.
|
|
62
62
|
* @param session - the session whose open turn records the event.
|
|
63
|
-
* @param invocation - the invocation identity;
|
|
63
|
+
* @param invocation - the invocation identity; absent or `undefined` matchers are omitted from the payload.
|
|
64
64
|
*/
|
|
65
65
|
export declare function appendHookInvoked(session: Session, invocation: HookInvocation): void;
|
|
66
66
|
/**
|
package/lib/types/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module @deepseek-ai/dsh-hook-protocol
|
|
7
7
|
*/
|
|
8
8
|
export type { CommandHook, HookDialect, HookOutput, MatcherGroup, MatcherMode, } from './types.ts';
|
|
9
|
-
export { matcherDiagnostic, matchesMatcher } from './matcher.ts';
|
|
9
|
+
export { matcherDiagnostic, matchesMatcher, selectHookGroups } from './matcher.ts';
|
|
10
10
|
export { parseHookOutput } from './codec.ts';
|
|
11
11
|
export { DEFAULT_HOOK_TIMEOUT_MS, runHook } from './runner.ts';
|
|
12
12
|
export type { RunHookOptions, RunHookResult } from './runner.ts';
|
package/lib/types/matcher.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Matching and command-group selection shared by both hook dialects. Claude treats alphanumeric/underscore/
|
|
3
3
|
* pipe patterns as literal alternatives and other patterns as regex; Codex
|
|
4
4
|
* treats every non-empty pattern as an unanchored regex. Missing, empty, and
|
|
5
5
|
* `*` match all. Runtime matching contains invalid regexes as non-matches;
|
|
6
6
|
* config parsers use {@link matcherDiagnostic} to reject them with a diagnostic.
|
|
7
7
|
* @module @deepseek-ai/dsh-hook-protocol/matcher
|
|
8
8
|
*/
|
|
9
|
-
import type { MatcherMode } from './types.ts';
|
|
9
|
+
import type { MatcherGroup, MatcherMode } from './types.ts';
|
|
10
10
|
/**
|
|
11
11
|
* Validate one matcher before a bridge accepts its config group.
|
|
12
12
|
* @param matcher - configured pattern; match-all sentinels are valid.
|
|
@@ -26,4 +26,12 @@ export declare function matcherDiagnostic(matcher: string | undefined, mode: Mat
|
|
|
26
26
|
* regex.
|
|
27
27
|
*/
|
|
28
28
|
export declare function matchesMatcher(matcher: string | undefined, query: string, mode: MatcherMode): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Select configured groups that have command hooks and match the event subject.
|
|
31
|
+
* @param groups - groups for one hook point; absent points select no groups.
|
|
32
|
+
* @param query - the event's matcher subject.
|
|
33
|
+
* @param mode - dialect controlling literal or regex matching.
|
|
34
|
+
* @returns Selected groups in config order, retaining their original references.
|
|
35
|
+
*/
|
|
36
|
+
export declare function selectHookGroups(groups: readonly MatcherGroup[] | undefined, query: string, mode: MatcherMode): MatcherGroup[];
|
|
29
37
|
//# sourceMappingURL=matcher.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-hook-protocol",
|
|
3
3
|
"description": "Shared Claude Code / Codex hook wire protocol: matcher engine, stdin/exit-code/stdout codec, multi-hook merge, and hook/* session events",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
-
"directory": "packages/
|
|
11
|
+
"directory": "packages/experimental/hook-protocol"
|
|
12
12
|
},
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "lib/index.js",
|
|
@@ -18,29 +18,22 @@
|
|
|
18
18
|
"types": "./lib/types/index.d.ts",
|
|
19
19
|
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
|
-
"./invariant": {
|
|
22
|
-
"types": "./lib/types/invariant.d.ts",
|
|
23
|
-
"default": "./lib/invariant.js"
|
|
24
|
-
},
|
|
25
21
|
"./src/*": "./src/*",
|
|
26
22
|
"./package.json": "./package.json"
|
|
27
23
|
},
|
|
28
24
|
"files": [
|
|
29
25
|
"lib/index.js",
|
|
30
|
-
"lib/invariant.js",
|
|
31
26
|
"lib/types/**/*.d.ts"
|
|
32
27
|
],
|
|
33
28
|
"license": "MIT",
|
|
34
29
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/dsh-shell": "0.2.
|
|
36
|
-
"@deepseek-ai/dsh-
|
|
37
|
-
"@deepseek-ai/
|
|
38
|
-
"@deepseek-ai/cordis": "~4.0.4"
|
|
30
|
+
"@deepseek-ai/dsh-shell": "0.2.1-alpha.2",
|
|
31
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.2",
|
|
32
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1"
|
|
39
33
|
},
|
|
40
34
|
"devDependencies": {
|
|
41
|
-
"@deepseek-ai/dsh-shell": "0.2.
|
|
42
|
-
"@deepseek-ai/dsh-
|
|
43
|
-
"@deepseek-ai/
|
|
44
|
-
"@deepseek-ai/cordis": "~4.0.4"
|
|
35
|
+
"@deepseek-ai/dsh-shell": "0.2.1-alpha.2",
|
|
36
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.2",
|
|
37
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1"
|
|
45
38
|
}
|
|
46
39
|
}
|
package/lib/invariant.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
//#region lib/types/invariant.js
|
|
2
|
-
/** Package-owned hook invocation/result stream invariants. @module @deepseek-ai/dsh-hook-protocol/invariant */
|
|
3
|
-
const PACKAGE_NAME = "@deepseek-ai/dsh-hook-protocol";
|
|
4
|
-
/** Cordis companion plugin name. */
|
|
5
|
-
const name = "hook-protocol-invariant";
|
|
6
|
-
/** Service required before the companion can reserve package ownership. */
|
|
7
|
-
const inject = ["invariants"];
|
|
8
|
-
/** Correlation key shared by an invoked/result pair. */
|
|
9
|
-
function hookKey(data) {
|
|
10
|
-
return `${data.turn}\0${data.point}\0${data.handlerId}`;
|
|
11
|
-
}
|
|
12
|
-
/** Validate one hook event against committed pending invocations. */
|
|
13
|
-
function validateHookEvent(trace, event, fail) {
|
|
14
|
-
if (event.type !== "hook/invoked" && event.type !== "hook/result") return void 0;
|
|
15
|
-
if (trace.openTurn === null) fail(`${event.type} appended outside any open turn`);
|
|
16
|
-
if (event.data.turn !== trace.openTurn) fail(`${event.type} names turn ${event.data.turn} but open turn is ${trace.openTurn}`);
|
|
17
|
-
if (event.type === "hook/invoked") {
|
|
18
|
-
if (event.data.point.length === 0 || event.data.handlerId.length === 0) fail("hook/invoked point and handlerId must be non-empty");
|
|
19
|
-
const dialect = event.data.dialect;
|
|
20
|
-
if (dialect !== "claude-code" && dialect !== "codex") fail(`hook/invoked carries unknown dialect ${JSON.stringify(dialect)}`);
|
|
21
|
-
return {
|
|
22
|
-
key: hookKey(event.data),
|
|
23
|
-
delta: 1
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
const key = hookKey(event.data);
|
|
27
|
-
if ((trace.pending.get(key) ?? 0) === 0) fail(`hook/result has no matching hook/invoked for ${JSON.stringify(event.data.handlerId)}`);
|
|
28
|
-
if (!Number.isFinite(event.data.durationMs) || event.data.durationMs < 0) fail("hook/result durationMs must be a non-negative finite number");
|
|
29
|
-
return {
|
|
30
|
-
key,
|
|
31
|
-
delta: -1
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/** Apply one committed hook-pair transition. */
|
|
35
|
-
function applyHookTransition(pending, transition) {
|
|
36
|
-
const next = (pending.get(transition.key) ?? 0) + transition.delta;
|
|
37
|
-
if (next === 0) pending.delete(transition.key);
|
|
38
|
-
else pending.set(transition.key, next);
|
|
39
|
-
}
|
|
40
|
-
/** Install hook invoked/result pairing checks. */
|
|
41
|
-
const install = Object.assign((ctx, fail) => {
|
|
42
|
-
const traces = /* @__PURE__ */ new WeakMap();
|
|
43
|
-
const staged = /* @__PURE__ */ new WeakMap();
|
|
44
|
-
const seed = (session) => {
|
|
45
|
-
const trace = {
|
|
46
|
-
openTurn: null,
|
|
47
|
-
pending: /* @__PURE__ */ new Map()
|
|
48
|
-
};
|
|
49
|
-
traces.set(session, trace);
|
|
50
|
-
for (const event of session.snapshotEvents()) {
|
|
51
|
-
if (event.type === "turn/start") trace.openTurn = event.data.turn;
|
|
52
|
-
else if (event.type === "turn/end") trace.openTurn = null;
|
|
53
|
-
const transition = validateHookEvent(trace, event, fail);
|
|
54
|
-
if (transition !== void 0) applyHookTransition(trace.pending, transition);
|
|
55
|
-
}
|
|
56
|
-
return trace;
|
|
57
|
-
};
|
|
58
|
-
const traceFor = (session) => traces.get(session) ?? seed(session);
|
|
59
|
-
for (const session of ctx.sessions.list()) seed(session);
|
|
60
|
-
ctx.on("session/created", (session) => {
|
|
61
|
-
seed(session);
|
|
62
|
-
}, { global: true });
|
|
63
|
-
ctx.on("session/event", (session, event) => {
|
|
64
|
-
const trace = traceFor(session);
|
|
65
|
-
if (event.type === "turn/start") {
|
|
66
|
-
trace.openTurn = event.data.turn;
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (event.type === "turn/end") {
|
|
70
|
-
trace.openTurn = null;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
if (event.type !== "hook/invoked" && event.type !== "hook/result") return;
|
|
74
|
-
const candidate = staged.get(event);
|
|
75
|
-
/* v8 ignore next -- internal/dispatch stages every hook invocation/result event */
|
|
76
|
-
if (candidate === void 0 || candidate.session !== session) return fail("hook event published without pre-commit validation");
|
|
77
|
-
staged.delete(event);
|
|
78
|
-
applyHookTransition(trace.pending, candidate.transition);
|
|
79
|
-
}, { global: true });
|
|
80
|
-
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
81
|
-
if (eventName !== "session/event") return;
|
|
82
|
-
const [session, event] = args;
|
|
83
|
-
const transition = validateHookEvent(traceFor(session), event, fail);
|
|
84
|
-
if (transition !== void 0) staged.set(event, {
|
|
85
|
-
session,
|
|
86
|
-
transition
|
|
87
|
-
});
|
|
88
|
-
}, { global: true });
|
|
89
|
-
}, { inject: ["sessions"] });
|
|
90
|
-
/**
|
|
91
|
-
* Register the hook-protocol invariant companion.
|
|
92
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
93
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
94
|
-
*/
|
|
95
|
-
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
96
|
-
//#endregion
|
|
97
|
-
export { apply, inject, name };
|
package/lib/types/invariant.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/** Package-owned hook invocation/result stream invariants. @module @deepseek-ai/dsh-hook-protocol/invariant */
|
|
2
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
-
/** Cordis companion plugin name. */
|
|
4
|
-
export declare const name = "hook-protocol-invariant";
|
|
5
|
-
/** Service required before the companion can reserve package ownership. */
|
|
6
|
-
export declare const inject: string[];
|
|
7
|
-
/**
|
|
8
|
-
* Register the hook-protocol invariant companion.
|
|
9
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
-
*/
|
|
12
|
-
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
-
//# sourceMappingURL=invariant.d.ts.map
|