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 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
- For Codex, add the installed command as a stdio MCP server:
62
+ Install the server into the current repository's agent config with one of the
63
+ supported targets:
63
64
 
64
65
  ```sh
65
- codex mcp add driftseal -- driftseal-mcp --root /absolute/path/to/repository
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
- Codex 中,可以把安装后的命令添加为 stdio MCP server:
62
+ 选择对应 target,即可把 server 安装到当前 repository 的 agent 配置:
63
63
 
64
64
  ```sh
65
- codex mcp add driftseal -- driftseal-mcp --root /absolute/path/to/repository
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
 
@@ -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: { last: z.number().int().positive().max(100).optional() },
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 = 2;
38
- const PROTOCOL_VERSION = 5;
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 v4 = intentProtocolBlock(version).replace(
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(decisionProtocolBlock(2), eol),
1591
- protocolEol(decisionProtocolBlock(3), eol),
1592
- protocolEol(decisionProtocolBlock(4), eol),
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] show intent history
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.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/ruanboyu/driftseal#readme",
15
+ "homepage": "https://github.com/rowan-hiro/DriftSeal#readme",
16
16
  "bugs": {
17
- "url": "https://github.com/ruanboyu/driftseal/issues"
17
+ "url": "https://github.com/rowan-hiro/DriftSeal/issues"
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "git+https://github.com/ruanboyu/driftseal.git"
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
- Run repository work as small, closed, auditable rounds. Treat the intent log,
9
- decision log, and Git history as complementary records rather than duplicate
10
- activity streams.
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. Use `driftseal_status`, `driftseal_begin`,
16
- `driftseal_end`, and the corresponding decision tools instead of invoking the
17
- CLI through a shell.
18
- - Work from the repository root unless its instructions specify another scope.
19
- - When MCP is unavailable, prefer `driftseal` from `PATH`. In a DriftSeal source
20
- checkout, fall back to `node bin/driftseal.js` when the global command is
21
- unavailable.
22
- - Follow the repository's `AGENTS.md`. Storage overrides such as
23
- `DRIFTSEAL_HOME` apply to CLI use; the MCP server deliberately keeps state in
24
- its fixed repository root.
25
- - If DriftSeal is unavailable, limit activity to read-only discovery and report the
26
- blocker. Do not mutate the repository without the required log.
27
-
28
- Use one interface consistently within a round. The examples below use
29
- `driftseal`; map them directly to the same-named MCP workflow when MCP is
30
- available, or substitute the local source command when necessary.
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 begin "<small objective for this round>" \
51
- --verify "<exact command or outcome check>"
36
+ driftseal status
37
+ driftseal log --last 3 # add --all to include reclaimed records
52
38
  ```
53
39
 
54
- Make the intent small enough to finish and verify in one round. Prefer an
55
- outcome-focused check over a vague activity such as "inspect the result."
56
- Starting the intent is the first permitted mutation. Single-step commands that
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 decision update <id> \
108
- --status <proposed|accepted|rejected|deferred|deprecated|superseded> \
109
- --note "<what changed or was confirmed, and why>"
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
- The update appends a decision history entry tied to the open intent. An
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 "<decision title>" \
125
- --status deferred \
126
- --context "<problem and constraints>" \
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
- ## Verify and Close
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 end \
139
- --status completed \
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
- Choose the status from evidence:
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`.