driftseal 0.3.0 → 0.5.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 +54 -4
- package/README.zh-CN.md +49 -4
- package/bin/driftseal-mcp.js +73 -3
- package/bin/driftseal.js +420 -13
- package/package.json +4 -4
- package/skills/use-driftseal/SKILL.md +39 -139
package/README.md
CHANGED
|
@@ -59,12 +59,42 @@ Fix the server to one repository when starting it:
|
|
|
59
59
|
driftseal-mcp --root /absolute/path/to/repository
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Install the server into the current repository's agent config with one of the
|
|
63
|
+
supported targets:
|
|
63
64
|
|
|
64
65
|
```sh
|
|
65
|
-
|
|
66
|
+
cd /path/to/repository
|
|
67
|
+
driftseal mcp install --target codex
|
|
68
|
+
driftseal mcp install --target kimi-code
|
|
69
|
+
driftseal mcp install --target opencode
|
|
70
|
+
driftseal mcp install --target claude-code
|
|
71
|
+
driftseal mcp install --target cursor
|
|
66
72
|
```
|
|
67
73
|
|
|
74
|
+
Project scope is the default because each DriftSeal MCP server belongs to one
|
|
75
|
+
repository. Every target pins `--root` to the repository's canonical absolute
|
|
76
|
+
path, and repeated installs are idempotent.
|
|
77
|
+
|
|
78
|
+
| Target | Project config | Global config |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `codex` | `.codex/config.toml` | `~/.codex/config.toml` |
|
|
81
|
+
| `kimi-code` | `.kimi-code/mcp.json` | `~/.kimi-code/mcp.json` or `$KIMI_CODE_HOME/mcp.json` |
|
|
82
|
+
| `opencode` | `opencode.json` | `~/.config/opencode/opencode.json` |
|
|
83
|
+
| `claude-code` | `.mcp.json` | `~/.claude.json` |
|
|
84
|
+
| `cursor` | `.cursor/mcp.json` | `~/.cursor/mcp.json` |
|
|
85
|
+
|
|
86
|
+
Use `--root <repository>` when running the installer elsewhere, or choose the
|
|
87
|
+
agent's user-level config explicitly:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
driftseal mcp install --target <target> --scope global --root /absolute/path/to/repository
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Global installs remain pinned to the selected repository. If the chosen config
|
|
94
|
+
already contains a different DriftSeal server entry, the installer leaves it
|
|
95
|
+
untouched unless `--force` is supplied. Other agent settings and MCP servers are
|
|
96
|
+
preserved.
|
|
97
|
+
|
|
68
98
|
The root is startup configuration, not a tool input. In MCP mode DriftSeal also
|
|
69
99
|
ignores inherited `DRIFTSEAL_HOME` and `DRIFTSEAL_DECISION_HOME` overrides, so a
|
|
70
100
|
tool call cannot redirect writes outside the selected repository.
|
|
@@ -75,6 +105,7 @@ The v1 server provides:
|
|
|
75
105
|
| --- | --- |
|
|
76
106
|
| `driftseal_status`, `driftseal_log` | Read the current intent and intent history. |
|
|
77
107
|
| `driftseal_begin`, `driftseal_end` | Open and honestly close a work round. |
|
|
108
|
+
| `driftseal_reclaim`, `driftseal_unreclaim` | Hide meaningless closed records behind append-only markers, or restore them. |
|
|
78
109
|
| `driftseal_decision_list`, `driftseal_decision_show` | Find and read MADR records. |
|
|
79
110
|
| `driftseal_decision_add`, `driftseal_decision_update` | Add selective decisions and reconcile linked ones. |
|
|
80
111
|
| `driftseal://intent/current` | Read the current intent as a JSON resource. |
|
|
@@ -113,11 +144,14 @@ Single-step commands that only build, check, or record work already done — com
|
|
|
113
144
|
| `driftseal begin "<intent>" [-v "<verify>"] [--decision id] [--force]` | Open a work-round intent and optionally link existing decisions. |
|
|
114
145
|
| `driftseal end [id] [-s status] [-n note] [-r verify-result]` | Close an intent honestly. |
|
|
115
146
|
| `driftseal status` | Show the intent currently in progress. |
|
|
116
|
-
| `driftseal log [-n N]` | Review intent history. |
|
|
147
|
+
| `driftseal log [-n N] [--all]` | Review intent history (`--all` includes reclaimed records). |
|
|
148
|
+
| `driftseal reclaim [id ...] --reason "..." [--older-than days] [--force] [--dry-run]` | Hide meaningless closed records behind append-only markers. |
|
|
149
|
+
| `driftseal unreclaim <id> --reason "..."` | Restore a reclaimed record to the visible log. |
|
|
117
150
|
| `driftseal decision add "<title>" -c "..." -o "..."` | Write a numbered MADR decision. |
|
|
118
151
|
| `driftseal decision update <id> [-s status] -n "..."` | Reconcile a linked decision in the open intent. |
|
|
119
152
|
| `driftseal decision list [-s status] [--last N \| --count]` | List or count decision records, optionally filtered by status. |
|
|
120
153
|
| `driftseal decision show <id>` | Read one decision record. |
|
|
154
|
+
| `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. |
|
|
121
155
|
| `driftseal init` | Add the adoption protocol to `AGENTS.md`. |
|
|
122
156
|
| `driftseal help` | Print CLI usage. |
|
|
123
157
|
|
|
@@ -127,6 +161,22 @@ close as `completed` or `partial`. The update changes the current status when
|
|
|
127
161
|
requested and appends a timestamped history entry tied to the intent. Intents
|
|
128
162
|
without decision links keep the ordinary workflow.
|
|
129
163
|
|
|
164
|
+
## Reclaiming noise records
|
|
165
|
+
|
|
166
|
+
Some closed records stop mattering: a harness or sandbox failure is recorded
|
|
167
|
+
honestly as `failed`, but it says nothing about the project. `driftseal
|
|
168
|
+
reclaim` retires such records without rewriting history — it appends a
|
|
169
|
+
`reclaim` marker (with a mandatory `--reason`) to the same append-only log,
|
|
170
|
+
and reclaimed records disappear from `driftseal log` and `driftseal status`
|
|
171
|
+
output while remaining in `events.jsonl` and visible with `log --all`.
|
|
172
|
+
`driftseal unreclaim <id> --reason "..."` restores a record that turned out to
|
|
173
|
+
matter.
|
|
174
|
+
|
|
175
|
+
Without ids, batch mode reclaims only closed `failed`/`abandoned` records that
|
|
176
|
+
are not linked to decisions and are older than `--older-than` days (default
|
|
177
|
+
7); use `--dry-run` to preview. `completed` and `partial` records, and any
|
|
178
|
+
decision-linked record, can only be reclaimed by explicit id with `--force`.
|
|
179
|
+
|
|
130
180
|
## Consistency and recovery
|
|
131
181
|
|
|
132
182
|
DriftSeal serializes mutating commands with locks on the configured intent and
|
|
@@ -157,7 +207,7 @@ index.
|
|
|
157
207
|
|
|
158
208
|
## Storage
|
|
159
209
|
|
|
160
|
-
- `.intent-log/events.jsonl` is the append-only intent log.
|
|
210
|
+
- `.intent-log/events.jsonl` is the append-only intent log. All access goes through `driftseal` (CLI or MCP) — never read, edit, move, or delete it directly; use `driftseal reclaim` to retire meaningless records instead of deleting log lines.
|
|
161
211
|
- `.decision-log/` contains numbered MADR decision records.
|
|
162
212
|
- Set `DRIFTSEAL_HOME` or `DRIFTSEAL_DECISION_HOME` to store either log outside the current project.
|
|
163
213
|
|
package/README.zh-CN.md
CHANGED
|
@@ -59,12 +59,40 @@ atomic write、schema 和 recovery 实现。server 不会启动 `driftseal` 子
|
|
|
59
59
|
driftseal-mcp --root /absolute/path/to/repository
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
选择对应 target,即可把 server 安装到当前 repository 的 agent 配置:
|
|
63
63
|
|
|
64
64
|
```sh
|
|
65
|
-
|
|
65
|
+
cd /path/to/repository
|
|
66
|
+
driftseal mcp install --target codex
|
|
67
|
+
driftseal mcp install --target kimi-code
|
|
68
|
+
driftseal mcp install --target opencode
|
|
69
|
+
driftseal mcp install --target claude-code
|
|
70
|
+
driftseal mcp install --target cursor
|
|
66
71
|
```
|
|
67
72
|
|
|
73
|
+
默认使用项目级配置,因为每个 DriftSeal MCP server 都只属于一个 repository。
|
|
74
|
+
所有 target 都会把 `--root` 固定为 repository 的规范化绝对路径,并且可以安全地
|
|
75
|
+
重复执行。
|
|
76
|
+
|
|
77
|
+
| Target | 项目级配置 | 全局配置 |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `codex` | `.codex/config.toml` | `~/.codex/config.toml` |
|
|
80
|
+
| `kimi-code` | `.kimi-code/mcp.json` | `~/.kimi-code/mcp.json` 或 `$KIMI_CODE_HOME/mcp.json` |
|
|
81
|
+
| `opencode` | `opencode.json` | `~/.config/opencode/opencode.json` |
|
|
82
|
+
| `claude-code` | `.mcp.json` | `~/.claude.json` |
|
|
83
|
+
| `cursor` | `.cursor/mcp.json` | `~/.cursor/mcp.json` |
|
|
84
|
+
|
|
85
|
+
在其他目录执行时可以显式传入 `--root <repository>`;也可以明确选择对应 agent
|
|
86
|
+
的用户级配置:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
driftseal mcp install --target <target> --scope global --root /absolute/path/to/repository
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
全局安装仍会固定到所选 repository。如果目标配置中已经存在不同的 DriftSeal
|
|
93
|
+
server entry,安装器不会修改它;只有显式传入 `--force` 才会替换。其他 agent
|
|
94
|
+
设置与 MCP servers 会被保留。
|
|
95
|
+
|
|
68
96
|
root 只能在启动时配置,不是 tool input。MCP 模式也会忽略继承到进程中的
|
|
69
97
|
`DRIFTSEAL_HOME` 和 `DRIFTSEAL_DECISION_HOME` override,因此 tool call 不能把
|
|
70
98
|
写入重定向到所选 repository 之外。
|
|
@@ -75,6 +103,7 @@ v1 server 提供:
|
|
|
75
103
|
| --- | --- |
|
|
76
104
|
| `driftseal_status`, `driftseal_log` | 读取当前 intent 和 intent 历史。 |
|
|
77
105
|
| `driftseal_begin`, `driftseal_end` | 开启并诚实关闭一轮工作。 |
|
|
106
|
+
| `driftseal_reclaim`, `driftseal_unreclaim` | 用 append-only 标记隐藏已无意义的已关闭记录,或将其恢复。 |
|
|
78
107
|
| `driftseal_decision_list`, `driftseal_decision_show` | 查找并读取 MADR record。 |
|
|
79
108
|
| `driftseal_decision_add`, `driftseal_decision_update` | 克制地增加 decision,并 reconcile 已关联的 decision。 |
|
|
80
109
|
| `driftseal://intent/current` | 以 JSON resource 读取当前 intent。 |
|
|
@@ -113,11 +142,14 @@ driftseal end \
|
|
|
113
142
|
| `driftseal begin "<intent>" [-v "<verify>"] [--decision id] [--force]` | 开启一轮工作,并可关联已有 decision。 |
|
|
114
143
|
| `driftseal end [id] [-s status] [-n note] [-r verify-result]` | 诚实地关闭 intent。 |
|
|
115
144
|
| `driftseal status` | 查看当前进行中的 intent。 |
|
|
116
|
-
| `driftseal log [-n N]` | 查看 intent
|
|
145
|
+
| `driftseal log [-n N] [--all]` | 查看 intent 历史(`--all` 包含已回收的记录)。 |
|
|
146
|
+
| `driftseal reclaim [id ...] --reason "..." [--older-than days] [--force] [--dry-run]` | 用 append-only 标记隐藏已无意义的已关闭记录。 |
|
|
147
|
+
| `driftseal unreclaim <id> --reason "..."` | 把已回收的记录恢复到可见历史中。 |
|
|
117
148
|
| `driftseal decision add "<title>" -c "..." -o "..."` | 写入编号化的 MADR decision。 |
|
|
118
149
|
| `driftseal decision update <id> [-s status] -n "..."` | 在当前 intent 中 reconcile 已关联的 decision。 |
|
|
119
150
|
| `driftseal decision list [-s status] [--last N \| --count]` | 列出或统计 decision records,也可按 status 筛选。 |
|
|
120
151
|
| `driftseal decision show <id>` | 查看单条 decision record。 |
|
|
152
|
+
| `driftseal mcp install --target TARGET [--scope project\|global] [--root path] [--force]` | 把固定到 repository 的 MCP server 安装到 Codex、Kimi Code、OpenCode、Claude Code 或 Cursor。 |
|
|
121
153
|
| `driftseal init` | 把接入协议写入 `AGENTS.md`。 |
|
|
122
154
|
| `driftseal help` | 查看 CLI 用法。 |
|
|
123
155
|
|
|
@@ -126,6 +158,19 @@ driftseal end \
|
|
|
126
158
|
每一条关联 decision。update 可以改变当前 status,并会追加一条包含时间和
|
|
127
159
|
intent ID 的 history。没有关联 decision 的 intent 仍沿用普通流程。
|
|
128
160
|
|
|
161
|
+
## 回收已无意义的记录
|
|
162
|
+
|
|
163
|
+
有些已关闭的记录会随着时间失去意义:harness 或 sandbox 导致的失败会被如实记录为
|
|
164
|
+
`failed`,但它与项目本身无关。`driftseal reclaim` 可以在不改写历史的前提下让这类
|
|
165
|
+
记录退场——它只是向同一个 append-only log 追加一条 `reclaim` 标记(必须附带
|
|
166
|
+
`--reason`),被回收的记录会从 `driftseal log` 和 `driftseal status` 的输出中隐藏,
|
|
167
|
+
但仍保留在 `events.jsonl` 中,并可通过 `log --all` 查看。若事后发现某条记录仍然
|
|
168
|
+
重要,用 `driftseal unreclaim <id> --reason "..."` 恢复。
|
|
169
|
+
|
|
170
|
+
不带 id 的批量模式只回收已关闭、未关联 decision、且早于 `--older-than` 天(默认 7
|
|
171
|
+
天)的 `failed`/`abandoned` 记录;可以先用 `--dry-run` 预览。`completed` 和 `partial`
|
|
172
|
+
记录,以及任何关联了 decision 的记录,只能按显式 id 加 `--force` 回收。
|
|
173
|
+
|
|
129
174
|
## 一致性与恢复
|
|
130
175
|
|
|
131
176
|
DriftSeal 会对配置后的 intent log 与 decision log 根目录加锁,并按固定顺序获取这些
|
|
@@ -152,7 +197,7 @@ recovery 只处理当前 intent,因此历史冲突不会阻塞之后的 decisi
|
|
|
152
197
|
|
|
153
198
|
## 数据保存在哪里
|
|
154
199
|
|
|
155
|
-
- `.intent-log/events.jsonl`:append-only intent log
|
|
200
|
+
- `.intent-log/events.jsonl`:append-only intent log。所有读写都必须经过 `driftseal`(CLI 或 MCP)——不要直接读取、修改、移动或删除该文件;需要让无意义的记录退场时使用 `driftseal reclaim`,而不是删除日志行。
|
|
156
201
|
- `.decision-log/`:编号化的 MADR decision records。
|
|
157
202
|
- 设置 `DRIFTSEAL_HOME` 或 `DRIFTSEAL_DECISION_HOME`,即可把对应 log 放到当前项目之外。
|
|
158
203
|
|
package/bin/driftseal-mcp.js
CHANGED
|
@@ -77,6 +77,9 @@ function registerTools(server, api, z) {
|
|
|
77
77
|
verifyResult: z.string().nullable(),
|
|
78
78
|
beganAt: z.string(),
|
|
79
79
|
endedAt: z.string().nullable(),
|
|
80
|
+
reclaimed: z.boolean(),
|
|
81
|
+
reclaimReason: z.string().nullable(),
|
|
82
|
+
reclaimedAt: z.string().nullable(),
|
|
80
83
|
});
|
|
81
84
|
const decisionRecord = z.object({
|
|
82
85
|
id: z.string(),
|
|
@@ -163,18 +166,85 @@ function registerTools(server, api, z) {
|
|
|
163
166
|
{
|
|
164
167
|
title: 'List DriftSeal intent history',
|
|
165
168
|
description:
|
|
166
|
-
'Review recent or complete DriftSeal intent history to re-anchor work and understand prior outcomes.',
|
|
167
|
-
inputSchema: {
|
|
169
|
+
'Review recent or complete DriftSeal intent history to re-anchor work and understand prior outcomes. Reclaimed records are hidden unless includeReclaimed is set.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
last: z.number().int().positive().max(100).optional(),
|
|
172
|
+
includeReclaimed: z.boolean().default(false),
|
|
173
|
+
},
|
|
168
174
|
outputSchema: { root: z.string(), intents: z.array(intentRecord) },
|
|
169
175
|
annotations: readOnly,
|
|
170
176
|
},
|
|
171
177
|
async (input) =>
|
|
172
178
|
guarded(() => {
|
|
173
|
-
const intents = api.log(input);
|
|
179
|
+
const intents = api.log({ last: input.last, all: input.includeReclaimed });
|
|
174
180
|
return success({ root: api.root, intents }, `Found ${intents.length} DriftSeal intent records.`);
|
|
175
181
|
})
|
|
176
182
|
);
|
|
177
183
|
|
|
184
|
+
server.registerTool(
|
|
185
|
+
'driftseal_reclaim',
|
|
186
|
+
{
|
|
187
|
+
title: 'Reclaim DriftSeal intent records',
|
|
188
|
+
description:
|
|
189
|
+
'Hide meaningless closed intent records (for example harness- or sandbox-caused failures) by appending reclaim markers. Never deletes log lines. Without ids, reclaims failed/abandoned, decision-unlinked records older than olderThan days.',
|
|
190
|
+
inputSchema: {
|
|
191
|
+
ids: z
|
|
192
|
+
.array(z.string())
|
|
193
|
+
.default([])
|
|
194
|
+
.describe('Intent IDs to reclaim; omit for batch mode by age.'),
|
|
195
|
+
reason: nonEmpty.describe('Why these records are meaningless (required, kept in the log).'),
|
|
196
|
+
olderThan: z
|
|
197
|
+
.number()
|
|
198
|
+
.int()
|
|
199
|
+
.positive()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe('Batch mode retention window in days (default 7).'),
|
|
202
|
+
force: z
|
|
203
|
+
.boolean()
|
|
204
|
+
.default(false)
|
|
205
|
+
.describe('Allow reclaiming partial/completed or decision-linked records by explicit id.'),
|
|
206
|
+
dryRun: z.boolean().default(false),
|
|
207
|
+
},
|
|
208
|
+
outputSchema: { root: z.string(), intents: z.array(intentRecord) },
|
|
209
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
210
|
+
},
|
|
211
|
+
async (input) =>
|
|
212
|
+
guarded(() => {
|
|
213
|
+
const intents = api.reclaim({
|
|
214
|
+
ids: input.ids,
|
|
215
|
+
reason: input.reason,
|
|
216
|
+
olderThan: input.olderThan,
|
|
217
|
+
force: input.force,
|
|
218
|
+
dryRun: input.dryRun,
|
|
219
|
+
});
|
|
220
|
+
return success(
|
|
221
|
+
{ root: api.root, intents },
|
|
222
|
+
input.dryRun
|
|
223
|
+
? `${intents.length} DriftSeal intent records match.`
|
|
224
|
+
: `Reclaimed ${intents.length} DriftSeal intent records.`
|
|
225
|
+
);
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
server.registerTool(
|
|
230
|
+
'driftseal_unreclaim',
|
|
231
|
+
{
|
|
232
|
+
title: 'Restore a reclaimed DriftSeal intent record',
|
|
233
|
+
description: 'Restore one reclaimed intent record to the visible log by appending an unreclaim marker.',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
id: z.string(),
|
|
236
|
+
reason: nonEmpty.describe('Why this record is being restored (required, kept in the log).'),
|
|
237
|
+
},
|
|
238
|
+
outputSchema: { root: z.string(), intent: intentRecord },
|
|
239
|
+
annotations: localWrite,
|
|
240
|
+
},
|
|
241
|
+
async (input) =>
|
|
242
|
+
guarded(() => {
|
|
243
|
+
const intent = api.unreclaim(input);
|
|
244
|
+
return success({ root: api.root, intent }, `Restored DriftSeal intent ${intent.id}.`);
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
|
|
178
248
|
server.registerTool(
|
|
179
249
|
'driftseal_decision_list',
|
|
180
250
|
{
|
package/bin/driftseal.js
CHANGED
|
@@ -34,8 +34,8 @@ const DECISION_STATUSES = [
|
|
|
34
34
|
'deprecated',
|
|
35
35
|
'superseded',
|
|
36
36
|
];
|
|
37
|
-
const EVENT_SCHEMA_VERSION =
|
|
38
|
-
const PROTOCOL_VERSION =
|
|
37
|
+
const EVENT_SCHEMA_VERSION = 3;
|
|
38
|
+
const PROTOCOL_VERSION = 7;
|
|
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;
|
|
@@ -130,6 +130,13 @@ function normalizeEvent(event, line) {
|
|
|
130
130
|
return event;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
if (event.type === 'reclaim' || event.type === 'unreclaim') {
|
|
134
|
+
if (typeof event.reason !== 'string' || event.reason.trim().length === 0) {
|
|
135
|
+
fail(`invalid ${event.type} event on log line ${line}`);
|
|
136
|
+
}
|
|
137
|
+
return event;
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
if (
|
|
134
141
|
event.type === 'decision_reconcile' ||
|
|
135
142
|
event.type === 'decision_reconcile_prepare' ||
|
|
@@ -566,8 +573,28 @@ function fold(events) {
|
|
|
566
573
|
tsEnd: null,
|
|
567
574
|
note: null,
|
|
568
575
|
verifyResult: null,
|
|
576
|
+
reclaimed: false,
|
|
577
|
+
reclaimReason: null,
|
|
578
|
+
reclaimedAt: null,
|
|
569
579
|
});
|
|
570
580
|
order.push(ev.id);
|
|
581
|
+
} else if (ev.type === 'reclaim' || ev.type === 'unreclaim') {
|
|
582
|
+
const rec = records.get(ev.id);
|
|
583
|
+
if (!rec) fail(`${ev.type} event references unknown intent id: ${ev.id}`);
|
|
584
|
+
if (ev.type === 'reclaim') {
|
|
585
|
+
if (rec.status === 'in_progress') {
|
|
586
|
+
fail(`cannot reclaim intent ${ev.id} while it is in_progress`);
|
|
587
|
+
}
|
|
588
|
+
if (rec.reclaimed) fail(`duplicate reclaim event for intent id: ${ev.id}`);
|
|
589
|
+
rec.reclaimed = true;
|
|
590
|
+
rec.reclaimReason = ev.reason;
|
|
591
|
+
rec.reclaimedAt = ev.ts;
|
|
592
|
+
} else {
|
|
593
|
+
if (!rec.reclaimed) fail(`unreclaim event for intent id that is not reclaimed: ${ev.id}`);
|
|
594
|
+
rec.reclaimed = false;
|
|
595
|
+
rec.reclaimReason = null;
|
|
596
|
+
rec.reclaimedAt = null;
|
|
597
|
+
}
|
|
571
598
|
} else if (ev.type === 'end') {
|
|
572
599
|
const rec = records.get(ev.id);
|
|
573
600
|
if (!rec) fail(`end event references unknown intent id: ${ev.id}`);
|
|
@@ -1038,6 +1065,7 @@ function render(rec) {
|
|
|
1038
1065
|
if (rec.verifyResult) lines.push(` verify-result: ${rec.verifyResult}`);
|
|
1039
1066
|
if (rec.note) lines.push(` note: ${rec.note}`);
|
|
1040
1067
|
lines.push(` began: ${rec.tsBegin}` + (rec.tsEnd ? ` ended: ${rec.tsEnd}` : ''));
|
|
1068
|
+
if (rec.reclaimed) lines.push(` reclaimed: ${rec.reclaimReason}`);
|
|
1041
1069
|
return lines.join('\n');
|
|
1042
1070
|
}
|
|
1043
1071
|
|
|
@@ -1053,6 +1081,9 @@ function publicIntent(rec) {
|
|
|
1053
1081
|
verifyResult: rec.verifyResult,
|
|
1054
1082
|
beganAt: rec.tsBegin,
|
|
1055
1083
|
endedAt: rec.tsEnd,
|
|
1084
|
+
reclaimed: rec.reclaimed,
|
|
1085
|
+
reclaimReason: rec.reclaimReason,
|
|
1086
|
+
reclaimedAt: rec.reclaimedAt,
|
|
1056
1087
|
};
|
|
1057
1088
|
}
|
|
1058
1089
|
|
|
@@ -1102,14 +1133,38 @@ This repo uses DriftSeal (\`driftseal\`) to prevent agent drift. Every work roun
|
|
|
1102
1133
|
just-closed log finalizes that round without requiring a new intent. Any content
|
|
1103
1134
|
change made while preparing the commit does require a new intent.
|
|
1104
1135
|
4. **Re-anchor after context loss**: run \`driftseal status\` and \`driftseal log --last 3\` before
|
|
1105
|
-
doing anything else. The open intent is the source of truth
|
|
1136
|
+
doing anything else. The open intent is the source of truth: resume it when its
|
|
1137
|
+
objective still matches the current task; otherwise close it (\`partial\` or
|
|
1138
|
+
\`abandoned\`, with a note) and \`begin\` a new one.
|
|
1139
|
+
|
|
1140
|
+
**Log access goes only through DriftSeal.** Never read, edit, move, or delete
|
|
1141
|
+
\`.intent-log/events.jsonl\` (or anything under \`$DRIFTSEAL_HOME\`) directly; use
|
|
1142
|
+
\`driftseal\` commands or the MCP tools. Retire meaningless closed records with
|
|
1143
|
+
\`driftseal reclaim [id ...] --reason "<why>"\` — it appends a marker, never
|
|
1144
|
+
deletes log lines; \`driftseal unreclaim <id> --reason "<why>"\` restores one.
|
|
1106
1145
|
|
|
1107
1146
|
Log: \`.intent-log/events.jsonl\` (override with \`$DRIFTSEAL_HOME\`); commit it with the code.
|
|
1108
1147
|
${INTENT_PROTOCOL_END}`;
|
|
1109
1148
|
}
|
|
1110
1149
|
|
|
1111
1150
|
function previousIntentProtocolBlock(version) {
|
|
1112
|
-
const
|
|
1151
|
+
const v6 = intentProtocolBlock(version).replace(
|
|
1152
|
+
'doing anything else. The open intent is the source of truth: resume it when its\n' +
|
|
1153
|
+
' objective still matches the current task; otherwise close it (`partial` or\n' +
|
|
1154
|
+
' `abandoned`, with a note) and `begin` a new one.',
|
|
1155
|
+
'doing anything else. The open intent is the source of truth.'
|
|
1156
|
+
);
|
|
1157
|
+
if (version >= 6) return v6;
|
|
1158
|
+
const v5 = v6.replace(
|
|
1159
|
+
'\n**Log access goes only through DriftSeal.** Never read, edit, move, or delete\n' +
|
|
1160
|
+
'`.intent-log/events.jsonl` (or anything under `$DRIFTSEAL_HOME`) directly; use\n' +
|
|
1161
|
+
'`driftseal` commands or the MCP tools. Retire meaningless closed records with\n' +
|
|
1162
|
+
'`driftseal reclaim [id ...] --reason "<why>"` — it appends a marker, never\n' +
|
|
1163
|
+
'deletes log lines; `driftseal unreclaim <id> --reason "<why>"` restores one.\n',
|
|
1164
|
+
''
|
|
1165
|
+
);
|
|
1166
|
+
if (version >= 5) return v5;
|
|
1167
|
+
const v4 = v5.replace(
|
|
1113
1168
|
'1. **Write intent first**, before modifying, creating, or deleting files, or\n' +
|
|
1114
1169
|
' making any other change that may need a rollback:\n' +
|
|
1115
1170
|
' `driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"`.\n' +
|
|
@@ -1144,7 +1199,7 @@ revisiting, non-obvious rationale behind a long-lived or costly-to-reverse accep
|
|
|
1144
1199
|
choice, or a deprecated or superseded decision. Do not record routine, local,
|
|
1145
1200
|
readily reversible choices.
|
|
1146
1201
|
|
|
1147
|
-
\`driftseal decision add "<title>" --context "<problem and constraints>" --outcome "<decision and rationale>" --option "<considered option>" --consequence "<result>"\`
|
|
1202
|
+
\`driftseal decision add "<title>" --context "<problem and constraints>" --outcome "<decision and rationale>" --driver "<decision driver>" --option "<considered option>" --consequence "<result>"\`
|
|
1148
1203
|
|
|
1149
1204
|
Add one \`--driver\`, \`--option\`, or \`--consequence\` flag per item. Use
|
|
1150
1205
|
\`--status proposed|accepted|rejected|deferred|deprecated|superseded\` when needed.
|
|
@@ -1209,6 +1264,10 @@ When an intent declares an existing decision with \`--decision <id>\`, use
|
|
|
1209
1264
|
Commit \`.decision-log/\` with the code.`;
|
|
1210
1265
|
}
|
|
1211
1266
|
|
|
1267
|
+
function previousDecisionProtocolBlock(version) {
|
|
1268
|
+
return decisionProtocolBlock(version).replace(' --driver "<decision driver>"', '');
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1212
1271
|
function upgradeManagedBlock({
|
|
1213
1272
|
content,
|
|
1214
1273
|
marker,
|
|
@@ -1260,6 +1319,221 @@ function upgradeManagedBlock({
|
|
|
1260
1319
|
fail(`cannot safely upgrade customized protocol block beginning with ${marker}`);
|
|
1261
1320
|
}
|
|
1262
1321
|
|
|
1322
|
+
const MCP_TARGETS = ['codex', 'kimi-code', 'opencode', 'claude-code', 'cursor'];
|
|
1323
|
+
const MCP_SCOPES = ['project', 'global'];
|
|
1324
|
+
const MCP_TARGET_LABELS = {
|
|
1325
|
+
codex: 'Codex',
|
|
1326
|
+
'kimi-code': 'Kimi Code',
|
|
1327
|
+
opencode: 'OpenCode',
|
|
1328
|
+
'claude-code': 'Claude Code',
|
|
1329
|
+
cursor: 'Cursor',
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
function mcpInstallUsage() {
|
|
1333
|
+
return 'usage: driftseal mcp install --target <codex|kimi-code|opencode|claude-code|cursor> [--scope project|global] [--root <repository>] [--force]';
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function mcpConfigLocation(target, scope, root) {
|
|
1337
|
+
const home = os.homedir();
|
|
1338
|
+
if (target === 'codex') {
|
|
1339
|
+
const configDir = scope === 'project' ? path.join(root, '.codex') : path.join(home, '.codex');
|
|
1340
|
+
return { configDir, configFile: path.join(configDir, 'config.toml') };
|
|
1341
|
+
}
|
|
1342
|
+
if (target === 'kimi-code') {
|
|
1343
|
+
const userDir = process.env.KIMI_CODE_HOME
|
|
1344
|
+
? path.resolve(process.env.KIMI_CODE_HOME)
|
|
1345
|
+
: path.join(home, '.kimi-code');
|
|
1346
|
+
const configDir = scope === 'project' ? path.join(root, '.kimi-code') : userDir;
|
|
1347
|
+
return { configDir, configFile: path.join(configDir, 'mcp.json') };
|
|
1348
|
+
}
|
|
1349
|
+
if (target === 'opencode') {
|
|
1350
|
+
const configDir =
|
|
1351
|
+
scope === 'project' ? root : path.join(home, '.config', 'opencode');
|
|
1352
|
+
return { configDir, configFile: path.join(configDir, 'opencode.json') };
|
|
1353
|
+
}
|
|
1354
|
+
if (target === 'claude-code') {
|
|
1355
|
+
const configDir = scope === 'project' ? root : home;
|
|
1356
|
+
return {
|
|
1357
|
+
configDir,
|
|
1358
|
+
configFile: path.join(configDir, scope === 'project' ? '.mcp.json' : '.claude.json'),
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
if (target === 'cursor') {
|
|
1362
|
+
const configDir = scope === 'project' ? path.join(root, '.cursor') : path.join(home, '.cursor');
|
|
1363
|
+
return { configDir, configFile: path.join(configDir, 'mcp.json') };
|
|
1364
|
+
}
|
|
1365
|
+
fail(`unsupported MCP target "${target}"`);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function parseMcpInstallRequest(argv) {
|
|
1369
|
+
const [subcommand, ...rest] = argv;
|
|
1370
|
+
if (subcommand !== 'install') {
|
|
1371
|
+
fail(mcpInstallUsage());
|
|
1372
|
+
}
|
|
1373
|
+
const { positionals, flags } = parseArgs(rest, {
|
|
1374
|
+
target: 'single',
|
|
1375
|
+
scope: 'single',
|
|
1376
|
+
root: 'single',
|
|
1377
|
+
force: 'boolean',
|
|
1378
|
+
});
|
|
1379
|
+
if (positionals.length > 0 || !flags.target) {
|
|
1380
|
+
fail(mcpInstallUsage());
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const target = flags.target.toLowerCase();
|
|
1384
|
+
if (!MCP_TARGETS.includes(target)) {
|
|
1385
|
+
fail(`unsupported MCP target "${flags.target}" (expected: ${MCP_TARGETS.join(', ')})`);
|
|
1386
|
+
}
|
|
1387
|
+
const scope = (flags.scope || 'project').toLowerCase();
|
|
1388
|
+
if (!MCP_SCOPES.includes(scope)) {
|
|
1389
|
+
fail(`invalid MCP install scope "${scope}" (expected: ${MCP_SCOPES.join(', ')})`);
|
|
1390
|
+
}
|
|
1391
|
+
const root = repositoryRoot(flags.root || process.cwd());
|
|
1392
|
+
const { configDir, configFile } = mcpConfigLocation(target, scope, root);
|
|
1393
|
+
return {
|
|
1394
|
+
target,
|
|
1395
|
+
targetLabel: MCP_TARGET_LABELS[target],
|
|
1396
|
+
scope,
|
|
1397
|
+
root,
|
|
1398
|
+
force: Boolean(flags.force),
|
|
1399
|
+
configDir,
|
|
1400
|
+
configFile,
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function tomlString(value) {
|
|
1405
|
+
return JSON.stringify(String(value));
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function codexMcpSection(root, eol = '\n') {
|
|
1409
|
+
return [
|
|
1410
|
+
'[mcp_servers.driftseal]',
|
|
1411
|
+
'command = "driftseal-mcp"',
|
|
1412
|
+
`args = ["--root", ${tomlString(root)}]`,
|
|
1413
|
+
].join(eol);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function codexMcpSectionRange(content) {
|
|
1417
|
+
const header = /^[ \t]*\[mcp_servers\.(?:driftseal|"driftseal"|'driftseal')\][ \t]*(?:#.*)?\r?$/gm;
|
|
1418
|
+
const matches = [...content.matchAll(header)];
|
|
1419
|
+
if (matches.length > 1) fail('Codex config contains duplicate mcp_servers.driftseal tables');
|
|
1420
|
+
if (matches.length === 0) return null;
|
|
1421
|
+
|
|
1422
|
+
const start = matches[0].index;
|
|
1423
|
+
const nextTable = /^[ \t]*\[[^\r\n]+\][ \t]*(?:#.*)?\r?$/gm;
|
|
1424
|
+
nextTable.lastIndex = start + matches[0][0].length;
|
|
1425
|
+
const next = nextTable.exec(content);
|
|
1426
|
+
return { start, end: next ? next.index : content.length };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function installCodexMcp(request) {
|
|
1430
|
+
const { configDir, configFile, force, root, scope, target, targetLabel } = request;
|
|
1431
|
+
const existed = fs.existsSync(configFile);
|
|
1432
|
+
const current = existed ? fs.readFileSync(configFile, 'utf8') : '';
|
|
1433
|
+
const eol = current.includes('\r\n') ? '\r\n' : '\n';
|
|
1434
|
+
const section = codexMcpSection(root, eol);
|
|
1435
|
+
const range = codexMcpSectionRange(current);
|
|
1436
|
+
let updated;
|
|
1437
|
+
|
|
1438
|
+
if (!range) {
|
|
1439
|
+
const separator =
|
|
1440
|
+
current.length === 0
|
|
1441
|
+
? ''
|
|
1442
|
+
: current.endsWith(eol + eol)
|
|
1443
|
+
? ''
|
|
1444
|
+
: current.endsWith(eol)
|
|
1445
|
+
? eol
|
|
1446
|
+
: eol + eol;
|
|
1447
|
+
updated = current + separator + section + eol;
|
|
1448
|
+
} else {
|
|
1449
|
+
const existingSection = current.slice(range.start, range.end).trim();
|
|
1450
|
+
if (existingSection.replace(/\r\n/g, '\n') === section.replace(/\r\n/g, '\n')) {
|
|
1451
|
+
printLine(`DriftSeal MCP is already installed for ${targetLabel} (${scope}): ${configFile}`);
|
|
1452
|
+
return { changed: false, target, scope, root, configFile };
|
|
1453
|
+
}
|
|
1454
|
+
if (!force) {
|
|
1455
|
+
fail(
|
|
1456
|
+
`Codex config already defines mcp_servers.driftseal in ${configFile}; ` +
|
|
1457
|
+
're-run with --force to replace that table'
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
const trailing = range.end < current.length ? eol + eol : eol;
|
|
1461
|
+
updated = current.slice(0, range.start) + section + trailing + current.slice(range.end);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
ensureDirectoryDurable(configDir);
|
|
1465
|
+
atomicWriteFile(configFile, updated);
|
|
1466
|
+
printLine(`Installed DriftSeal MCP for ${targetLabel} (${scope}): ${configFile}`);
|
|
1467
|
+
printLine(`Repository root: ${root}`);
|
|
1468
|
+
return { changed: true, target, scope, root, configFile };
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function jsonObject(value) {
|
|
1472
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function readJsonConfig(configFile, targetLabel, target) {
|
|
1476
|
+
if (!fs.existsSync(configFile)) {
|
|
1477
|
+
return target === 'opencode' ? { $schema: 'https://opencode.ai/config.json' } : {};
|
|
1478
|
+
}
|
|
1479
|
+
let parsed;
|
|
1480
|
+
try {
|
|
1481
|
+
parsed = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
1482
|
+
} catch {
|
|
1483
|
+
fail(`${targetLabel} config is not valid JSON: ${configFile}`);
|
|
1484
|
+
}
|
|
1485
|
+
if (!jsonObject(parsed)) fail(`${targetLabel} config must contain a JSON object: ${configFile}`);
|
|
1486
|
+
return parsed;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function jsonMcpDefinition(target, root) {
|
|
1490
|
+
if (target === 'opencode') {
|
|
1491
|
+
return {
|
|
1492
|
+
containerKey: 'mcp',
|
|
1493
|
+
server: { type: 'local', command: ['driftseal-mcp', '--root', root] },
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
return {
|
|
1497
|
+
containerKey: 'mcpServers',
|
|
1498
|
+
server: { command: 'driftseal-mcp', args: ['--root', root] },
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function installJsonMcp(request) {
|
|
1503
|
+
const { configDir, configFile, force, root, scope, target, targetLabel } = request;
|
|
1504
|
+
const config = readJsonConfig(configFile, targetLabel, target);
|
|
1505
|
+
const { containerKey, server } = jsonMcpDefinition(target, root);
|
|
1506
|
+
if (config[containerKey] === undefined) config[containerKey] = {};
|
|
1507
|
+
if (!jsonObject(config[containerKey])) {
|
|
1508
|
+
fail(`${targetLabel} config field ${containerKey} must be a JSON object: ${configFile}`);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const existing = config[containerKey].driftseal;
|
|
1512
|
+
if (existing !== undefined) {
|
|
1513
|
+
if (JSON.stringify(existing) === JSON.stringify(server)) {
|
|
1514
|
+
printLine(`DriftSeal MCP is already installed for ${targetLabel} (${scope}): ${configFile}`);
|
|
1515
|
+
return { changed: false, target, scope, root, configFile };
|
|
1516
|
+
}
|
|
1517
|
+
if (!force) {
|
|
1518
|
+
fail(
|
|
1519
|
+
`${targetLabel} config already defines the driftseal MCP server in ${configFile}; ` +
|
|
1520
|
+
're-run with --force to replace that entry'
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
config[containerKey].driftseal = server;
|
|
1526
|
+
ensureDirectoryDurable(configDir);
|
|
1527
|
+
atomicWriteFile(configFile, JSON.stringify(config, null, 2) + '\n');
|
|
1528
|
+
printLine(`Installed DriftSeal MCP for ${targetLabel} (${scope}): ${configFile}`);
|
|
1529
|
+
printLine(`Repository root: ${root}`);
|
|
1530
|
+
return { changed: true, target, scope, root, configFile };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function installMcp(request) {
|
|
1534
|
+
return request.target === 'codex' ? installCodexMcp(request) : installJsonMcp(request);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1263
1537
|
const commands = {
|
|
1264
1538
|
begin(argv) {
|
|
1265
1539
|
const { positionals, flags } = parseArgs(argv, {
|
|
@@ -1408,9 +1682,10 @@ const commands = {
|
|
|
1408
1682
|
},
|
|
1409
1683
|
|
|
1410
1684
|
log(argv) {
|
|
1411
|
-
const { positionals, flags } = parseArgs(argv, { last: '-n' });
|
|
1412
|
-
if (positionals.length > 0) fail('usage: driftseal log [--last N]');
|
|
1685
|
+
const { positionals, flags } = parseArgs(argv, { last: '-n', all: 'boolean' });
|
|
1686
|
+
if (positionals.length > 0) fail('usage: driftseal log [--last N] [--all]');
|
|
1413
1687
|
let records = fold(readEvents({ repairTail: true }));
|
|
1688
|
+
if (!flags.all) records = records.filter((record) => !record.reclaimed);
|
|
1414
1689
|
if (flags.last) {
|
|
1415
1690
|
const n = positiveInteger(flags.last, '--last');
|
|
1416
1691
|
records = records.slice(-n);
|
|
@@ -1423,6 +1698,109 @@ const commands = {
|
|
|
1423
1698
|
return records.map(publicIntent);
|
|
1424
1699
|
},
|
|
1425
1700
|
|
|
1701
|
+
reclaim(argv) {
|
|
1702
|
+
const { positionals, flags } = parseArgs(argv, {
|
|
1703
|
+
reason: '-r',
|
|
1704
|
+
'older-than': 'single',
|
|
1705
|
+
force: 'boolean',
|
|
1706
|
+
'dry-run': 'boolean',
|
|
1707
|
+
});
|
|
1708
|
+
const reason = flags.reason && flags.reason.trim();
|
|
1709
|
+
if (!reason) {
|
|
1710
|
+
fail(
|
|
1711
|
+
'usage: driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]'
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
let olderThanDays = 7;
|
|
1715
|
+
if (flags['older-than'] !== undefined) {
|
|
1716
|
+
olderThanDays = positiveInteger(flags['older-than'], '--older-than');
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const records = fold(readEvents({ repairTail: true }));
|
|
1720
|
+
let targets;
|
|
1721
|
+
if (positionals.length > 0) {
|
|
1722
|
+
const ids = [...new Set(positionals)];
|
|
1723
|
+
targets = ids.map((id) => {
|
|
1724
|
+
const record = records.find((candidate) => candidate.id === id);
|
|
1725
|
+
if (!record) fail(`unknown intent id: ${id}`);
|
|
1726
|
+
if (record.status === 'in_progress') {
|
|
1727
|
+
fail(`cannot reclaim intent ${id} while it is in_progress`);
|
|
1728
|
+
}
|
|
1729
|
+
if (record.reclaimed) fail(`intent ${id} is already reclaimed`);
|
|
1730
|
+
const routine = ['failed', 'abandoned'].includes(record.status) &&
|
|
1731
|
+
record.decisions.length === 0;
|
|
1732
|
+
if (!routine && !flags.force) {
|
|
1733
|
+
fail(
|
|
1734
|
+
`intent ${id} is ${record.status}` +
|
|
1735
|
+
(record.decisions.length > 0 ? ' and linked to decisions' : '') +
|
|
1736
|
+
'; re-run with --force to reclaim it anyway'
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
return record;
|
|
1740
|
+
});
|
|
1741
|
+
} else {
|
|
1742
|
+
if (flags.force) fail('--force requires explicit intent ids');
|
|
1743
|
+
const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
|
|
1744
|
+
targets = records.filter(
|
|
1745
|
+
(record) =>
|
|
1746
|
+
record.status !== 'in_progress' &&
|
|
1747
|
+
['failed', 'abandoned'].includes(record.status) &&
|
|
1748
|
+
record.decisions.length === 0 &&
|
|
1749
|
+
!record.reclaimed &&
|
|
1750
|
+
Date.parse(record.tsEnd) < cutoff
|
|
1751
|
+
);
|
|
1752
|
+
if (targets.length === 0) {
|
|
1753
|
+
printLine('no reclaimable intents');
|
|
1754
|
+
return [];
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (flags['dry-run']) {
|
|
1759
|
+
printLine(targets.map((record) => `${record.id} ${record.status} — ${record.intent}`).join('\n'));
|
|
1760
|
+
return targets.map(publicIntent);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
let events = readEvents({ repairTail: true });
|
|
1764
|
+
for (const record of targets) {
|
|
1765
|
+
events.push(
|
|
1766
|
+
appendEvent({
|
|
1767
|
+
type: 'reclaim',
|
|
1768
|
+
id: record.id,
|
|
1769
|
+
ts: new Date().toISOString(),
|
|
1770
|
+
reason,
|
|
1771
|
+
})
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
const reclaimed = fold(events).filter((record) =>
|
|
1775
|
+
targets.some((target) => target.id === record.id)
|
|
1776
|
+
);
|
|
1777
|
+
printLine(targets.map((record) => `${record.id} reclaimed`).join('\n'));
|
|
1778
|
+
return reclaimed.map(publicIntent);
|
|
1779
|
+
},
|
|
1780
|
+
|
|
1781
|
+
unreclaim(argv) {
|
|
1782
|
+
const { positionals, flags } = parseArgs(argv, { reason: '-r' });
|
|
1783
|
+
const reason = flags.reason && flags.reason.trim();
|
|
1784
|
+
if (positionals.length !== 1 || !reason) {
|
|
1785
|
+
fail('usage: driftseal unreclaim <id> --reason "<why>"');
|
|
1786
|
+
}
|
|
1787
|
+
const events = readEvents({ repairTail: true });
|
|
1788
|
+
const record = fold(events).find((candidate) => candidate.id === positionals[0]);
|
|
1789
|
+
if (!record) fail(`unknown intent id: ${positionals[0]}`);
|
|
1790
|
+
if (!record.reclaimed) fail(`intent ${positionals[0]} is not reclaimed`);
|
|
1791
|
+
events.push(
|
|
1792
|
+
appendEvent({
|
|
1793
|
+
type: 'unreclaim',
|
|
1794
|
+
id: record.id,
|
|
1795
|
+
ts: new Date().toISOString(),
|
|
1796
|
+
reason,
|
|
1797
|
+
})
|
|
1798
|
+
);
|
|
1799
|
+
const restored = fold(events).find((candidate) => candidate.id === record.id);
|
|
1800
|
+
printLine(`${record.id} unreclaimed`);
|
|
1801
|
+
return publicIntent(restored);
|
|
1802
|
+
},
|
|
1803
|
+
|
|
1426
1804
|
decision(argv) {
|
|
1427
1805
|
const [subcommand, ...rest] = argv;
|
|
1428
1806
|
if (subcommand === 'add') {
|
|
@@ -1556,6 +1934,11 @@ const commands = {
|
|
|
1556
1934
|
fail('usage: driftseal decision add|update|list|show (run: driftseal help)');
|
|
1557
1935
|
},
|
|
1558
1936
|
|
|
1937
|
+
mcp(argv) {
|
|
1938
|
+
const request = parseMcpInstallRequest(argv);
|
|
1939
|
+
return installMcp(request);
|
|
1940
|
+
},
|
|
1941
|
+
|
|
1559
1942
|
init(argv) {
|
|
1560
1943
|
const { positionals } = parseArgs(argv, {});
|
|
1561
1944
|
if (positionals.length > 0) fail('usage: driftseal init');
|
|
@@ -1576,6 +1959,8 @@ const commands = {
|
|
|
1576
1959
|
protocolEol(previousIntentProtocolBlock(2), eol),
|
|
1577
1960
|
protocolEol(previousIntentProtocolBlock(3), eol),
|
|
1578
1961
|
protocolEol(previousIntentProtocolBlock(4), eol),
|
|
1962
|
+
protocolEol(previousIntentProtocolBlock(5), eol),
|
|
1963
|
+
protocolEol(previousIntentProtocolBlock(6), eol),
|
|
1579
1964
|
],
|
|
1580
1965
|
knownLegacyBlocks: [protocolEol(legacyIntentProtocolBlock(), eol)],
|
|
1581
1966
|
});
|
|
@@ -1587,9 +1972,11 @@ const commands = {
|
|
|
1587
1972
|
versionPattern: /^<!-- driftseal-decisions-version: (\d+) -->\r?$/m,
|
|
1588
1973
|
replacement: decisionBlock,
|
|
1589
1974
|
knownManagedBlocks: [
|
|
1590
|
-
protocolEol(
|
|
1591
|
-
protocolEol(
|
|
1592
|
-
protocolEol(
|
|
1975
|
+
protocolEol(previousDecisionProtocolBlock(2), eol),
|
|
1976
|
+
protocolEol(previousDecisionProtocolBlock(3), eol),
|
|
1977
|
+
protocolEol(previousDecisionProtocolBlock(4), eol),
|
|
1978
|
+
protocolEol(previousDecisionProtocolBlock(5), eol),
|
|
1979
|
+
protocolEol(previousDecisionProtocolBlock(6), eol),
|
|
1593
1980
|
],
|
|
1594
1981
|
knownLegacyBlocks: [protocolEol(legacyDecisionProtocolBlock(), eol)],
|
|
1595
1982
|
});
|
|
@@ -1627,13 +2014,20 @@ usage:
|
|
|
1627
2014
|
driftseal begin "<intent>" [--verify "<how to verify>"] [--decision <id>] [--force]
|
|
1628
2015
|
driftseal end [id] [--status completed|partial|failed|abandoned] [--note "..."] [--verify-result "..."]
|
|
1629
2016
|
driftseal status show the intent currently in progress (re-anchor after drift)
|
|
1630
|
-
driftseal log [--last N]
|
|
2017
|
+
driftseal log [--last N] [--all] show intent history (--all includes reclaimed records)
|
|
2018
|
+
driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]
|
|
2019
|
+
hide meaningless closed records without deleting them
|
|
2020
|
+
driftseal unreclaim <id> --reason "<why>"
|
|
2021
|
+
restore a reclaimed record to the visible log
|
|
1631
2022
|
driftseal decision add "<title>" --context "..." --outcome "..." [options]
|
|
1632
2023
|
driftseal decision update <id> [--status STATUS] --note "..."
|
|
1633
2024
|
reconcile a linked decision in the open intent
|
|
1634
2025
|
driftseal decision list [--status STATUS] [--last N | --count]
|
|
1635
2026
|
list or count filtered MADR decision records
|
|
1636
2027
|
driftseal decision show <id> print one MADR decision record
|
|
2028
|
+
driftseal mcp install --target TARGET [--scope project|global] [--root <repository>] [--force]
|
|
2029
|
+
install the repository-pinned MCP server (default: project)
|
|
2030
|
+
targets: codex, kimi-code, opencode, claude-code, cursor
|
|
1637
2031
|
driftseal init inject intent and decision protocols into ./AGENTS.md
|
|
1638
2032
|
driftseal help
|
|
1639
2033
|
|
|
@@ -1658,7 +2052,9 @@ function requestedEndStatus(argv) {
|
|
|
1658
2052
|
}
|
|
1659
2053
|
|
|
1660
2054
|
function mutationResources(cmd, argv) {
|
|
2055
|
+
if (cmd === 'mcp') return [parseMcpInstallRequest(argv).configDir];
|
|
1661
2056
|
if (cmd === 'init') return [process.cwd()];
|
|
2057
|
+
if (cmd === 'reclaim' || cmd === 'unreclaim') return [logDir()];
|
|
1662
2058
|
if (cmd === 'begin' && !argv.some((arg) => arg === '--decision' || arg.startsWith('--decision='))) {
|
|
1663
2059
|
return [logDir()];
|
|
1664
2060
|
}
|
|
@@ -1676,7 +2072,7 @@ function dispatch(argv) {
|
|
|
1676
2072
|
const fn = commands[cmd];
|
|
1677
2073
|
if (!fn) fail(`unknown command: ${cmd} (run: driftseal help)`);
|
|
1678
2074
|
const mutates =
|
|
1679
|
-
['begin', 'end', 'init'].includes(cmd) ||
|
|
2075
|
+
['begin', 'end', 'init', 'mcp', 'reclaim', 'unreclaim'].includes(cmd) ||
|
|
1680
2076
|
(cmd === 'decision' && ['add', 'update'].includes(rest[0]));
|
|
1681
2077
|
const readsIntentLog = ['status', 'log'].includes(cmd);
|
|
1682
2078
|
if (mutates || readsIntentLog) {
|
|
@@ -1768,11 +2164,22 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
|
1768
2164
|
appendFlag(argv, '--verify-result', verifyResult);
|
|
1769
2165
|
return call(argv);
|
|
1770
2166
|
},
|
|
1771
|
-
log({ last } = {}) {
|
|
2167
|
+
log({ last, all = false } = {}) {
|
|
1772
2168
|
const argv = ['log'];
|
|
1773
2169
|
appendFlag(argv, '--last', last);
|
|
2170
|
+
if (all) argv.push('--all');
|
|
1774
2171
|
return call(argv);
|
|
1775
2172
|
},
|
|
2173
|
+
reclaim({ ids = [], reason, olderThan, force = false, dryRun = false }) {
|
|
2174
|
+
const argv = ['reclaim', ...ids.map(String), '--reason', reason];
|
|
2175
|
+
appendFlag(argv, '--older-than', olderThan);
|
|
2176
|
+
if (force) argv.push('--force');
|
|
2177
|
+
if (dryRun) argv.push('--dry-run');
|
|
2178
|
+
return call(argv);
|
|
2179
|
+
},
|
|
2180
|
+
unreclaim({ id, reason }) {
|
|
2181
|
+
return call(['unreclaim', String(id), '--reason', reason]);
|
|
2182
|
+
},
|
|
1776
2183
|
decisionAdd({ title, context, outcome, status, drivers = [], options = [], consequences = [] }) {
|
|
1777
2184
|
const argv = ['decision', 'add', title, '--context', context, '--outcome', outcome];
|
|
1778
2185
|
appendFlag(argv, '--status', status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftseal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Seal intent, verification, and decisions into an auditable workflow for agentic coding",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"driftseal",
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"decision-log",
|
|
13
13
|
"madr"
|
|
14
14
|
],
|
|
15
|
-
"homepage": "https://github.com/
|
|
15
|
+
"homepage": "https://github.com/rowan-hiro/DriftSeal#readme",
|
|
16
16
|
"bugs": {
|
|
17
|
-
"url": "https://github.com/
|
|
17
|
+
"url": "https://github.com/rowan-hiro/DriftSeal/issues"
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/
|
|
21
|
+
"url": "git+https://github.com/rowan-hiro/DriftSeal.git"
|
|
22
22
|
},
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"author": "Ruan Boyu <boyuruan@gmail.com>",
|
|
@@ -5,162 +5,62 @@ description: Run repository work through the DriftSeal (`driftseal`) intent, ver
|
|
|
5
5
|
|
|
6
6
|
# Use DriftSeal
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
11
14
|
|
|
12
15
|
## Locate DriftSeal
|
|
13
16
|
|
|
14
17
|
- Prefer the `driftseal_*` MCP tools when the DriftSeal MCP server is available
|
|
15
|
-
for the target repository
|
|
16
|
-
`driftseal_end`,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
## Re-anchor Before Acting
|
|
33
|
-
|
|
34
|
-
1. Run `driftseal status` at the start of work.
|
|
35
|
-
2. Run `driftseal log --last 3` after compaction, a resumed session, or uncertainty.
|
|
36
|
-
3. Continue an open intent when it matches the requested work. Treat it as the
|
|
37
|
-
source of truth; do not open a duplicate intent.
|
|
38
|
-
4. Close a conflicting intent as `partial` or `abandoned` with an honest note,
|
|
39
|
-
then begin the replacement round.
|
|
40
|
-
|
|
41
|
-
Do not use `--force` merely for convenience. If another live actor owns the
|
|
42
|
-
open intent, stop mutating and coordinate instead of abandoning its work.
|
|
43
|
-
|
|
44
|
-
## Begin the Round
|
|
45
|
-
|
|
46
|
-
Before modifying, creating, or deleting files — or making any other change that
|
|
47
|
-
may need a rollback — declare one objective and its proof:
|
|
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`.
|
|
26
|
+
- Use one interface consistently within a round.
|
|
27
|
+
- If DriftSeal is unavailable, limit activity to read-only discovery and report
|
|
28
|
+
the blocker. Do not mutate the repository without the required log.
|
|
29
|
+
|
|
30
|
+
## Command Map
|
|
31
|
+
|
|
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:
|
|
48
34
|
|
|
49
35
|
```sh
|
|
50
|
-
driftseal
|
|
51
|
-
|
|
36
|
+
driftseal status
|
|
37
|
+
driftseal log --last 3 # add --all to include reclaimed records
|
|
52
38
|
```
|
|
53
39
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
only build, check, or record work already done — compiling, running tests,
|
|
58
|
-
`git add`/`git commit` — need no intent of their own.
|
|
59
|
-
|
|
60
|
-
When the round may change or confirm an existing decision, declare each one at
|
|
61
|
-
the boundary with `--decision <id>`. Do not add decision links speculatively.
|
|
62
|
-
|
|
63
|
-
Read-only inspection needed to choose the objective or verifier may happen
|
|
64
|
-
before `begin`. Do not let that inspection turn into unlogged implementation.
|
|
65
|
-
|
|
66
|
-
## Execute Without Drift
|
|
67
|
-
|
|
68
|
-
- Change only what the open intent covers.
|
|
69
|
-
- Preserve unrelated worktree changes and other actors' edits.
|
|
70
|
-
- If the objective expands or changes, close the current intent as `partial`
|
|
71
|
-
or `abandoned`, then start a new round before continuing.
|
|
72
|
-
- If the declared verifier becomes invalid, record that honestly and start a
|
|
73
|
-
new round with the correct verifier instead of silently substituting proof.
|
|
74
|
-
- If the user replaces the active request, reconcile the open intent before
|
|
75
|
-
acting on the replacement.
|
|
76
|
-
|
|
77
|
-
## Record Decisions Selectively
|
|
78
|
-
|
|
79
|
-
Before adding a decision record, ask what useful information would disappear
|
|
80
|
-
if only the intent log and final Git commit remained.
|
|
81
|
-
|
|
82
|
-
Add a MADR record only when it preserves at least one of these:
|
|
83
|
-
|
|
84
|
-
- a rejected path worth preventing future agents from retrying;
|
|
85
|
-
- an unresolved or deliberately deferred path with a concrete revisit trigger;
|
|
86
|
-
- non-obvious rationale or trade-offs behind an accepted choice that is
|
|
87
|
-
long-lived, cross-cutting, or costly to reverse;
|
|
88
|
-
- the reason an earlier decision became deprecated or superseded.
|
|
89
|
-
|
|
90
|
-
Skip routine, local, readily reversible choices. Do not restate an accepted
|
|
91
|
-
change that the intent and commit already explain.
|
|
92
|
-
|
|
93
|
-
Use `proposed` for unresolved choices still under active consideration. Use
|
|
94
|
-
`deferred` for choices that are deliberately postponed, and state the revisit
|
|
95
|
-
trigger in the outcome or consequences. Use `rejected` for an explicitly
|
|
96
|
-
ruled-out choice. Reserve `accepted` for the exceptional accepted decisions
|
|
97
|
-
whose rationale would otherwise be lost.
|
|
98
|
-
|
|
99
|
-
Count postponed choices with `driftseal decision list --status deferred --count`,
|
|
100
|
-
then review them with `driftseal decision list --status deferred` so they do not
|
|
101
|
-
disappear into the chronological log.
|
|
102
|
-
|
|
103
|
-
For every decision explicitly linked by the open intent, reconcile its current
|
|
104
|
-
status and rationale before a successful close:
|
|
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`):
|
|
105
43
|
|
|
106
44
|
```sh
|
|
107
|
-
driftseal
|
|
108
|
-
|
|
109
|
-
|
|
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>"
|
|
110
48
|
```
|
|
111
49
|
|
|
112
|
-
|
|
113
|
-
unchanged decision still needs an explicit confirmation note. DriftSeal rejects a
|
|
114
|
-
`completed` or `partial` close if any declared decision remains unreconciled;
|
|
115
|
-
`failed` and `abandoned` remain available as escape paths.
|
|
116
|
-
|
|
117
|
-
Do not edit a linked decision after reconciling it. Run `decision update` again
|
|
118
|
-
so the final content hash is recorded. If an update is interrupted, rerun it or
|
|
119
|
-
successfully close the linked intent; DriftSeal recovers only that intent's pending
|
|
120
|
-
transaction. Closing as `failed` or `abandoned` cancels its pending recovery so
|
|
121
|
-
historical conflicts cannot block future decision work.
|
|
50
|
+
Record and reconcile decisions as the repository's decision protocol directs:
|
|
122
51
|
|
|
123
52
|
```sh
|
|
124
|
-
driftseal decision add "<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
--outcome "<current disposition, rationale, and revisit trigger>" \
|
|
128
|
-
--option "<considered option>" \
|
|
129
|
-
--consequence "<result of this disposition>"
|
|
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
|
|
130
56
|
```
|
|
131
57
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Run the declared verification exactly as written. Then close the intent before
|
|
135
|
-
reporting success:
|
|
58
|
+
Retire meaningless closed records as the repository's protocol directs:
|
|
136
59
|
|
|
137
60
|
```sh
|
|
138
|
-
driftseal
|
|
139
|
-
|
|
140
|
-
--note "<what actually happened>" \
|
|
141
|
-
--verify-result "<concise, honest result>"
|
|
61
|
+
driftseal reclaim [id ...] --reason "<why>" [--dry-run]
|
|
62
|
+
driftseal unreclaim <id> --reason "<why>"
|
|
142
63
|
```
|
|
143
64
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
- `completed`: achieve the objective and pass the declared verification.
|
|
147
|
-
- `partial`: leave useful work but do not achieve the whole objective.
|
|
148
|
-
- `failed`: fail to produce a usable result or fail essential verification.
|
|
149
|
-
- `abandoned`: intentionally stop or replace the round.
|
|
150
|
-
|
|
151
|
-
Never leave an intent open merely because the work failed. Never report a
|
|
152
|
-
completed result while the log still says `in_progress`.
|
|
153
|
-
|
|
154
|
-
## Persist the Round in Git
|
|
155
|
-
|
|
156
|
-
Treat a focused Git commit as the third record: the intent says what was
|
|
157
|
-
planned and how it was checked, the decision log preserves otherwise-lost
|
|
158
|
-
context, and the commit shows what actually landed.
|
|
159
|
-
|
|
160
|
-
When the user has authorized a commit, stage and commit only the verified
|
|
161
|
-
changes, the closed intent events, and any relevant decision record. This
|
|
162
|
-
bookkeeping finalizes the just-closed round and does not require a new intent.
|
|
163
|
-
|
|
164
|
-
Keep this exception narrow. Open a new intent before making content changes,
|
|
165
|
-
fixing a hook failure, rewriting history, rebasing, pushing, or including work
|
|
166
|
-
outside the closed round.
|
|
65
|
+
For exact flags, eligibility rules, and recovery behavior, run
|
|
66
|
+
`driftseal help` and read the repository's `AGENTS.md`.
|