driftseal 0.4.0 → 0.6.0

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.md CHANGED
@@ -42,11 +42,36 @@ For local development from this checkout:
42
42
  npm link
43
43
  ```
44
44
 
45
- ## Give your coding agent the complete workflow
45
+ ## Recommended agent setup
46
46
 
47
- The package includes `skills/use-driftseal`, an agent-agnostic companion skill that drives repository work through the complete DriftSeal loop while keeping decision records selective. Install or link it using your agent runtime’s skill discovery convention, then invoke `use-driftseal` by name.
47
+ Use `AGENTS.md` + the companion skill + the CLI as the default integration:
48
48
 
49
- ## Use DriftSeal through MCP
49
+ - `AGENTS.md`, installed by `driftseal init`, is the authoritative policy.
50
+ - `skills/use-driftseal` is a small, agent-agnostic discovery and recovery guide.
51
+ - `driftseal` is the default execution surface.
52
+
53
+ Install the bundled skill for one platform. Project scope is the default:
54
+
55
+ ```sh
56
+ driftseal skill install --target codex
57
+ driftseal skill install --target kimi-code --scope global
58
+ ```
59
+
60
+ | Target | Project scope | Global scope |
61
+ | --- | --- | --- |
62
+ | `codex` | `.agents/skills/use-driftseal` | `~/.agents/skills/use-driftseal` |
63
+ | `kimi-code` | `.kimi/skills/use-driftseal` | `~/.kimi/skills/use-driftseal` |
64
+ | `opencode` | `.opencode/skills/use-driftseal` | `~/.config/opencode/skills/use-driftseal` |
65
+ | `claude-code` | `.claude/skills/use-driftseal` | `~/.claude/skills/use-driftseal` |
66
+ | `cursor` | `.cursor/skills/use-driftseal` | `~/.cursor/skills/use-driftseal` |
67
+
68
+ Use `--root <repository>` to select a project when running the installer
69
+ elsewhere. Repeated installs of identical content are no-ops; a different
70
+ existing skill requires `--force`. MCP and lifecycle hooks are optional
71
+ adapters; enable them only for a concrete host constraint or reminder need, not
72
+ as additional policy layers.
73
+
74
+ ## Optional: use DriftSeal through MCP
50
75
 
51
76
  The same package includes `driftseal-mcp`, a local stdio MCP server. It exposes
52
77
  structured tools for the complete intent and decision workflow while reusing the
@@ -59,12 +84,42 @@ Fix the server to one repository when starting it:
59
84
  driftseal-mcp --root /absolute/path/to/repository
60
85
  ```
61
86
 
62
- For Codex, add the installed command as a stdio MCP server:
87
+ Install the server into the current repository's agent config with one of the
88
+ supported targets:
63
89
 
64
90
  ```sh
65
- codex mcp add driftseal -- driftseal-mcp --root /absolute/path/to/repository
91
+ cd /path/to/repository
92
+ driftseal mcp install --target codex
93
+ driftseal mcp install --target kimi-code
94
+ driftseal mcp install --target opencode
95
+ driftseal mcp install --target claude-code
96
+ driftseal mcp install --target cursor
66
97
  ```
67
98
 
99
+ Project scope is the default because each DriftSeal MCP server belongs to one
100
+ repository. Every target pins `--root` to the repository's canonical absolute
101
+ path, and repeated installs are idempotent.
102
+
103
+ | Target | Project config | Global config |
104
+ | --- | --- | --- |
105
+ | `codex` | `.codex/config.toml` | `~/.codex/config.toml` |
106
+ | `kimi-code` | `.kimi-code/mcp.json` | `~/.kimi-code/mcp.json` or `$KIMI_CODE_HOME/mcp.json` |
107
+ | `opencode` | `opencode.json` | `~/.config/opencode/opencode.json` |
108
+ | `claude-code` | `.mcp.json` | `~/.claude.json` |
109
+ | `cursor` | `.cursor/mcp.json` | `~/.cursor/mcp.json` |
110
+
111
+ Use `--root <repository>` when running the installer elsewhere, or choose the
112
+ agent's user-level config explicitly:
113
+
114
+ ```sh
115
+ driftseal mcp install --target <target> --scope global --root /absolute/path/to/repository
116
+ ```
117
+
118
+ Global installs remain pinned to the selected repository. If the chosen config
119
+ already contains a different DriftSeal server entry, the installer leaves it
120
+ untouched unless `--force` is supplied. Other agent settings and MCP servers are
121
+ preserved.
122
+
68
123
  The root is startup configuration, not a tool input. In MCP mode DriftSeal also
69
124
  ignores inherited `DRIFTSEAL_HOME` and `DRIFTSEAL_DECISION_HOME` overrides, so a
70
125
  tool call cannot redirect writes outside the selected repository.
@@ -82,8 +137,43 @@ The v1 server provides:
82
137
  | `driftseal://intents/recent` | Read the ten most recent intents as a JSON resource. |
83
138
  | `driftseal://decisions` | Read the decision catalog as a JSON resource. |
84
139
 
85
- The companion skill remains important: MCP supplies controlled, structured
86
- operations; the skill teaches the agent when to use them and how to avoid drift.
140
+ MCP changes only the execution surface. It does not add policy beyond the
141
+ repository's `AGENTS.md`, and the companion skill remains limited to discovery
142
+ and recovery guidance.
143
+
144
+ ## Optional: keep the agent reminded through hooks
145
+
146
+ Agents that support lifecycle hooks can inject a short DriftSeal reminder before
147
+ the agent starts answering (`UserPromptSubmit`) and surface a warning when it
148
+ finishes (`Stop`). The reminders are advisory — they ask whether the round needs
149
+ an intent and whether an open intent still needs verification and
150
+ `driftseal end`; they never force another model turn, and they stay silent in
151
+ repositories without an intent log.
152
+
153
+ Install them with:
154
+
155
+ ```sh
156
+ cd /path/to/repository
157
+ driftseal hook install --target kimi-code --scope global
158
+ driftseal hook install --target claude-code
159
+ driftseal hook install --target codex
160
+ ```
161
+
162
+ | Target | Project config | Global config |
163
+ | --- | --- | --- |
164
+ | `kimi-code` | Not supported | `~/.kimi-code/config.toml` or `$KIMI_CODE_HOME/config.toml` |
165
+ | `claude-code` | `.claude/settings.json` | `~/.claude/settings.json` |
166
+ | `codex` | `.codex/hooks.json` | `~/.codex/hooks.json` |
167
+
168
+ Like `mcp install`, the hook installer accepts `--scope global`,
169
+ `--root <repository>`, and `--force`, is idempotent, and preserves unrelated
170
+ config entries. Kimi Code documents hooks only in its global `config.toml`, so
171
+ its target requires `--scope global`. Claude Code receives prompt context through
172
+ `hookSpecificOutput.additionalContext`; its `Stop` reminder uses a UI-only
173
+ `systemMessage`, avoiding a continuation loop. Codex installs only the prompt
174
+ hook because its `Stop` event has no advisory context channel. Hook commands
175
+ search the current directory and its ancestors for an intent log. OpenCode and
176
+ Cursor have no supported hook surface for this yet.
87
177
 
88
178
  ## A work round
89
179
 
@@ -121,6 +211,10 @@ Single-step commands that only build, check, or record work already done — com
121
211
  | `driftseal decision update <id> [-s status] -n "..."` | Reconcile a linked decision in the open intent. |
122
212
  | `driftseal decision list [-s status] [--last N \| --count]` | List or count decision records, optionally filtered by status. |
123
213
  | `driftseal decision show <id>` | Read one decision record. |
214
+ | `driftseal skill install --target TARGET [--scope project\|global] [--root path] [--force]` | Install the bundled skill for Codex, Kimi Code, OpenCode, Claude Code, or Cursor. |
215
+ | `driftseal mcp install --target TARGET [--scope project\|global] [--root path] [--force]` | Install the repository-pinned MCP server into Codex, Kimi Code, OpenCode, Claude Code, or Cursor. |
216
+ | `driftseal hook install --target TARGET [--scope project\|global] [--root path] [--force]` | Install advisory lifecycle reminders into Kimi Code, Claude Code, or Codex. |
217
+ | `driftseal hook prompt\|stop [--format plain\|claude-code]` | Emit the reminder a lifecycle hook injects; never blocks. |
124
218
  | `driftseal init` | Add the adoption protocol to `AGENTS.md`. |
125
219
  | `driftseal help` | Print CLI usage. |
126
220
 
package/README.zh-CN.md CHANGED
@@ -42,11 +42,35 @@ driftseal init
42
42
  npm link
43
43
  ```
44
44
 
45
- ## coding agent 掌握完整工作流
45
+ ## 推荐的 agent 配置
46
46
 
47
- npm package 内含 `skills/use-driftseal`。这是一个不绑定特定 agent runtime 的配套 skill,会按完整 DriftSeal 闭环执行仓库任务,同时克制地使用 decision record。按照所用 agent runtime 的 skill discovery 约定安装或 link,之后通过名称 `use-driftseal` 调用即可。
47
+ 默认组合是 `AGENTS.md` + 配套 skill + CLI:
48
48
 
49
- ## 通过 MCP 使用 DriftSeal
49
+ - `driftseal init` 写入的 `AGENTS.md` 是唯一的 policy 来源。
50
+ - `skills/use-driftseal` 是不绑定特定 agent runtime 的轻量发现与恢复指南。
51
+ - `driftseal` CLI 是默认执行入口。
52
+
53
+ 为指定平台安装 package 内置的 skill。默认使用项目级 scope:
54
+
55
+ ```sh
56
+ driftseal skill install --target codex
57
+ driftseal skill install --target kimi-code --scope global
58
+ ```
59
+
60
+ | Target | 项目级 scope | 全局 scope |
61
+ | --- | --- | --- |
62
+ | `codex` | `.agents/skills/use-driftseal` | `~/.agents/skills/use-driftseal` |
63
+ | `kimi-code` | `.kimi/skills/use-driftseal` | `~/.kimi/skills/use-driftseal` |
64
+ | `opencode` | `.opencode/skills/use-driftseal` | `~/.config/opencode/skills/use-driftseal` |
65
+ | `claude-code` | `.claude/skills/use-driftseal` | `~/.claude/skills/use-driftseal` |
66
+ | `cursor` | `.cursor/skills/use-driftseal` | `~/.cursor/skills/use-driftseal` |
67
+
68
+ 如果不在目标 repository 中执行,用 `--root <repository>` 明确指定项目。
69
+ 重复安装相同内容不会产生改动;目标位置已有不同版本时必须显式传入 `--force`。
70
+ MCP 与 lifecycle hook 都是可选适配层;只有确实存在 host 限制或提醒需求时
71
+ 才启用,不要把它们叠成额外的 policy 层。
72
+
73
+ ## 可选:通过 MCP 使用 DriftSeal
50
74
 
51
75
  同一个 package 还提供本地 stdio MCP server:`driftseal-mcp`。它为完整的
52
76
  intent 与 decision 工作流提供结构化 tools,并与 CLI 复用同一套锁、WAL、
@@ -59,12 +83,40 @@ atomic write、schema 和 recovery 实现。server 不会启动 `driftseal` 子
59
83
  driftseal-mcp --root /absolute/path/to/repository
60
84
  ```
61
85
 
62
- Codex 中,可以把安装后的命令添加为 stdio MCP server:
86
+ 选择对应 target,即可把 server 安装到当前 repository 的 agent 配置:
63
87
 
64
88
  ```sh
65
- codex mcp add driftseal -- driftseal-mcp --root /absolute/path/to/repository
89
+ cd /path/to/repository
90
+ driftseal mcp install --target codex
91
+ driftseal mcp install --target kimi-code
92
+ driftseal mcp install --target opencode
93
+ driftseal mcp install --target claude-code
94
+ driftseal mcp install --target cursor
66
95
  ```
67
96
 
97
+ 默认使用项目级配置,因为每个 DriftSeal MCP server 都只属于一个 repository。
98
+ 所有 target 都会把 `--root` 固定为 repository 的规范化绝对路径,并且可以安全地
99
+ 重复执行。
100
+
101
+ | Target | 项目级配置 | 全局配置 |
102
+ | --- | --- | --- |
103
+ | `codex` | `.codex/config.toml` | `~/.codex/config.toml` |
104
+ | `kimi-code` | `.kimi-code/mcp.json` | `~/.kimi-code/mcp.json` 或 `$KIMI_CODE_HOME/mcp.json` |
105
+ | `opencode` | `opencode.json` | `~/.config/opencode/opencode.json` |
106
+ | `claude-code` | `.mcp.json` | `~/.claude.json` |
107
+ | `cursor` | `.cursor/mcp.json` | `~/.cursor/mcp.json` |
108
+
109
+ 在其他目录执行时可以显式传入 `--root <repository>`;也可以明确选择对应 agent
110
+ 的用户级配置:
111
+
112
+ ```sh
113
+ driftseal mcp install --target <target> --scope global --root /absolute/path/to/repository
114
+ ```
115
+
116
+ 全局安装仍会固定到所选 repository。如果目标配置中已经存在不同的 DriftSeal
117
+ server entry,安装器不会修改它;只有显式传入 `--force` 才会替换。其他 agent
118
+ 设置与 MCP servers 会被保留。
119
+
68
120
  root 只能在启动时配置,不是 tool input。MCP 模式也会忽略继承到进程中的
69
121
  `DRIFTSEAL_HOME` 和 `DRIFTSEAL_DECISION_HOME` override,因此 tool call 不能把
70
122
  写入重定向到所选 repository 之外。
@@ -82,8 +134,40 @@ v1 server 提供:
82
134
  | `driftseal://intents/recent` | 以 JSON resource 读取最近十条 intent。 |
83
135
  | `driftseal://decisions` | 以 JSON resource 读取 decision catalog。 |
84
136
 
85
- 配套 skill 仍然不可替代:MCP 提供受控、结构化的操作,skill 则告诉 agent
86
- 何时使用这些操作,以及怎样避免 drift。
137
+ MCP 只替换执行入口,不会在 repository `AGENTS.md` 之外增加 policy;
138
+ 配套 skill 也仍只负责发现与恢复工作流。
139
+
140
+ ## 可选:用 hook 持续提醒 agent
141
+
142
+ 对于支持 lifecycle hook 的 agent,可以在每轮回答前(`UserPromptSubmit`)
143
+ 注入一条简短的 DriftSeal 提醒,并在回答完毕时(`Stop`)显示警告。提醒是
144
+ 建议性的——它提示是否需要开启 intent、是否还有未关闭的 intent 需要验证并
145
+ `driftseal end`;它不会强制模型再跑一轮,并且在还没有 intent log 的
146
+ repository 中保持沉默。
147
+
148
+ 安装方式:
149
+
150
+ ```sh
151
+ cd /path/to/repository
152
+ driftseal hook install --target kimi-code --scope global
153
+ driftseal hook install --target claude-code
154
+ driftseal hook install --target codex
155
+ ```
156
+
157
+ | Target | 项目级配置 | 全局配置 |
158
+ | --- | --- | --- |
159
+ | `kimi-code` | 不支持 | `~/.kimi-code/config.toml` 或 `$KIMI_CODE_HOME/config.toml` |
160
+ | `claude-code` | `.claude/settings.json` | `~/.claude/settings.json` |
161
+ | `codex` | `.codex/hooks.json` | `~/.codex/hooks.json` |
162
+
163
+ 与 `mcp install` 一样,hook 安装器支持 `--scope global`、
164
+ `--root <repository>` 和 `--force`,重复安装是幂等的,并保留无关的配置项。
165
+ Kimi Code 只在全局 `config.toml` 中记录 hook,因此该 target 必须指定
166
+ `--scope global`。Claude Code 的 prompt 提醒使用
167
+ `hookSpecificOutput.additionalContext`,`Stop` 提醒则使用只显示在 UI 中的
168
+ `systemMessage`,不会造成 continuation loop。Codex 只安装 prompt hook,
169
+ 因为它的 `Stop` 事件没有建议性上下文通道。Hook 命令会从当前目录开始向上
170
+ 查找 intent log。OpenCode 和 Cursor 目前还没有可用的 hook 入口。
87
171
 
88
172
  ## 一轮标准工作流
89
173
 
@@ -121,6 +205,10 @@ driftseal end \
121
205
  | `driftseal decision update <id> [-s status] -n "..."` | 在当前 intent 中 reconcile 已关联的 decision。 |
122
206
  | `driftseal decision list [-s status] [--last N \| --count]` | 列出或统计 decision records,也可按 status 筛选。 |
123
207
  | `driftseal decision show <id>` | 查看单条 decision record。 |
208
+ | `driftseal skill install --target TARGET [--scope project\|global] [--root path] [--force]` | 为 Codex、Kimi Code、OpenCode、Claude Code 或 Cursor 安装内置 skill。 |
209
+ | `driftseal mcp install --target TARGET [--scope project\|global] [--root path] [--force]` | 把固定到 repository 的 MCP server 安装到 Codex、Kimi Code、OpenCode、Claude Code 或 Cursor。 |
210
+ | `driftseal hook install --target TARGET [--scope project\|global] [--root path] [--force]` | 把建议性的 lifecycle 提醒安装到 Kimi Code、Claude Code 或 Codex。 |
211
+ | `driftseal hook prompt\|stop [--format plain\|claude-code]` | 输出 lifecycle hook 注入的提醒;绝不阻断。 |
124
212
  | `driftseal init` | 把接入协议写入 `AGENTS.md`。 |
125
213
  | `driftseal help` | 查看 CLI 用法。 |
126
214
 
package/bin/driftseal.js CHANGED
@@ -35,7 +35,7 @@ const DECISION_STATUSES = [
35
35
  'superseded',
36
36
  ];
37
37
  const EVENT_SCHEMA_VERSION = 3;
38
- const PROTOCOL_VERSION = 7;
38
+ const PROTOCOL_VERSION = 8;
39
39
  const LOCK_STALE_MS = 30 * 60 * 1000;
40
40
  const LOCK_INIT_STALE_MS = 5 * 1000;
41
41
  const MAX_DECISION_SLUG_LENGTH = 180;
@@ -191,8 +191,7 @@ function normalizeEvent(event, line) {
191
191
  fail(`unknown event type "${event.type}" on log line ${line}`);
192
192
  }
193
193
 
194
- function readEvents({ repairTail = false } = {}) {
195
- const file = logFile();
194
+ function readEvents({ repairTail = false, file = logFile() } = {}) {
196
195
  if (!fs.existsSync(file)) return [];
197
196
  let content = fs.readFileSync(file, 'utf8');
198
197
  const rawLines = content.split('\n');
@@ -1111,6 +1110,10 @@ function intentProtocolBlock(version = PROTOCOL_VERSION) {
1111
1110
 
1112
1111
  This repo uses DriftSeal (\`driftseal\`) to prevent agent drift. Every work round:
1113
1112
 
1113
+ This \`AGENTS.md\` protocol is the source of truth. Use the \`driftseal\` CLI by
1114
+ default; the companion skill only helps discover and resume the workflow, while
1115
+ MCP and lifecycle hooks are optional adapters.
1116
+
1114
1117
  1. **Write intent first**, before modifying, creating, or deleting files, or
1115
1118
  making any other change that may need a rollback:
1116
1119
  \`driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"\`.
@@ -1148,7 +1151,14 @@ ${INTENT_PROTOCOL_END}`;
1148
1151
  }
1149
1152
 
1150
1153
  function previousIntentProtocolBlock(version) {
1151
- const v6 = intentProtocolBlock(version).replace(
1154
+ const v7 = intentProtocolBlock(version).replace(
1155
+ '\nThis `AGENTS.md` protocol is the source of truth. Use the `driftseal` CLI by\n' +
1156
+ 'default; the companion skill only helps discover and resume the workflow, while\n' +
1157
+ 'MCP and lifecycle hooks are optional adapters.\n',
1158
+ ''
1159
+ );
1160
+ if (version >= 7) return v7;
1161
+ const v6 = v7.replace(
1152
1162
  'doing anything else. The open intent is the source of truth: resume it when its\n' +
1153
1163
  ' objective still matches the current task; otherwise close it (`partial` or\n' +
1154
1164
  ' `abandoned`, with a note) and `begin` a new one.',
@@ -1265,6 +1275,7 @@ Commit \`.decision-log/\` with the code.`;
1265
1275
  }
1266
1276
 
1267
1277
  function previousDecisionProtocolBlock(version) {
1278
+ if (version >= 7) return decisionProtocolBlock(version);
1268
1279
  return decisionProtocolBlock(version).replace(' --driver "<decision driver>"', '');
1269
1280
  }
1270
1281
 
@@ -1319,6 +1330,680 @@ function upgradeManagedBlock({
1319
1330
  fail(`cannot safely upgrade customized protocol block beginning with ${marker}`);
1320
1331
  }
1321
1332
 
1333
+ const MCP_TARGETS = ['codex', 'kimi-code', 'opencode', 'claude-code', 'cursor'];
1334
+ const MCP_SCOPES = ['project', 'global'];
1335
+ const MCP_TARGET_LABELS = {
1336
+ codex: 'Codex',
1337
+ 'kimi-code': 'Kimi Code',
1338
+ opencode: 'OpenCode',
1339
+ 'claude-code': 'Claude Code',
1340
+ cursor: 'Cursor',
1341
+ };
1342
+
1343
+ function mcpInstallUsage() {
1344
+ return 'usage: driftseal mcp install --target <codex|kimi-code|opencode|claude-code|cursor> [--scope project|global] [--root <repository>] [--force]';
1345
+ }
1346
+
1347
+ function mcpConfigLocation(target, scope, root) {
1348
+ const home = os.homedir();
1349
+ if (target === 'codex') {
1350
+ const configDir = scope === 'project' ? path.join(root, '.codex') : path.join(home, '.codex');
1351
+ return { configDir, configFile: path.join(configDir, 'config.toml') };
1352
+ }
1353
+ if (target === 'kimi-code') {
1354
+ const userDir = process.env.KIMI_CODE_HOME
1355
+ ? path.resolve(process.env.KIMI_CODE_HOME)
1356
+ : path.join(home, '.kimi-code');
1357
+ const configDir = scope === 'project' ? path.join(root, '.kimi-code') : userDir;
1358
+ return { configDir, configFile: path.join(configDir, 'mcp.json') };
1359
+ }
1360
+ if (target === 'opencode') {
1361
+ const configDir =
1362
+ scope === 'project' ? root : path.join(home, '.config', 'opencode');
1363
+ return { configDir, configFile: path.join(configDir, 'opencode.json') };
1364
+ }
1365
+ if (target === 'claude-code') {
1366
+ const configDir = scope === 'project' ? root : home;
1367
+ return {
1368
+ configDir,
1369
+ configFile: path.join(configDir, scope === 'project' ? '.mcp.json' : '.claude.json'),
1370
+ };
1371
+ }
1372
+ if (target === 'cursor') {
1373
+ const configDir = scope === 'project' ? path.join(root, '.cursor') : path.join(home, '.cursor');
1374
+ return { configDir, configFile: path.join(configDir, 'mcp.json') };
1375
+ }
1376
+ fail(`unsupported MCP target "${target}"`);
1377
+ }
1378
+
1379
+ function parseMcpInstallRequest(argv) {
1380
+ const [subcommand, ...rest] = argv;
1381
+ if (subcommand !== 'install') {
1382
+ fail(mcpInstallUsage());
1383
+ }
1384
+ const { positionals, flags } = parseArgs(rest, {
1385
+ target: 'single',
1386
+ scope: 'single',
1387
+ root: 'single',
1388
+ force: 'boolean',
1389
+ });
1390
+ if (positionals.length > 0 || !flags.target) {
1391
+ fail(mcpInstallUsage());
1392
+ }
1393
+
1394
+ const target = flags.target.toLowerCase();
1395
+ if (!MCP_TARGETS.includes(target)) {
1396
+ fail(`unsupported MCP target "${flags.target}" (expected: ${MCP_TARGETS.join(', ')})`);
1397
+ }
1398
+ const scope = (flags.scope || 'project').toLowerCase();
1399
+ if (!MCP_SCOPES.includes(scope)) {
1400
+ fail(`invalid MCP install scope "${scope}" (expected: ${MCP_SCOPES.join(', ')})`);
1401
+ }
1402
+ const root = repositoryRoot(flags.root || process.cwd());
1403
+ const { configDir, configFile } = mcpConfigLocation(target, scope, root);
1404
+ return {
1405
+ target,
1406
+ targetLabel: MCP_TARGET_LABELS[target],
1407
+ scope,
1408
+ root,
1409
+ force: Boolean(flags.force),
1410
+ configDir,
1411
+ configFile,
1412
+ };
1413
+ }
1414
+
1415
+ function tomlString(value) {
1416
+ return JSON.stringify(String(value));
1417
+ }
1418
+
1419
+ function codexMcpSection(root, eol = '\n') {
1420
+ return [
1421
+ '[mcp_servers.driftseal]',
1422
+ 'command = "driftseal-mcp"',
1423
+ `args = ["--root", ${tomlString(root)}]`,
1424
+ ].join(eol);
1425
+ }
1426
+
1427
+ function codexMcpSectionRange(content) {
1428
+ const header = /^[ \t]*\[mcp_servers\.(?:driftseal|"driftseal"|'driftseal')\][ \t]*(?:#.*)?\r?$/gm;
1429
+ const matches = [...content.matchAll(header)];
1430
+ if (matches.length > 1) fail('Codex config contains duplicate mcp_servers.driftseal tables');
1431
+ if (matches.length === 0) return null;
1432
+
1433
+ const start = matches[0].index;
1434
+ const nextTable = /^[ \t]*\[[^\r\n]+\][ \t]*(?:#.*)?\r?$/gm;
1435
+ nextTable.lastIndex = start + matches[0][0].length;
1436
+ const next = nextTable.exec(content);
1437
+ return { start, end: next ? next.index : content.length };
1438
+ }
1439
+
1440
+ function installCodexMcp(request) {
1441
+ const { configDir, configFile, force, root, scope, target, targetLabel } = request;
1442
+ const existed = fs.existsSync(configFile);
1443
+ const current = existed ? fs.readFileSync(configFile, 'utf8') : '';
1444
+ const eol = current.includes('\r\n') ? '\r\n' : '\n';
1445
+ const section = codexMcpSection(root, eol);
1446
+ const range = codexMcpSectionRange(current);
1447
+ let updated;
1448
+
1449
+ if (!range) {
1450
+ const separator =
1451
+ current.length === 0
1452
+ ? ''
1453
+ : current.endsWith(eol + eol)
1454
+ ? ''
1455
+ : current.endsWith(eol)
1456
+ ? eol
1457
+ : eol + eol;
1458
+ updated = current + separator + section + eol;
1459
+ } else {
1460
+ const existingSection = current.slice(range.start, range.end).trim();
1461
+ if (existingSection.replace(/\r\n/g, '\n') === section.replace(/\r\n/g, '\n')) {
1462
+ printLine(`DriftSeal MCP is already installed for ${targetLabel} (${scope}): ${configFile}`);
1463
+ return { changed: false, target, scope, root, configFile };
1464
+ }
1465
+ if (!force) {
1466
+ fail(
1467
+ `Codex config already defines mcp_servers.driftseal in ${configFile}; ` +
1468
+ 're-run with --force to replace that table'
1469
+ );
1470
+ }
1471
+ const trailing = range.end < current.length ? eol + eol : eol;
1472
+ updated = current.slice(0, range.start) + section + trailing + current.slice(range.end);
1473
+ }
1474
+
1475
+ ensureDirectoryDurable(configDir);
1476
+ atomicWriteFile(configFile, updated);
1477
+ printLine(`Installed DriftSeal MCP for ${targetLabel} (${scope}): ${configFile}`);
1478
+ printLine(`Repository root: ${root}`);
1479
+ return { changed: true, target, scope, root, configFile };
1480
+ }
1481
+
1482
+ function jsonObject(value) {
1483
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1484
+ }
1485
+
1486
+ function readJsonConfig(configFile, targetLabel, target) {
1487
+ if (!fs.existsSync(configFile)) {
1488
+ return target === 'opencode' ? { $schema: 'https://opencode.ai/config.json' } : {};
1489
+ }
1490
+ let parsed;
1491
+ try {
1492
+ parsed = JSON.parse(fs.readFileSync(configFile, 'utf8'));
1493
+ } catch {
1494
+ fail(`${targetLabel} config is not valid JSON: ${configFile}`);
1495
+ }
1496
+ if (!jsonObject(parsed)) fail(`${targetLabel} config must contain a JSON object: ${configFile}`);
1497
+ return parsed;
1498
+ }
1499
+
1500
+ function jsonMcpDefinition(target, root) {
1501
+ if (target === 'opencode') {
1502
+ return {
1503
+ containerKey: 'mcp',
1504
+ server: { type: 'local', command: ['driftseal-mcp', '--root', root] },
1505
+ };
1506
+ }
1507
+ return {
1508
+ containerKey: 'mcpServers',
1509
+ server: { command: 'driftseal-mcp', args: ['--root', root] },
1510
+ };
1511
+ }
1512
+
1513
+ function installJsonMcp(request) {
1514
+ const { configDir, configFile, force, root, scope, target, targetLabel } = request;
1515
+ const config = readJsonConfig(configFile, targetLabel, target);
1516
+ const { containerKey, server } = jsonMcpDefinition(target, root);
1517
+ if (config[containerKey] === undefined) config[containerKey] = {};
1518
+ if (!jsonObject(config[containerKey])) {
1519
+ fail(`${targetLabel} config field ${containerKey} must be a JSON object: ${configFile}`);
1520
+ }
1521
+
1522
+ const existing = config[containerKey].driftseal;
1523
+ if (existing !== undefined) {
1524
+ if (JSON.stringify(existing) === JSON.stringify(server)) {
1525
+ printLine(`DriftSeal MCP is already installed for ${targetLabel} (${scope}): ${configFile}`);
1526
+ return { changed: false, target, scope, root, configFile };
1527
+ }
1528
+ if (!force) {
1529
+ fail(
1530
+ `${targetLabel} config already defines the driftseal MCP server in ${configFile}; ` +
1531
+ 're-run with --force to replace that entry'
1532
+ );
1533
+ }
1534
+ }
1535
+
1536
+ config[containerKey].driftseal = server;
1537
+ ensureDirectoryDurable(configDir);
1538
+ atomicWriteFile(configFile, JSON.stringify(config, null, 2) + '\n');
1539
+ printLine(`Installed DriftSeal MCP for ${targetLabel} (${scope}): ${configFile}`);
1540
+ printLine(`Repository root: ${root}`);
1541
+ return { changed: true, target, scope, root, configFile };
1542
+ }
1543
+
1544
+ function installMcp(request) {
1545
+ return request.target === 'codex' ? installCodexMcp(request) : installJsonMcp(request);
1546
+ }
1547
+
1548
+ const SKILL_NAME = 'use-driftseal';
1549
+
1550
+ function skillInstallUsage() {
1551
+ return 'usage: driftseal skill install --target <codex|kimi-code|opencode|claude-code|cursor> [--scope project|global] [--root <repository>] [--force]';
1552
+ }
1553
+
1554
+ function skillInstallLocation(target, scope, root) {
1555
+ const home = os.homedir();
1556
+ const roots = {
1557
+ codex: {
1558
+ project: path.join(root, '.agents', 'skills'),
1559
+ global: path.join(home, '.agents', 'skills'),
1560
+ },
1561
+ 'kimi-code': {
1562
+ project: path.join(root, '.kimi', 'skills'),
1563
+ global: path.join(home, '.kimi', 'skills'),
1564
+ },
1565
+ opencode: {
1566
+ project: path.join(root, '.opencode', 'skills'),
1567
+ global: path.join(home, '.config', 'opencode', 'skills'),
1568
+ },
1569
+ 'claude-code': {
1570
+ project: path.join(root, '.claude', 'skills'),
1571
+ global: path.join(home, '.claude', 'skills'),
1572
+ },
1573
+ cursor: {
1574
+ project: path.join(root, '.cursor', 'skills'),
1575
+ global: path.join(home, '.cursor', 'skills'),
1576
+ },
1577
+ };
1578
+ const skillsDir = roots[target][scope];
1579
+ return { skillsDir, skillDir: path.join(skillsDir, SKILL_NAME) };
1580
+ }
1581
+
1582
+ function parseSkillInstallRequest(argv) {
1583
+ const [subcommand, ...rest] = argv;
1584
+ if (subcommand !== 'install') fail(skillInstallUsage());
1585
+ const { positionals, flags } = parseArgs(rest, {
1586
+ target: 'single',
1587
+ scope: 'single',
1588
+ root: 'single',
1589
+ force: 'boolean',
1590
+ });
1591
+ if (positionals.length > 0 || !flags.target) fail(skillInstallUsage());
1592
+
1593
+ const target = flags.target.toLowerCase();
1594
+ if (!MCP_TARGETS.includes(target)) {
1595
+ fail(`unsupported skill target "${flags.target}" (expected: ${MCP_TARGETS.join(', ')})`);
1596
+ }
1597
+ const scope = (flags.scope || 'project').toLowerCase();
1598
+ if (!MCP_SCOPES.includes(scope)) {
1599
+ fail(`invalid skill install scope "${scope}" (expected: ${MCP_SCOPES.join(', ')})`);
1600
+ }
1601
+ const root = repositoryRoot(flags.root || process.cwd());
1602
+ return {
1603
+ target,
1604
+ targetLabel: MCP_TARGET_LABELS[target],
1605
+ scope,
1606
+ root,
1607
+ force: Boolean(flags.force),
1608
+ ...skillInstallLocation(target, scope, root),
1609
+ };
1610
+ }
1611
+
1612
+ function directoryDigest(directory) {
1613
+ if (!fs.existsSync(directory)) return null;
1614
+ const digest = crypto.createHash('sha256');
1615
+
1616
+ function visit(current, relative) {
1617
+ const stat = fs.lstatSync(current);
1618
+ if (stat.isDirectory()) {
1619
+ digest.update(`directory\0${relative}\0${stat.mode & 0o777}\0`);
1620
+ for (const name of fs.readdirSync(current).sort()) {
1621
+ visit(path.join(current, name), relative ? path.join(relative, name) : name);
1622
+ }
1623
+ return;
1624
+ }
1625
+ if (stat.isFile()) {
1626
+ digest.update(`file\0${relative}\0${stat.mode & 0o777}\0`);
1627
+ digest.update(fs.readFileSync(current));
1628
+ digest.update('\0');
1629
+ return;
1630
+ }
1631
+ if (stat.isSymbolicLink()) {
1632
+ digest.update(`symlink\0${relative}\0${fs.readlinkSync(current)}\0`);
1633
+ return;
1634
+ }
1635
+ digest.update(`other\0${relative}\0${stat.mode}\0`);
1636
+ }
1637
+
1638
+ visit(directory, '');
1639
+ return digest.digest('hex');
1640
+ }
1641
+
1642
+ function installSkill(request) {
1643
+ const { force, root, scope, skillDir, skillsDir, target, targetLabel } = request;
1644
+ const sourceDir = path.join(__dirname, '..', 'skills', SKILL_NAME);
1645
+ if (!fs.existsSync(path.join(sourceDir, 'SKILL.md'))) {
1646
+ fail(`bundled ${SKILL_NAME} skill is missing from this DriftSeal installation: ${sourceDir}`);
1647
+ }
1648
+
1649
+ const sourceDigest = directoryDigest(sourceDir);
1650
+ const existingDigest = directoryDigest(skillDir);
1651
+ if (existingDigest === sourceDigest) {
1652
+ printLine(`${SKILL_NAME} skill is already installed for ${targetLabel} (${scope}): ${skillDir}`);
1653
+ return { changed: false, target, scope, root, skillDir };
1654
+ }
1655
+ if (existingDigest !== null && !force) {
1656
+ fail(
1657
+ `${targetLabel} already has a different ${SKILL_NAME} skill at ${skillDir}; ` +
1658
+ 're-run with --force to replace it'
1659
+ );
1660
+ }
1661
+
1662
+ ensureDirectoryDurable(skillsDir);
1663
+ const suffix = `${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
1664
+ const temporary = path.join(skillsDir, `.${SKILL_NAME}.tmp-${suffix}`);
1665
+ const backup = path.join(skillsDir, `.${SKILL_NAME}.backup-${suffix}`);
1666
+ let movedExisting = false;
1667
+ try {
1668
+ fs.cpSync(sourceDir, temporary, { recursive: true, errorOnExist: true });
1669
+ if (existingDigest !== null) {
1670
+ fs.renameSync(skillDir, backup);
1671
+ movedExisting = true;
1672
+ }
1673
+ fs.renameSync(temporary, skillDir);
1674
+ if (movedExisting) fs.rmSync(backup, { recursive: true, force: true });
1675
+ fsyncDirectory(skillsDir);
1676
+ } catch (err) {
1677
+ try {
1678
+ fs.rmSync(temporary, { recursive: true, force: true });
1679
+ if (movedExisting && !fs.existsSync(skillDir) && fs.existsSync(backup)) {
1680
+ fs.renameSync(backup, skillDir);
1681
+ }
1682
+ } catch {}
1683
+ throw err;
1684
+ }
1685
+
1686
+ printLine(`Installed ${SKILL_NAME} skill for ${targetLabel} (${scope}): ${skillDir}`);
1687
+ if (scope === 'project') printLine(`Repository root: ${root}`);
1688
+ return { changed: true, target, scope, root, skillDir };
1689
+ }
1690
+
1691
+ const HOOK_TARGETS = ['kimi-code', 'claude-code', 'codex'];
1692
+ const HOOK_EVENTS = ['prompt', 'stop'];
1693
+ const HOOK_EVENT_NAMES = { prompt: 'UserPromptSubmit', stop: 'Stop' };
1694
+
1695
+ function hookUsage() {
1696
+ return (
1697
+ 'usage: driftseal hook install --target <kimi-code|claude-code|codex> [--scope project|global] [--root <repository>] [--force]\n' +
1698
+ ' driftseal hook prompt|stop [--format plain|claude-code]'
1699
+ );
1700
+ }
1701
+
1702
+ function hookConfigLocation(target, scope, root) {
1703
+ const home = os.homedir();
1704
+ if (target === 'kimi-code') {
1705
+ const userDir = process.env.KIMI_CODE_HOME
1706
+ ? path.resolve(process.env.KIMI_CODE_HOME)
1707
+ : path.join(home, '.kimi-code');
1708
+ if (scope === 'project') {
1709
+ fail('Kimi Code hooks support only global scope; re-run with --scope global');
1710
+ }
1711
+ const configDir = userDir;
1712
+ return { configDir, configFile: path.join(configDir, 'config.toml') };
1713
+ }
1714
+ if (target === 'claude-code') {
1715
+ const configDir = scope === 'project' ? path.join(root, '.claude') : path.join(home, '.claude');
1716
+ return { configDir, configFile: path.join(configDir, 'settings.json') };
1717
+ }
1718
+ if (target === 'codex') {
1719
+ const configDir = scope === 'project' ? path.join(root, '.codex') : path.join(home, '.codex');
1720
+ return { configDir, configFile: path.join(configDir, 'hooks.json') };
1721
+ }
1722
+ fail(`unsupported hook target "${target}"`);
1723
+ }
1724
+
1725
+ function parseHookInstallRequest(argv) {
1726
+ const { positionals, flags } = parseArgs(argv, {
1727
+ target: 'single',
1728
+ scope: 'single',
1729
+ root: 'single',
1730
+ force: 'boolean',
1731
+ });
1732
+ if (positionals.length > 0 || !flags.target) {
1733
+ fail(hookUsage());
1734
+ }
1735
+
1736
+ const target = flags.target.toLowerCase();
1737
+ if (!HOOK_TARGETS.includes(target)) {
1738
+ fail(`unsupported hook target "${flags.target}" (expected: ${HOOK_TARGETS.join(', ')})`);
1739
+ }
1740
+ const scope = (flags.scope || 'project').toLowerCase();
1741
+ if (!MCP_SCOPES.includes(scope)) {
1742
+ fail(`invalid hook install scope "${scope}" (expected: ${MCP_SCOPES.join(', ')})`);
1743
+ }
1744
+ const root = repositoryRoot(flags.root || process.cwd());
1745
+ const { configDir, configFile } = hookConfigLocation(target, scope, root);
1746
+ return {
1747
+ target,
1748
+ targetLabel: MCP_TARGET_LABELS[target],
1749
+ scope,
1750
+ root,
1751
+ force: Boolean(flags.force),
1752
+ configDir,
1753
+ configFile,
1754
+ };
1755
+ }
1756
+
1757
+ function hookCommand(event, format) {
1758
+ return format === 'plain' ? `driftseal hook ${event}` : `driftseal hook ${event} --format ${format}`;
1759
+ }
1760
+
1761
+ function kimiHookSection(eol = '\n') {
1762
+ const blocks = HOOK_EVENTS.map((event) =>
1763
+ [
1764
+ '[[hooks]]',
1765
+ `event = "${HOOK_EVENT_NAMES[event]}"`,
1766
+ `command = ${tomlString(hookCommand(event, 'plain'))}`,
1767
+ 'timeout = 5',
1768
+ ].join(eol)
1769
+ );
1770
+ return blocks.join(eol + eol);
1771
+ }
1772
+
1773
+ /** Ranges of [[hooks]] tables whose command invokes driftseal hook. */
1774
+ function driftsealHookBlockRanges(content) {
1775
+ const tables = [
1776
+ ...content.matchAll(/^[ \t]*\[(?:\[[^\]\r\n]+\]|[^\]\r\n]+)\][ \t]*(?:#.*)?\r?$/gm),
1777
+ ];
1778
+ const ranges = [];
1779
+ for (let index = 0; index < tables.length; index++) {
1780
+ const start = tables[index].index;
1781
+ const end = index + 1 < tables.length ? tables[index + 1].index : content.length;
1782
+ const body = content.slice(start, end);
1783
+ if (
1784
+ /^[ \t]*\[\[hooks\]\]/.test(tables[index][0]) &&
1785
+ /^[ \t]*command[ \t]*=[ \t]*["']driftseal\s+hook\s/m.test(body)
1786
+ ) {
1787
+ ranges.push({ start, end, body });
1788
+ }
1789
+ }
1790
+ return ranges;
1791
+ }
1792
+
1793
+ function installKimiHook(request) {
1794
+ const { configDir, configFile, force, scope, target, targetLabel } = request;
1795
+ const existed = fs.existsSync(configFile);
1796
+ const current = existed ? fs.readFileSync(configFile, 'utf8') : '';
1797
+ const eol = current.includes('\r\n') ? '\r\n' : '\n';
1798
+ const section = kimiHookSection(eol);
1799
+ const ranges = driftsealHookBlockRanges(current);
1800
+ let updated;
1801
+
1802
+ if (ranges.length === 0) {
1803
+ const separator =
1804
+ current.length === 0
1805
+ ? ''
1806
+ : current.endsWith(eol + eol)
1807
+ ? ''
1808
+ : current.endsWith(eol)
1809
+ ? eol
1810
+ : eol + eol;
1811
+ updated = current + separator + section + eol;
1812
+ } else {
1813
+ const existing = ranges
1814
+ .map((range) => range.body.trim().replace(/\r\n/g, '\n'))
1815
+ .join('\n\n');
1816
+ if (ranges.length === HOOK_EVENTS.length && existing === section.replace(/\r\n/g, '\n')) {
1817
+ printLine(`DriftSeal hooks are already installed for ${targetLabel} (${scope}): ${configFile}`);
1818
+ return { changed: false, target, scope, configFile };
1819
+ }
1820
+ if (!force) {
1821
+ fail(
1822
+ `${targetLabel} config already defines driftseal hooks in ${configFile}; ` +
1823
+ 're-run with --force to replace those entries'
1824
+ );
1825
+ }
1826
+ let stripped = current;
1827
+ for (const range of [...ranges].reverse()) {
1828
+ stripped = stripped.slice(0, range.start) + stripped.slice(range.end);
1829
+ }
1830
+ stripped = stripped.replace(new RegExp(`(?:\\r?\\n){3,}$`), eol + eol);
1831
+ const separator =
1832
+ stripped.length === 0 || stripped.endsWith(eol + eol)
1833
+ ? ''
1834
+ : stripped.endsWith(eol)
1835
+ ? eol
1836
+ : eol + eol;
1837
+ updated = stripped + separator + section + eol;
1838
+ }
1839
+
1840
+ ensureDirectoryDurable(configDir);
1841
+ atomicWriteFile(configFile, updated);
1842
+ printLine(`Installed DriftSeal hooks for ${targetLabel} (${scope}): ${configFile}`);
1843
+ return { changed: true, target, scope, configFile };
1844
+ }
1845
+
1846
+ /**
1847
+ * Hook groups per JSON-config target. Codex gets only the prompt hook. Claude
1848
+ * Code also gets Stop, but its output is a UI-only systemMessage so the hook
1849
+ * does not force another model turn.
1850
+ */
1851
+ function jsonHookGroups(target) {
1852
+ if (target === 'codex') {
1853
+ return {
1854
+ [HOOK_EVENT_NAMES.prompt]: [
1855
+ { hooks: [{ type: 'command', command: hookCommand('prompt', 'plain') }] },
1856
+ ],
1857
+ };
1858
+ }
1859
+ const groups = {};
1860
+ for (const event of HOOK_EVENTS) {
1861
+ groups[HOOK_EVENT_NAMES[event]] = [
1862
+ { hooks: [{ type: 'command', command: hookCommand(event, 'claude-code') }] },
1863
+ ];
1864
+ }
1865
+ return groups;
1866
+ }
1867
+
1868
+ /** True when every hook group in the list is one of ours. */
1869
+ function isDriftsealHookGroup(group) {
1870
+ return (
1871
+ jsonObject(group) &&
1872
+ Array.isArray(group.hooks) &&
1873
+ group.hooks.some(
1874
+ (hook) => jsonObject(hook) && typeof hook.command === 'string' && /\bdriftseal hook\s/.test(hook.command)
1875
+ )
1876
+ );
1877
+ }
1878
+
1879
+ function installJsonHook(request) {
1880
+ const { configDir, configFile, force, scope, target, targetLabel } = request;
1881
+ const config = readJsonConfig(configFile, targetLabel, target);
1882
+ if (config.hooks === undefined) config.hooks = {};
1883
+ if (!jsonObject(config.hooks)) {
1884
+ fail(`${targetLabel} config field hooks must be a JSON object: ${configFile}`);
1885
+ }
1886
+
1887
+ const groups = jsonHookGroups(target);
1888
+ let changed = false;
1889
+ let conflict = false;
1890
+ for (const eventName of Object.keys(groups)) {
1891
+ const existing = config.hooks[eventName];
1892
+ if (existing === undefined) {
1893
+ changed = true;
1894
+ continue;
1895
+ }
1896
+ if (!Array.isArray(existing)) {
1897
+ fail(`${targetLabel} config field hooks.${eventName} must be an array: ${configFile}`);
1898
+ }
1899
+ const ours = existing.filter(isDriftsealHookGroup);
1900
+ if (ours.length === 0) {
1901
+ changed = true;
1902
+ } else if (ours.some((group) => JSON.stringify(group) !== JSON.stringify(groups[eventName][0]))) {
1903
+ changed = true;
1904
+ conflict = true;
1905
+ }
1906
+ }
1907
+
1908
+ if (!changed) {
1909
+ printLine(`DriftSeal hooks are already installed for ${targetLabel} (${scope}): ${configFile}`);
1910
+ return { changed: false, target, scope, configFile };
1911
+ }
1912
+ if (conflict && !force) {
1913
+ fail(
1914
+ `${targetLabel} config already defines driftseal hooks in ${configFile}; ` +
1915
+ 're-run with --force to replace those entries'
1916
+ );
1917
+ }
1918
+
1919
+ for (const [eventName, group] of Object.entries(groups)) {
1920
+ const existing = config.hooks[eventName] || [];
1921
+ config.hooks[eventName] = [...existing.filter((entry) => !isDriftsealHookGroup(entry)), ...group];
1922
+ }
1923
+ ensureDirectoryDurable(configDir);
1924
+ atomicWriteFile(configFile, JSON.stringify(config, null, 2) + '\n');
1925
+ printLine(`Installed DriftSeal hooks for ${targetLabel} (${scope}): ${configFile}`);
1926
+ return { changed: true, target, scope, configFile };
1927
+ }
1928
+
1929
+ function installHook(request) {
1930
+ return request.target === 'kimi-code' ? installKimiHook(request) : installJsonHook(request);
1931
+ }
1932
+
1933
+ function hookLogFile() {
1934
+ if (process.env.DRIFTSEAL_HOME) {
1935
+ const configured = logFile();
1936
+ return fs.existsSync(configured) ? configured : null;
1937
+ }
1938
+ let current = path.resolve(process.cwd());
1939
+ while (true) {
1940
+ const candidate = path.join(current, '.intent-log', 'events.jsonl');
1941
+ if (fs.existsSync(candidate)) return candidate;
1942
+ const parent = path.dirname(current);
1943
+ if (parent === current) return null;
1944
+ current = parent;
1945
+ }
1946
+ }
1947
+
1948
+ /** Advisory reminder text; null when no ancestor has an intent log yet. */
1949
+ function hookReminder(event) {
1950
+ const file = hookLogFile();
1951
+ if (!file) return null;
1952
+ if (event === 'prompt') {
1953
+ return (
1954
+ 'DriftSeal reminder: if this round will modify files or anything else that may need a ' +
1955
+ 'rollback, begin an intent first: driftseal begin "<intent>" --verify "<check>". ' +
1956
+ 'Questions, read-only exploration, and single-step checks need no intent — skip this ' +
1957
+ 'reminder when it does not apply.'
1958
+ );
1959
+ }
1960
+ const open = openIntent(fold(readEvents({ file })));
1961
+ if (open) {
1962
+ return (
1963
+ `DriftSeal reminder: intent ${open.id} is still in_progress: "${open.intent}". ` +
1964
+ 'If its work is done, run the declared verification and close it with driftseal end; ' +
1965
+ 'if this turn was unrelated, ignore this reminder.'
1966
+ );
1967
+ }
1968
+ return (
1969
+ 'DriftSeal reminder: no intent is open. If this round changed files without one, consider ' +
1970
+ 'whether the work should have been logged; ignore this reminder when nothing changed.'
1971
+ );
1972
+ }
1973
+
1974
+ function runHookReminder(event, argv) {
1975
+ const { positionals, flags } = parseArgs(argv, { format: 'single' });
1976
+ if (positionals.length > 0) fail(hookUsage());
1977
+ const format = (flags.format || 'plain').toLowerCase();
1978
+ if (!['plain', 'claude-code'].includes(format)) {
1979
+ fail(`unsupported hook output format "${flags.format}" (expected: plain, claude-code)`);
1980
+ }
1981
+
1982
+ // Hooks must never block the agent: any failure exits quietly with no output.
1983
+ let reminder = null;
1984
+ try {
1985
+ reminder = hookReminder(event);
1986
+ } catch {
1987
+ reminder = null;
1988
+ }
1989
+ if (reminder === null) return { changed: false, event, format };
1990
+ if (format === 'claude-code') {
1991
+ const output =
1992
+ event === 'stop'
1993
+ ? { systemMessage: reminder }
1994
+ : {
1995
+ hookSpecificOutput: {
1996
+ hookEventName: HOOK_EVENT_NAMES[event],
1997
+ additionalContext: reminder,
1998
+ },
1999
+ };
2000
+ printLine(JSON.stringify(output));
2001
+ } else {
2002
+ printLine(reminder);
2003
+ }
2004
+ return { changed: true, event, format };
2005
+ }
2006
+
1322
2007
  const commands = {
1323
2008
  begin(argv) {
1324
2009
  const { positionals, flags } = parseArgs(argv, {
@@ -1719,6 +2404,26 @@ const commands = {
1719
2404
  fail('usage: driftseal decision add|update|list|show (run: driftseal help)');
1720
2405
  },
1721
2406
 
2407
+ mcp(argv) {
2408
+ const request = parseMcpInstallRequest(argv);
2409
+ return installMcp(request);
2410
+ },
2411
+
2412
+ skill(argv) {
2413
+ return installSkill(parseSkillInstallRequest(argv));
2414
+ },
2415
+
2416
+ hook(argv) {
2417
+ const [subcommand, ...rest] = argv;
2418
+ if (subcommand === 'install') {
2419
+ return installHook(parseHookInstallRequest(rest));
2420
+ }
2421
+ if (HOOK_EVENTS.includes(subcommand)) {
2422
+ return runHookReminder(subcommand, rest);
2423
+ }
2424
+ fail(hookUsage());
2425
+ },
2426
+
1722
2427
  init(argv) {
1723
2428
  const { positionals } = parseArgs(argv, {});
1724
2429
  if (positionals.length > 0) fail('usage: driftseal init');
@@ -1741,6 +2446,7 @@ const commands = {
1741
2446
  protocolEol(previousIntentProtocolBlock(4), eol),
1742
2447
  protocolEol(previousIntentProtocolBlock(5), eol),
1743
2448
  protocolEol(previousIntentProtocolBlock(6), eol),
2449
+ protocolEol(previousIntentProtocolBlock(7), eol),
1744
2450
  ],
1745
2451
  knownLegacyBlocks: [protocolEol(legacyIntentProtocolBlock(), eol)],
1746
2452
  });
@@ -1757,6 +2463,7 @@ const commands = {
1757
2463
  protocolEol(previousDecisionProtocolBlock(4), eol),
1758
2464
  protocolEol(previousDecisionProtocolBlock(5), eol),
1759
2465
  protocolEol(previousDecisionProtocolBlock(6), eol),
2466
+ protocolEol(previousDecisionProtocolBlock(7), eol),
1760
2467
  ],
1761
2468
  knownLegacyBlocks: [protocolEol(legacyDecisionProtocolBlock(), eol)],
1762
2469
  });
@@ -1805,6 +2512,17 @@ usage:
1805
2512
  driftseal decision list [--status STATUS] [--last N | --count]
1806
2513
  list or count filtered MADR decision records
1807
2514
  driftseal decision show <id> print one MADR decision record
2515
+ driftseal skill install --target TARGET [--scope project|global] [--root <repository>] [--force]
2516
+ install the bundled use-driftseal skill (default: project)
2517
+ targets: codex, kimi-code, opencode, claude-code, cursor
2518
+ driftseal mcp install --target TARGET [--scope project|global] [--root <repository>] [--force]
2519
+ install the repository-pinned MCP server (default: project)
2520
+ targets: codex, kimi-code, opencode, claude-code, cursor
2521
+ driftseal hook install --target TARGET [--scope project|global] [--root <repository>] [--force]
2522
+ install advisory lifecycle hooks (default: project)
2523
+ targets: kimi-code (global only), claude-code, codex (prompt only)
2524
+ driftseal hook prompt|stop [--format plain|claude-code]
2525
+ emit the reminder a lifecycle hook injects; never blocks
1808
2526
  driftseal init inject intent and decision protocols into ./AGENTS.md
1809
2527
  driftseal help
1810
2528
 
@@ -1829,6 +2547,9 @@ function requestedEndStatus(argv) {
1829
2547
  }
1830
2548
 
1831
2549
  function mutationResources(cmd, argv) {
2550
+ if (cmd === 'skill') return [parseSkillInstallRequest(argv).skillsDir];
2551
+ if (cmd === 'mcp') return [parseMcpInstallRequest(argv).configDir];
2552
+ if (cmd === 'hook') return [parseHookInstallRequest(argv.slice(1)).configDir];
1832
2553
  if (cmd === 'init') return [process.cwd()];
1833
2554
  if (cmd === 'reclaim' || cmd === 'unreclaim') return [logDir()];
1834
2555
  if (cmd === 'begin' && !argv.some((arg) => arg === '--decision' || arg.startsWith('--decision='))) {
@@ -1848,7 +2569,8 @@ function dispatch(argv) {
1848
2569
  const fn = commands[cmd];
1849
2570
  if (!fn) fail(`unknown command: ${cmd} (run: driftseal help)`);
1850
2571
  const mutates =
1851
- ['begin', 'end', 'init', 'reclaim', 'unreclaim'].includes(cmd) ||
2572
+ ['begin', 'end', 'init', 'skill', 'mcp', 'reclaim', 'unreclaim'].includes(cmd) ||
2573
+ (cmd === 'hook' && rest[0] === 'install') ||
1852
2574
  (cmd === 'decision' && ['add', 'update'].includes(rest[0]));
1853
2575
  const readsIntentLog = ['status', 'log'].includes(cmd);
1854
2576
  if (mutates || readsIntentLog) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftseal",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Seal intent, verification, and decisions into an auditable workflow for agentic coding",
5
5
  "keywords": [
6
6
  "driftseal",
@@ -21,7 +21,7 @@
21
21
  "url": "git+https://github.com/rowan-hiro/DriftSeal.git"
22
22
  },
23
23
  "license": "MIT",
24
- "author": "Ruan Boyu <boyuruan@gmail.com>",
24
+ "author": "Hiro <rowan_hiro@proton.me>",
25
25
  "type": "commonjs",
26
26
  "main": "index.js",
27
27
  "bin": {
@@ -1,66 +1,41 @@
1
1
  ---
2
2
  name: use-driftseal
3
- description: Run repository work through the DriftSeal (`driftseal`) intent, verification, and selective decision workflow. Use when the user asks to use DriftSeal, invokes the skill by name, or works in a repository whose instructions require `driftseal`; also use to resume a DriftSeal-managed task after context loss, reconcile scope changes, close work honestly, or preserve decision context that Git and the intent log cannot recover.
3
+ description: Follow DriftSeal-managed repository work when AGENTS.md requires `driftseal`, the user invokes DriftSeal, or an interrupted intent must be resumed. Use this skill to locate the authoritative repository policy, re-anchor state after context loss, and choose the execution surface; prefer the `driftseal` CLI, using MCP only when the repository or user explicitly selects it.
4
4
  ---
5
5
 
6
6
  # Use DriftSeal
7
7
 
8
- This skill is the usage guide for DriftSeal: how to find it and which command
9
- or tool to reach for. The binding protocol when an intent is required, how
10
- to close one honestly, when a decision record is warranted, how reclamation
11
- works — lives in the target repository's `AGENTS.md` (injected by
12
- `driftseal init`). Follow that file; do not substitute this guide or memory
13
- for it.
8
+ Treat the target repository's applicable `AGENTS.md` as the source of truth.
9
+ This skill helps locate and resume that workflow; it does not restate or extend
10
+ the policy.
14
11
 
15
12
  ## Locate DriftSeal
16
13
 
17
- - Prefer the `driftseal_*` MCP tools when the DriftSeal MCP server is available
18
- for the target repository: `driftseal_status`, `driftseal_begin`,
19
- `driftseal_end`, `driftseal_log`, `driftseal_reclaim`, `driftseal_unreclaim`,
20
- and the `driftseal_decision_*` tools. Their input schemas come from the MCP
21
- client, not from `driftseal help`. The server keeps state in its fixed
22
- repository root and ignores storage-override environment variables.
23
- - Otherwise prefer `driftseal` from `PATH`, where `DRIFTSEAL_HOME` and
24
- `DRIFTSEAL_DECISION_HOME` overrides apply. In a DriftSeal source checkout,
25
- fall back to `node bin/driftseal.js`.
14
+ - Read the applicable `AGENTS.md` before making changes.
15
+ - Prefer `driftseal` from `PATH`. In a DriftSeal source checkout, fall back to
16
+ `node bin/driftseal.js`.
17
+ - Use an explicitly configured, repository-pinned MCP server only when the user
18
+ or repository selects it, or when shell execution is unavailable. Its tool
19
+ schemas, not `driftseal help`, define the MCP interface.
26
20
  - Use one interface consistently within a round.
27
21
  - If DriftSeal is unavailable, limit activity to read-only discovery and report
28
22
  the blocker. Do not mutate the repository without the required log.
29
23
 
30
- ## Command Map
24
+ ## Re-anchor When Needed
31
25
 
32
- Re-anchor after context loss or uncertainty; when `status` reports an open
33
- intent, the repository protocol defines whether to resume or replace it:
26
+ After context loss or when intent state is uncertain, run:
34
27
 
35
28
  ```sh
36
29
  driftseal status
37
- driftseal log --last 3 # add --all to include reclaimed records
30
+ driftseal log --last 3
38
31
  ```
39
32
 
40
- Run each work round as the repository's protocol directs (`-v`, `-s`, `-n`,
41
- and `-r` are the short forms of `--verify`, `--status`, `--note`, and
42
- `--verify-result`):
33
+ Then resume, replace, verify, or close the intent exactly as `AGENTS.md`
34
+ requires. For command syntax, run:
43
35
 
44
36
  ```sh
45
- driftseal begin "<objective>" --verify "<proof>" [--decision <id>]
46
- # ... do only what the intent covers ...
47
- driftseal end --status <status> --note "<what happened>" --verify-result "<proof output>"
37
+ driftseal help
48
38
  ```
49
39
 
50
- Record and reconcile decisions as the repository's decision protocol directs:
51
-
52
- ```sh
53
- driftseal decision add "<title>" --context "..." --outcome "..."
54
- driftseal decision update <id> [--status <status>] --note "<what changed or was confirmed>"
55
- driftseal decision list --status deferred
56
- ```
57
-
58
- Retire meaningless closed records as the repository's protocol directs:
59
-
60
- ```sh
61
- driftseal reclaim [id ...] --reason "<why>" [--dry-run]
62
- driftseal unreclaim <id> --reason "<why>"
63
- ```
64
-
65
- For exact flags, eligibility rules, and recovery behavior, run
66
- `driftseal help` and read the repository's `AGENTS.md`.
40
+ Do not treat this skill, MCP descriptions, or lifecycle-hook reminders as
41
+ additional policy. If they conflict with `AGENTS.md`, follow `AGENTS.md`.