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 +101 -7
- package/README.zh-CN.md +95 -7
- package/bin/driftseal.js +727 -5
- package/package.json +2 -2
- package/skills/use-driftseal/SKILL.md +18 -43
package/README.md
CHANGED
|
@@ -42,11 +42,36 @@ For local development from this checkout:
|
|
|
42
42
|
npm link
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
##
|
|
45
|
+
## Recommended agent setup
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Use `AGENTS.md` + the companion skill + the CLI as the default integration:
|
|
48
48
|
|
|
49
|
-
|
|
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
|
-
|
|
87
|
+
Install the server into the current repository's agent config with one of the
|
|
88
|
+
supported targets:
|
|
63
89
|
|
|
64
90
|
```sh
|
|
65
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
##
|
|
45
|
+
## 推荐的 agent 配置
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
默认组合是 `AGENTS.md` + 配套 skill + CLI:
|
|
48
48
|
|
|
49
|
-
|
|
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
|
-
|
|
86
|
+
选择对应 target,即可把 server 安装到当前 repository 的 agent 配置:
|
|
63
87
|
|
|
64
88
|
```sh
|
|
65
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
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": "
|
|
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:
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
-
|
|
18
|
-
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
##
|
|
24
|
+
## Re-anchor When Needed
|
|
31
25
|
|
|
32
|
-
|
|
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
|
|
30
|
+
driftseal log --last 3
|
|
38
31
|
```
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
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`.
|