driftseal 0.3.0 → 0.4.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
@@ -75,6 +75,7 @@ The v1 server provides:
75
75
  | --- | --- |
76
76
  | `driftseal_status`, `driftseal_log` | Read the current intent and intent history. |
77
77
  | `driftseal_begin`, `driftseal_end` | Open and honestly close a work round. |
78
+ | `driftseal_reclaim`, `driftseal_unreclaim` | Hide meaningless closed records behind append-only markers, or restore them. |
78
79
  | `driftseal_decision_list`, `driftseal_decision_show` | Find and read MADR records. |
79
80
  | `driftseal_decision_add`, `driftseal_decision_update` | Add selective decisions and reconcile linked ones. |
80
81
  | `driftseal://intent/current` | Read the current intent as a JSON resource. |
@@ -113,7 +114,9 @@ Single-step commands that only build, check, or record work already done — com
113
114
  | `driftseal begin "<intent>" [-v "<verify>"] [--decision id] [--force]` | Open a work-round intent and optionally link existing decisions. |
114
115
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | Close an intent honestly. |
115
116
  | `driftseal status` | Show the intent currently in progress. |
116
- | `driftseal log [-n N]` | Review intent history. |
117
+ | `driftseal log [-n N] [--all]` | Review intent history (`--all` includes reclaimed records). |
118
+ | `driftseal reclaim [id ...] --reason "..." [--older-than days] [--force] [--dry-run]` | Hide meaningless closed records behind append-only markers. |
119
+ | `driftseal unreclaim <id> --reason "..."` | Restore a reclaimed record to the visible log. |
117
120
  | `driftseal decision add "<title>" -c "..." -o "..."` | Write a numbered MADR decision. |
118
121
  | `driftseal decision update <id> [-s status] -n "..."` | Reconcile a linked decision in the open intent. |
119
122
  | `driftseal decision list [-s status] [--last N \| --count]` | List or count decision records, optionally filtered by status. |
@@ -127,6 +130,22 @@ close as `completed` or `partial`. The update changes the current status when
127
130
  requested and appends a timestamped history entry tied to the intent. Intents
128
131
  without decision links keep the ordinary workflow.
129
132
 
133
+ ## Reclaiming noise records
134
+
135
+ Some closed records stop mattering: a harness or sandbox failure is recorded
136
+ honestly as `failed`, but it says nothing about the project. `driftseal
137
+ reclaim` retires such records without rewriting history — it appends a
138
+ `reclaim` marker (with a mandatory `--reason`) to the same append-only log,
139
+ and reclaimed records disappear from `driftseal log` and `driftseal status`
140
+ output while remaining in `events.jsonl` and visible with `log --all`.
141
+ `driftseal unreclaim <id> --reason "..."` restores a record that turned out to
142
+ matter.
143
+
144
+ Without ids, batch mode reclaims only closed `failed`/`abandoned` records that
145
+ are not linked to decisions and are older than `--older-than` days (default
146
+ 7); use `--dry-run` to preview. `completed` and `partial` records, and any
147
+ decision-linked record, can only be reclaimed by explicit id with `--force`.
148
+
130
149
  ## Consistency and recovery
131
150
 
132
151
  DriftSeal serializes mutating commands with locks on the configured intent and
@@ -157,7 +176,7 @@ index.
157
176
 
158
177
  ## Storage
159
178
 
160
- - `.intent-log/events.jsonl` is the append-only intent log.
179
+ - `.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
180
  - `.decision-log/` contains numbered MADR decision records.
162
181
  - Set `DRIFTSEAL_HOME` or `DRIFTSEAL_DECISION_HOME` to store either log outside the current project.
163
182
 
package/README.zh-CN.md CHANGED
@@ -75,6 +75,7 @@ v1 server 提供:
75
75
  | --- | --- |
76
76
  | `driftseal_status`, `driftseal_log` | 读取当前 intent 和 intent 历史。 |
77
77
  | `driftseal_begin`, `driftseal_end` | 开启并诚实关闭一轮工作。 |
78
+ | `driftseal_reclaim`, `driftseal_unreclaim` | 用 append-only 标记隐藏已无意义的已关闭记录,或将其恢复。 |
78
79
  | `driftseal_decision_list`, `driftseal_decision_show` | 查找并读取 MADR record。 |
79
80
  | `driftseal_decision_add`, `driftseal_decision_update` | 克制地增加 decision,并 reconcile 已关联的 decision。 |
80
81
  | `driftseal://intent/current` | 以 JSON resource 读取当前 intent。 |
@@ -113,7 +114,9 @@ driftseal end \
113
114
  | `driftseal begin "<intent>" [-v "<verify>"] [--decision id] [--force]` | 开启一轮工作,并可关联已有 decision。 |
114
115
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | 诚实地关闭 intent。 |
115
116
  | `driftseal status` | 查看当前进行中的 intent。 |
116
- | `driftseal log [-n N]` | 查看 intent 历史。 |
117
+ | `driftseal log [-n N] [--all]` | 查看 intent 历史(`--all` 包含已回收的记录)。 |
118
+ | `driftseal reclaim [id ...] --reason "..." [--older-than days] [--force] [--dry-run]` | 用 append-only 标记隐藏已无意义的已关闭记录。 |
119
+ | `driftseal unreclaim <id> --reason "..."` | 把已回收的记录恢复到可见历史中。 |
117
120
  | `driftseal decision add "<title>" -c "..." -o "..."` | 写入编号化的 MADR decision。 |
118
121
  | `driftseal decision update <id> [-s status] -n "..."` | 在当前 intent 中 reconcile 已关联的 decision。 |
119
122
  | `driftseal decision list [-s status] [--last N \| --count]` | 列出或统计 decision records,也可按 status 筛选。 |
@@ -126,6 +129,19 @@ driftseal end \
126
129
  每一条关联 decision。update 可以改变当前 status,并会追加一条包含时间和
127
130
  intent ID 的 history。没有关联 decision 的 intent 仍沿用普通流程。
128
131
 
132
+ ## 回收已无意义的记录
133
+
134
+ 有些已关闭的记录会随着时间失去意义:harness 或 sandbox 导致的失败会被如实记录为
135
+ `failed`,但它与项目本身无关。`driftseal reclaim` 可以在不改写历史的前提下让这类
136
+ 记录退场——它只是向同一个 append-only log 追加一条 `reclaim` 标记(必须附带
137
+ `--reason`),被回收的记录会从 `driftseal log` 和 `driftseal status` 的输出中隐藏,
138
+ 但仍保留在 `events.jsonl` 中,并可通过 `log --all` 查看。若事后发现某条记录仍然
139
+ 重要,用 `driftseal unreclaim <id> --reason "..."` 恢复。
140
+
141
+ 不带 id 的批量模式只回收已关闭、未关联 decision、且早于 `--older-than` 天(默认 7
142
+ 天)的 `failed`/`abandoned` 记录;可以先用 `--dry-run` 预览。`completed` 和 `partial`
143
+ 记录,以及任何关联了 decision 的记录,只能按显式 id 加 `--force` 回收。
144
+
129
145
  ## 一致性与恢复
130
146
 
131
147
  DriftSeal 会对配置后的 intent log 与 decision log 根目录加锁,并按固定顺序获取这些
@@ -152,7 +168,7 @@ recovery 只处理当前 intent,因此历史冲突不会阻塞之后的 decisi
152
168
 
153
169
  ## 数据保存在哪里
154
170
 
155
- - `.intent-log/events.jsonl`:append-only intent log
171
+ - `.intent-log/events.jsonl`:append-only intent log。所有读写都必须经过 `driftseal`(CLI 或 MCP)——不要直接读取、修改、移动或删除该文件;需要让无意义的记录退场时使用 `driftseal reclaim`,而不是删除日志行。
156
172
  - `.decision-log/`:编号化的 MADR decision records。
157
173
  - 设置 `DRIFTSEAL_HOME` 或 `DRIFTSEAL_DECISION_HOME`,即可把对应 log 放到当前项目之外。
158
174
 
@@ -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,
@@ -1408,9 +1467,10 @@ const commands = {
1408
1467
  },
1409
1468
 
1410
1469
  log(argv) {
1411
- const { positionals, flags } = parseArgs(argv, { last: '-n' });
1412
- if (positionals.length > 0) fail('usage: driftseal log [--last N]');
1470
+ const { positionals, flags } = parseArgs(argv, { last: '-n', all: 'boolean' });
1471
+ if (positionals.length > 0) fail('usage: driftseal log [--last N] [--all]');
1413
1472
  let records = fold(readEvents({ repairTail: true }));
1473
+ if (!flags.all) records = records.filter((record) => !record.reclaimed);
1414
1474
  if (flags.last) {
1415
1475
  const n = positiveInteger(flags.last, '--last');
1416
1476
  records = records.slice(-n);
@@ -1423,6 +1483,109 @@ const commands = {
1423
1483
  return records.map(publicIntent);
1424
1484
  },
1425
1485
 
1486
+ reclaim(argv) {
1487
+ const { positionals, flags } = parseArgs(argv, {
1488
+ reason: '-r',
1489
+ 'older-than': 'single',
1490
+ force: 'boolean',
1491
+ 'dry-run': 'boolean',
1492
+ });
1493
+ const reason = flags.reason && flags.reason.trim();
1494
+ if (!reason) {
1495
+ fail(
1496
+ 'usage: driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]'
1497
+ );
1498
+ }
1499
+ let olderThanDays = 7;
1500
+ if (flags['older-than'] !== undefined) {
1501
+ olderThanDays = positiveInteger(flags['older-than'], '--older-than');
1502
+ }
1503
+
1504
+ const records = fold(readEvents({ repairTail: true }));
1505
+ let targets;
1506
+ if (positionals.length > 0) {
1507
+ const ids = [...new Set(positionals)];
1508
+ targets = ids.map((id) => {
1509
+ const record = records.find((candidate) => candidate.id === id);
1510
+ if (!record) fail(`unknown intent id: ${id}`);
1511
+ if (record.status === 'in_progress') {
1512
+ fail(`cannot reclaim intent ${id} while it is in_progress`);
1513
+ }
1514
+ if (record.reclaimed) fail(`intent ${id} is already reclaimed`);
1515
+ const routine = ['failed', 'abandoned'].includes(record.status) &&
1516
+ record.decisions.length === 0;
1517
+ if (!routine && !flags.force) {
1518
+ fail(
1519
+ `intent ${id} is ${record.status}` +
1520
+ (record.decisions.length > 0 ? ' and linked to decisions' : '') +
1521
+ '; re-run with --force to reclaim it anyway'
1522
+ );
1523
+ }
1524
+ return record;
1525
+ });
1526
+ } else {
1527
+ if (flags.force) fail('--force requires explicit intent ids');
1528
+ const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
1529
+ targets = records.filter(
1530
+ (record) =>
1531
+ record.status !== 'in_progress' &&
1532
+ ['failed', 'abandoned'].includes(record.status) &&
1533
+ record.decisions.length === 0 &&
1534
+ !record.reclaimed &&
1535
+ Date.parse(record.tsEnd) < cutoff
1536
+ );
1537
+ if (targets.length === 0) {
1538
+ printLine('no reclaimable intents');
1539
+ return [];
1540
+ }
1541
+ }
1542
+
1543
+ if (flags['dry-run']) {
1544
+ printLine(targets.map((record) => `${record.id} ${record.status} — ${record.intent}`).join('\n'));
1545
+ return targets.map(publicIntent);
1546
+ }
1547
+
1548
+ let events = readEvents({ repairTail: true });
1549
+ for (const record of targets) {
1550
+ events.push(
1551
+ appendEvent({
1552
+ type: 'reclaim',
1553
+ id: record.id,
1554
+ ts: new Date().toISOString(),
1555
+ reason,
1556
+ })
1557
+ );
1558
+ }
1559
+ const reclaimed = fold(events).filter((record) =>
1560
+ targets.some((target) => target.id === record.id)
1561
+ );
1562
+ printLine(targets.map((record) => `${record.id} reclaimed`).join('\n'));
1563
+ return reclaimed.map(publicIntent);
1564
+ },
1565
+
1566
+ unreclaim(argv) {
1567
+ const { positionals, flags } = parseArgs(argv, { reason: '-r' });
1568
+ const reason = flags.reason && flags.reason.trim();
1569
+ if (positionals.length !== 1 || !reason) {
1570
+ fail('usage: driftseal unreclaim <id> --reason "<why>"');
1571
+ }
1572
+ const events = readEvents({ repairTail: true });
1573
+ const record = fold(events).find((candidate) => candidate.id === positionals[0]);
1574
+ if (!record) fail(`unknown intent id: ${positionals[0]}`);
1575
+ if (!record.reclaimed) fail(`intent ${positionals[0]} is not reclaimed`);
1576
+ events.push(
1577
+ appendEvent({
1578
+ type: 'unreclaim',
1579
+ id: record.id,
1580
+ ts: new Date().toISOString(),
1581
+ reason,
1582
+ })
1583
+ );
1584
+ const restored = fold(events).find((candidate) => candidate.id === record.id);
1585
+ printLine(`${record.id} unreclaimed`);
1586
+ return publicIntent(restored);
1587
+ },
1588
+
1426
1589
  decision(argv) {
1427
1590
  const [subcommand, ...rest] = argv;
1428
1591
  if (subcommand === 'add') {
@@ -1576,6 +1739,8 @@ const commands = {
1576
1739
  protocolEol(previousIntentProtocolBlock(2), eol),
1577
1740
  protocolEol(previousIntentProtocolBlock(3), eol),
1578
1741
  protocolEol(previousIntentProtocolBlock(4), eol),
1742
+ protocolEol(previousIntentProtocolBlock(5), eol),
1743
+ protocolEol(previousIntentProtocolBlock(6), eol),
1579
1744
  ],
1580
1745
  knownLegacyBlocks: [protocolEol(legacyIntentProtocolBlock(), eol)],
1581
1746
  });
@@ -1587,9 +1752,11 @@ const commands = {
1587
1752
  versionPattern: /^<!-- driftseal-decisions-version: (\d+) -->\r?$/m,
1588
1753
  replacement: decisionBlock,
1589
1754
  knownManagedBlocks: [
1590
- protocolEol(decisionProtocolBlock(2), eol),
1591
- protocolEol(decisionProtocolBlock(3), eol),
1592
- protocolEol(decisionProtocolBlock(4), eol),
1755
+ protocolEol(previousDecisionProtocolBlock(2), eol),
1756
+ protocolEol(previousDecisionProtocolBlock(3), eol),
1757
+ protocolEol(previousDecisionProtocolBlock(4), eol),
1758
+ protocolEol(previousDecisionProtocolBlock(5), eol),
1759
+ protocolEol(previousDecisionProtocolBlock(6), eol),
1593
1760
  ],
1594
1761
  knownLegacyBlocks: [protocolEol(legacyDecisionProtocolBlock(), eol)],
1595
1762
  });
@@ -1627,7 +1794,11 @@ usage:
1627
1794
  driftseal begin "<intent>" [--verify "<how to verify>"] [--decision <id>] [--force]
1628
1795
  driftseal end [id] [--status completed|partial|failed|abandoned] [--note "..."] [--verify-result "..."]
1629
1796
  driftseal status show the intent currently in progress (re-anchor after drift)
1630
- driftseal log [--last N] show intent history
1797
+ driftseal log [--last N] [--all] show intent history (--all includes reclaimed records)
1798
+ driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]
1799
+ hide meaningless closed records without deleting them
1800
+ driftseal unreclaim <id> --reason "<why>"
1801
+ restore a reclaimed record to the visible log
1631
1802
  driftseal decision add "<title>" --context "..." --outcome "..." [options]
1632
1803
  driftseal decision update <id> [--status STATUS] --note "..."
1633
1804
  reconcile a linked decision in the open intent
@@ -1659,6 +1830,7 @@ function requestedEndStatus(argv) {
1659
1830
 
1660
1831
  function mutationResources(cmd, argv) {
1661
1832
  if (cmd === 'init') return [process.cwd()];
1833
+ if (cmd === 'reclaim' || cmd === 'unreclaim') return [logDir()];
1662
1834
  if (cmd === 'begin' && !argv.some((arg) => arg === '--decision' || arg.startsWith('--decision='))) {
1663
1835
  return [logDir()];
1664
1836
  }
@@ -1676,7 +1848,7 @@ function dispatch(argv) {
1676
1848
  const fn = commands[cmd];
1677
1849
  if (!fn) fail(`unknown command: ${cmd} (run: driftseal help)`);
1678
1850
  const mutates =
1679
- ['begin', 'end', 'init'].includes(cmd) ||
1851
+ ['begin', 'end', 'init', 'reclaim', 'unreclaim'].includes(cmd) ||
1680
1852
  (cmd === 'decision' && ['add', 'update'].includes(rest[0]));
1681
1853
  const readsIntentLog = ['status', 'log'].includes(cmd);
1682
1854
  if (mutates || readsIntentLog) {
@@ -1768,11 +1940,22 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
1768
1940
  appendFlag(argv, '--verify-result', verifyResult);
1769
1941
  return call(argv);
1770
1942
  },
1771
- log({ last } = {}) {
1943
+ log({ last, all = false } = {}) {
1772
1944
  const argv = ['log'];
1773
1945
  appendFlag(argv, '--last', last);
1946
+ if (all) argv.push('--all');
1947
+ return call(argv);
1948
+ },
1949
+ reclaim({ ids = [], reason, olderThan, force = false, dryRun = false }) {
1950
+ const argv = ['reclaim', ...ids.map(String), '--reason', reason];
1951
+ appendFlag(argv, '--older-than', olderThan);
1952
+ if (force) argv.push('--force');
1953
+ if (dryRun) argv.push('--dry-run');
1774
1954
  return call(argv);
1775
1955
  },
1956
+ unreclaim({ id, reason }) {
1957
+ return call(['unreclaim', String(id), '--reason', reason]);
1958
+ },
1776
1959
  decisionAdd({ title, context, outcome, status, drivers = [], options = [], consequences = [] }) {
1777
1960
  const argv = ['decision', 'add', title, '--context', context, '--outcome', outcome];
1778
1961
  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.4.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`.