driftseal 0.5.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
@@ -112,8 +137,43 @@ The v1 server provides:
112
137
  | `driftseal://intents/recent` | Read the ten most recent intents as a JSON resource. |
113
138
  | `driftseal://decisions` | Read the decision catalog as a JSON resource. |
114
139
 
115
- The companion skill remains important: MCP supplies controlled, structured
116
- 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.
117
177
 
118
178
  ## A work round
119
179
 
@@ -151,7 +211,10 @@ Single-step commands that only build, check, or record work already done — com
151
211
  | `driftseal decision update <id> [-s status] -n "..."` | Reconcile a linked decision in the open intent. |
152
212
  | `driftseal decision list [-s status] [--last N \| --count]` | List or count decision records, optionally filtered by status. |
153
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. |
154
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. |
155
218
  | `driftseal init` | Add the adoption protocol to `AGENTS.md`. |
156
219
  | `driftseal help` | Print CLI usage. |
157
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、
@@ -110,8 +134,40 @@ v1 server 提供:
110
134
  | `driftseal://intents/recent` | 以 JSON resource 读取最近十条 intent。 |
111
135
  | `driftseal://decisions` | 以 JSON resource 读取 decision catalog。 |
112
136
 
113
- 配套 skill 仍然不可替代:MCP 提供受控、结构化的操作,skill 则告诉 agent
114
- 何时使用这些操作,以及怎样避免 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 入口。
115
171
 
116
172
  ## 一轮标准工作流
117
173
 
@@ -149,7 +205,10 @@ driftseal end \
149
205
  | `driftseal decision update <id> [-s status] -n "..."` | 在当前 intent 中 reconcile 已关联的 decision。 |
150
206
  | `driftseal decision list [-s status] [--last N \| --count]` | 列出或统计 decision records,也可按 status 筛选。 |
151
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。 |
152
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 注入的提醒;绝不阻断。 |
153
212
  | `driftseal init` | 把接入协议写入 `AGENTS.md`。 |
154
213
  | `driftseal help` | 查看 CLI 用法。 |
155
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
 
@@ -1534,6 +1545,465 @@ function installMcp(request) {
1534
1545
  return request.target === 'codex' ? installCodexMcp(request) : installJsonMcp(request);
1535
1546
  }
1536
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
+
1537
2007
  const commands = {
1538
2008
  begin(argv) {
1539
2009
  const { positionals, flags } = parseArgs(argv, {
@@ -1939,6 +2409,21 @@ const commands = {
1939
2409
  return installMcp(request);
1940
2410
  },
1941
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
+
1942
2427
  init(argv) {
1943
2428
  const { positionals } = parseArgs(argv, {});
1944
2429
  if (positionals.length > 0) fail('usage: driftseal init');
@@ -1961,6 +2446,7 @@ const commands = {
1961
2446
  protocolEol(previousIntentProtocolBlock(4), eol),
1962
2447
  protocolEol(previousIntentProtocolBlock(5), eol),
1963
2448
  protocolEol(previousIntentProtocolBlock(6), eol),
2449
+ protocolEol(previousIntentProtocolBlock(7), eol),
1964
2450
  ],
1965
2451
  knownLegacyBlocks: [protocolEol(legacyIntentProtocolBlock(), eol)],
1966
2452
  });
@@ -1977,6 +2463,7 @@ const commands = {
1977
2463
  protocolEol(previousDecisionProtocolBlock(4), eol),
1978
2464
  protocolEol(previousDecisionProtocolBlock(5), eol),
1979
2465
  protocolEol(previousDecisionProtocolBlock(6), eol),
2466
+ protocolEol(previousDecisionProtocolBlock(7), eol),
1980
2467
  ],
1981
2468
  knownLegacyBlocks: [protocolEol(legacyDecisionProtocolBlock(), eol)],
1982
2469
  });
@@ -2025,9 +2512,17 @@ usage:
2025
2512
  driftseal decision list [--status STATUS] [--last N | --count]
2026
2513
  list or count filtered MADR decision records
2027
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
2028
2518
  driftseal mcp install --target TARGET [--scope project|global] [--root <repository>] [--force]
2029
2519
  install the repository-pinned MCP server (default: project)
2030
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
2031
2526
  driftseal init inject intent and decision protocols into ./AGENTS.md
2032
2527
  driftseal help
2033
2528
 
@@ -2052,7 +2547,9 @@ function requestedEndStatus(argv) {
2052
2547
  }
2053
2548
 
2054
2549
  function mutationResources(cmd, argv) {
2550
+ if (cmd === 'skill') return [parseSkillInstallRequest(argv).skillsDir];
2055
2551
  if (cmd === 'mcp') return [parseMcpInstallRequest(argv).configDir];
2552
+ if (cmd === 'hook') return [parseHookInstallRequest(argv.slice(1)).configDir];
2056
2553
  if (cmd === 'init') return [process.cwd()];
2057
2554
  if (cmd === 'reclaim' || cmd === 'unreclaim') return [logDir()];
2058
2555
  if (cmd === 'begin' && !argv.some((arg) => arg === '--decision' || arg.startsWith('--decision='))) {
@@ -2072,7 +2569,8 @@ function dispatch(argv) {
2072
2569
  const fn = commands[cmd];
2073
2570
  if (!fn) fail(`unknown command: ${cmd} (run: driftseal help)`);
2074
2571
  const mutates =
2075
- ['begin', 'end', 'init', 'mcp', 'reclaim', 'unreclaim'].includes(cmd) ||
2572
+ ['begin', 'end', 'init', 'skill', 'mcp', 'reclaim', 'unreclaim'].includes(cmd) ||
2573
+ (cmd === 'hook' && rest[0] === 'install') ||
2076
2574
  (cmd === 'decision' && ['add', 'update'].includes(rest[0]));
2077
2575
  const readsIntentLog = ['status', 'log'].includes(cmd);
2078
2576
  if (mutates || readsIntentLog) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftseal",
3
- "version": "0.5.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`.