driftseal 1.2.0 → 1.3.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
@@ -67,7 +67,7 @@ driftseal skill install --target kimi-code --scope global
67
67
  | Target | Project scope | Global scope |
68
68
  | --- | --- | --- |
69
69
  | `codex` | `.agents/skills/use-driftseal` | `~/.agents/skills/use-driftseal` |
70
- | `kimi-code` | `.kimi/skills/use-driftseal` | `~/.kimi/skills/use-driftseal` |
70
+ | `kimi-code` | `.kimi-code/skills/use-driftseal` | `~/.kimi-code/skills/use-driftseal` |
71
71
  | `opencode` | `.opencode/skills/use-driftseal` | `~/.config/opencode/skills/use-driftseal` |
72
72
  | `claude-code` | `.claude/skills/use-driftseal` | `~/.claude/skills/use-driftseal` |
73
73
  | `cursor` | `.cursor/skills/use-driftseal` | `~/.cursor/skills/use-driftseal` |
@@ -137,7 +137,7 @@ The v1 server provides:
137
137
  | MCP capability | Purpose |
138
138
  | --- | --- |
139
139
  | `driftseal_status`, `driftseal_log` | Read the current intent and intent history. |
140
- | `driftseal_begin`, `driftseal_end` | Open and honestly close a work round. |
140
+ | `driftseal_begin`, `driftseal_verify`, `driftseal_end` | Open a work round, capture machine verification evidence, and honestly close it. |
141
141
  | `driftseal_absorb` | Repair merge collisions or absorb another worktree's logs while remapping colliding IDs. |
142
142
  | `driftseal_reclaim`, `driftseal_unreclaim` | Hide meaningless closed records behind append-only markers, or restore them. |
143
143
  | `driftseal_decision_list`, `driftseal_decision_show` | Find and read MADR records. |
@@ -196,10 +196,40 @@ Declare the round before making non-Git changes:
196
196
 
197
197
  ```sh
198
198
  driftseal begin "add rate limiting to /api/login" \
199
+ --accept "the sixth login attempt within one minute receives HTTP 429" \
199
200
  --verify "npm test test/rate-limit.test.js"
200
201
  ```
201
202
 
202
- Do the work, run the declared check, then reconcile the result:
203
+ Do the work, reconcile any linked decisions, inspect the declared command with
204
+ `driftseal status`, and only then let DriftSeal run it:
205
+
206
+ ```sh
207
+ driftseal verify
208
+ ```
209
+
210
+ `driftseal verify` passes the exact stored string to the operating-system shell.
211
+ The command can therefore read or modify files, access the network, or run any
212
+ other program available to the current user. Treat it as executable code, not as
213
+ passive log data. A locally created open intent is parked in Git metadata and runs
214
+ normally. If the open intent came from the repository's tracked intent log,
215
+ DriftSeal prints the command to stderr and refuses to execute it until you inspect
216
+ it and explicitly run `driftseal verify --allow-tracked-command`. The programmatic
217
+ API and MCP tool expose the equivalent `allowTrackedCommand` opt-in.
218
+
219
+ The verification event records the command's exit status, duration, output
220
+ digest and byte counts, Git HEAD, and a fingerprint of every tracked or
221
+ untracked non-ignored workspace file except the intent event log. A successful
222
+ result becomes stale if those workspace contents change. DriftSeal therefore
223
+ rejects `completed` until the command passes again on the current workspace.
224
+ Ignored files are deliberately outside this fingerprint. Outside a Git
225
+ worktree the fingerprint is unavailable, so the gate proves only the command's
226
+ recorded exit status and cannot detect later content changes.
227
+
228
+ This proves that the declared command passed on recorded contents; it does not
229
+ prove that the acceptance criterion or test is adequate. Existing intents
230
+ without `--accept` retain the manual verification workflow for compatibility.
231
+ Use protected CI, independent review, or human approval when the verifier was
232
+ written by the same agent, the outcome is subjective, or the change is high risk.
203
233
 
204
234
  ```sh
205
235
  driftseal end \
@@ -224,7 +254,8 @@ also need no intent. Any other non-Git content change starts a new work round.
224
254
 
225
255
  | Command | Purpose |
226
256
  | --- | --- |
227
- | `driftseal begin "<intent>" [-v "<verify>"] [--decision id] [--force]` | Open a work-round intent and optionally link existing decisions. |
257
+ | `driftseal begin "<intent>" [--accept "<outcome>"] [-v "<command>"] [--decision id] [--force]` | Open a work-round intent. Repeat `--accept` for observable completion criteria; acceptance requires a verification command. |
258
+ | `driftseal verify [--allow-tracked-command]` | Execute the acceptance-bound intent's predeclared command and bind machine evidence to the current Git-visible workspace contents. Commands sourced from the tracked intent log require the explicit opt-in. |
228
259
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | Close an intent honestly. |
229
260
  | `driftseal status` | Show the intent currently in progress. |
230
261
  | `driftseal log [-n N] [--all]` | Review intent history (`--all` includes reclaimed records). |
@@ -248,7 +279,9 @@ When `begin` declares one or more `--decision <id>` links, every linked
248
279
  decision must be reconciled with `driftseal decision update` before that intent can
249
280
  close as `completed` or `partial`. The update changes the current status when
250
281
  requested and appends a timestamped history entry tied to the intent. Intents
251
- without decision links keep the ordinary workflow.
282
+ without decision links keep the ordinary workflow. For acceptance-bound linked
283
+ intents, perform every decision update before `driftseal verify`, because a decision
284
+ update changes the workspace fingerprint: reconcile, verify, then end.
252
285
 
253
286
  ## Reclaiming noise records
254
287
 
package/README.zh-CN.md CHANGED
@@ -65,7 +65,7 @@ driftseal skill install --target kimi-code --scope global
65
65
  | Target | 项目级 scope | 全局 scope |
66
66
  | --- | --- | --- |
67
67
  | `codex` | `.agents/skills/use-driftseal` | `~/.agents/skills/use-driftseal` |
68
- | `kimi-code` | `.kimi/skills/use-driftseal` | `~/.kimi/skills/use-driftseal` |
68
+ | `kimi-code` | `.kimi-code/skills/use-driftseal` | `~/.kimi-code/skills/use-driftseal` |
69
69
  | `opencode` | `.opencode/skills/use-driftseal` | `~/.config/opencode/skills/use-driftseal` |
70
70
  | `claude-code` | `.claude/skills/use-driftseal` | `~/.claude/skills/use-driftseal` |
71
71
  | `cursor` | `.cursor/skills/use-driftseal` | `~/.cursor/skills/use-driftseal` |
@@ -132,7 +132,7 @@ v1 server 提供:
132
132
  | MCP capability | 用途 |
133
133
  | --- | --- |
134
134
  | `driftseal_status`, `driftseal_log` | 读取当前 intent 和 intent 历史。 |
135
- | `driftseal_begin`, `driftseal_end` | 开启并诚实关闭一轮工作。 |
135
+ | `driftseal_begin`, `driftseal_verify`, `driftseal_end` | 开启一轮工作、采集机器验证证据,并诚实关闭。 |
136
136
  | `driftseal_absorb` | 修复 merge 撞号,或吸收另一条 worktree 日志并重编号冲突 ID。 |
137
137
  | `driftseal_reclaim`, `driftseal_unreclaim` | 用 append-only 标记隐藏已无意义的已关闭记录,或将其恢复。 |
138
138
  | `driftseal_decision_list`, `driftseal_decision_show` | 查找并读取 MADR record。 |
@@ -186,10 +186,38 @@ Kimi Code 只在全局 `config.toml` 中记录 hook,因此该 target 必须指
186
186
 
187
187
  ```sh
188
188
  driftseal begin "add rate limiting to /api/login" \
189
+ --accept "the sixth login attempt within one minute receives HTTP 429" \
189
190
  --verify "npm test test/rate-limit.test.js"
190
191
  ```
191
192
 
192
- 完成工作并运行约定的 check 后,记录实际结果:
193
+ 完成工作后,先 reconcile 所有关联 decision,再用 `driftseal status` 检查声明的
194
+ 命令,确认无误后才让 DriftSeal 执行:
195
+
196
+ ```sh
197
+ driftseal verify
198
+ ```
199
+
200
+ `driftseal verify` 会把日志中保存的完整字符串交给操作系统 shell。这个命令可以
201
+ 读写文件、访问网络,也可以运行当前用户有权执行的任何程序;因此它是可执行代码,
202
+ 不是被动的日志数据。当前 worktree 本地创建的 open intent 会 park 在 Git metadata
203
+ 中,可以直接运行。如果 open intent 来自 repository 中被跟踪的 intent log,
204
+ DriftSeal 会先把命令输出到 stderr,并拒绝执行;只有检查并信任该命令后,才能显式
205
+ 运行 `driftseal verify --allow-tracked-command`。Programmatic API 和 MCP tool 中对应的
206
+ 显式开关是 `allowTrackedCommand`。
207
+
208
+ 验证事件会记录 exit status、耗时、输出摘要及字节数、Git HEAD,以及当前所有
209
+ tracked 和未被 ignore 的 untracked 文件的内容指纹(intent event log 除外)。
210
+ 验证后只要这些内容发生变化,成功证据就会过期,必须重新运行;否则 DriftSeal
211
+ 会拒绝把 intent 关闭为 `completed`。被 ignore 的文件不在指纹范围内。
212
+ 如果当前目录不是 Git worktree,指纹不可用;此时 gate 只能证明记录到的 exit
213
+ status,无法发现之后发生的内容变化。
214
+
215
+ 这只能证明预先声明的命令在记录的内容上通过,不能证明 acceptance criterion
216
+ 或测试本身足够可靠。为兼容旧记录,没有 `--accept` 的 intent 仍沿用手动验证流程。
217
+ 如果验证器也由同一个 agent 编写、结果带有主观判断,或改动风险较高,应再使用
218
+ 受保护的 CI、独立 review 或人工确认。
219
+
220
+ 最后记录实际结果:
193
221
 
194
222
  ```sh
195
223
  driftseal end \
@@ -211,7 +239,8 @@ intent;会被提交且无法重建的内容改动(比如编辑 `.gitignore`
211
239
 
212
240
  | Command | 用途 |
213
241
  | --- | --- |
214
- | `driftseal begin "<intent>" [-v "<verify>"] [--decision id] [--force]` | 开启一轮工作,并可关联已有 decision。 |
242
+ | `driftseal begin "<intent>" [--accept "<outcome>"] [-v "<command>"] [--decision id] [--force]` | 开启一轮工作。可重复使用 `--accept` 声明可观察的完成条件;一旦声明,就必须同时提供验证命令。 |
243
+ | `driftseal verify [--allow-tracked-command]` | 执行 acceptance-bound intent 预先声明的命令,并把机器证据绑定到当前 Git 可见的工作区内容;来自 tracked intent log 的命令必须显式 opt in。 |
215
244
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | 诚实地关闭 intent。 |
216
245
  | `driftseal status` | 查看当前进行中的 intent。 |
217
246
  | `driftseal log [-n N] [--all]` | 查看 intent 历史(`--all` 包含已回收的记录)。 |
@@ -234,7 +263,10 @@ intent;会被提交且无法重建的内容改动(比如编辑 `.gitignore`
234
263
  如果 `begin` 通过一个或多个 `--decision <id>` 声明了关联,那么 intent
235
264
  以 `completed` 或 `partial` 关闭前,必须用 `driftseal decision update` reconcile
236
265
  每一条关联 decision。update 可以改变当前 status,并会追加一条包含时间和
237
- intent ID 的 history。没有关联 decision 的 intent 仍沿用普通流程。
266
+ intent ID 的 history。没有关联 decision 的 intent 仍沿用普通流程。对于
267
+ acceptance-bound linked intent,所有 decision update 都必须发生在
268
+ `driftseal verify` 之前,因为 update 会改变 workspace fingerprint;顺序应当是
269
+ reconcile、verify、end。
238
270
 
239
271
  ## 回收已无意义的记录
240
272
 
@@ -69,10 +69,25 @@ function guarded(action) {
69
69
  }
70
70
 
71
71
  function registerTools(server, api, z) {
72
+ const verificationRecord = z.object({
73
+ id: z.string(),
74
+ passed: z.boolean(),
75
+ exitCode: z.number().int().nonnegative().nullable(),
76
+ signal: z.string().nullable(),
77
+ durationMs: z.number().int().nonnegative(),
78
+ outputHash: z.string().regex(/^[a-f0-9]{64}$/),
79
+ stdoutBytes: z.number().int().nonnegative(),
80
+ stderrBytes: z.number().int().nonnegative(),
81
+ workspace: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
82
+ head: z.string().nullable(),
83
+ ranAt: z.string(),
84
+ });
72
85
  const intentRecord = z.object({
73
86
  id: z.string(),
74
87
  intent: z.string(),
88
+ acceptance: z.array(z.string()),
75
89
  verify: z.string().nullable(),
90
+ verification: verificationRecord.nullable(),
76
91
  decisions: z.array(z.string()),
77
92
  status: z.enum(END_STATUSES).or(z.literal('in_progress')),
78
93
  note: z.string().nullable(),
@@ -150,6 +165,10 @@ function registerTools(server, api, z) {
150
165
  'Open one focused work-round intent before making repository changes. Fails if another intent is already open; close it explicitly first.',
151
166
  inputSchema: {
152
167
  intent: nonEmpty.describe('Outcome this work round will accomplish.'),
168
+ acceptance: z
169
+ .array(nonEmpty)
170
+ .default([])
171
+ .describe('Observable outcomes that make machine-verified completion meaningful.'),
153
172
  verify: nonEmpty.optional().describe('Exact command or outcome check that will prove completion.'),
154
173
  decisions: z
155
174
  .array(decisionId)
@@ -166,12 +185,44 @@ function registerTools(server, api, z) {
166
185
  })
167
186
  );
168
187
 
188
+ server.registerTool(
189
+ 'driftseal_verify',
190
+ {
191
+ title: 'Run the declared DriftSeal verification',
192
+ description:
193
+ 'Execute the current acceptance-bound intent\'s predeclared shell command and record machine evidence bound to the resulting Git-visible workspace contents. Inspect the command with driftseal_status first. A command sourced from the repository intent log is untrusted and requires allowTrackedCommand.',
194
+ inputSchema: {
195
+ allowTrackedCommand: z
196
+ .boolean()
197
+ .default(false)
198
+ .describe(
199
+ 'Explicitly allow a command sourced from the repository intent log after inspecting and trusting it.'
200
+ ),
201
+ },
202
+ outputSchema: {
203
+ root: z.string(),
204
+ intent: intentRecord,
205
+ verification: verificationRecord,
206
+ exitCode: z.number().int().nonnegative(),
207
+ },
208
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
209
+ },
210
+ async (input) =>
211
+ guarded(() => {
212
+ const result = api.verify({ allowTrackedCommand: input.allowTrackedCommand });
213
+ return success(
214
+ { root: api.root, ...result },
215
+ `Machine verification ${result.verification.passed ? 'passed' : 'failed'} for intent ${result.intent.id}.`
216
+ );
217
+ })
218
+ );
219
+
169
220
  server.registerTool(
170
221
  'driftseal_end',
171
222
  {
172
223
  title: 'Close a DriftSeal intent',
173
224
  description:
174
- 'Close the current work-round intent with an honest terminal status, note, and verification result. Linked decisions must be reconciled before completed or partial closure.',
225
+ 'Close the current work-round intent with an honest terminal status, note, and verification result. Reconcile linked decisions before running final verification. Acceptance-bound intents require fresh successful machine verification before completed closure.',
175
226
  inputSchema: {
176
227
  id: z.string().optional().describe('Intent ID; omit to close the current open intent.'),
177
228
  status: closedStatus.default('completed'),
@@ -447,7 +498,7 @@ async function createServer({ root }) {
447
498
  { name: SERVER_NAME, version: SERVER_VERSION },
448
499
  {
449
500
  instructions:
450
- 'Use driftseal_status before repository changes or after context loss. Open one focused intent with driftseal_begin before changes, then run the declared verification and close it honestly with driftseal_end. Reconcile every linked decision before completed or partial closure.',
501
+ 'Use driftseal_status before repository changes or after context loss. Open one focused intent with driftseal_begin before changes. Reconcile every linked decision before verification. When the intent declares acceptance criteria, inspect its command and use driftseal_verify to capture machine evidence before completed closure; a command sourced from the repository log requires explicit allowTrackedCommand after it is trusted. Otherwise run the declared check directly. Close honestly with driftseal_end.',
451
502
  }
452
503
  );
453
504
  registerTools(server, api, z);
package/bin/driftseal.js CHANGED
@@ -7,12 +7,14 @@
7
7
  * Intent-level write-ahead log and MADR decision log for agentic coding sessions.
8
8
  *
9
9
  * Protocol per work round:
10
- * 1. driftseal begin "<intent>" [--verify "<how to verify>"] (before changes that may need a rollback)
10
+ * 1. driftseal begin "<intent>" [--accept "<observable outcome>"] [--verify "<command>"]
11
11
  * 2. execute the intent
12
- * 3. driftseal end [--status ...] [--note ...] [--verify-result ...] (reconcile against intent)
12
+ * 3. driftseal verify (for acceptance-bound machine evidence)
13
+ * 4. driftseal end [--status ...] [--note ...] [--verify-result ...] (reconcile against intent)
13
14
  *
14
15
  * Events are appended to an append-only JSONL log (WAL semantics):
15
- * { "type": "begin", "id", "ts", "intent", "verify" }
16
+ * { "type": "begin", "id", "ts", "intent", "acceptance", "verify" }
17
+ * { "type": "verify", "id", "ts", "command", "passed", "workspace" }
16
18
  * { "type": "end", "id", "ts", "status", "note", "verifyResult" }
17
19
  *
18
20
  * Intent log: $DRIFTSEAL_HOME/events.jsonl, or .intent-log/events.jsonl in cwd.
@@ -25,7 +27,7 @@ const path = require('path');
25
27
  const crypto = require('crypto');
26
28
  const os = require('os');
27
29
  const { isDeepStrictEqual } = require('util');
28
- const { execFileSync } = require('child_process');
30
+ const { execFileSync, spawnSync } = require('child_process');
29
31
  const { version: PACKAGE_VERSION } = require('../package.json');
30
32
 
31
33
  const END_STATUSES = ['completed', 'partial', 'failed', 'abandoned'];
@@ -37,8 +39,8 @@ const DECISION_STATUSES = [
37
39
  'deprecated',
38
40
  'superseded',
39
41
  ];
40
- const EVENT_SCHEMA_VERSION = 3;
41
- const PROTOCOL_VERSION = 12;
42
+ const EVENT_SCHEMA_VERSION = 4;
43
+ const PROTOCOL_VERSION = 13;
42
44
  const DEFAULT_LOG_LANGUAGE = 'en';
43
45
  const IN_PROGRESS_GIT_PATH = 'driftseal-in-progress.jsonl';
44
46
  const LOCK_STALE_MS = 30 * 60 * 1000;
@@ -66,7 +68,9 @@ class HelpRequested extends DriftSealError {
66
68
  /** Single source of truth for per-command usage lines. */
67
69
  function usageFor(key) {
68
70
  const lines = {
69
- begin: 'usage: driftseal begin "<intent>" [--verify "<how to verify>"] [--decision <id>] [--force]',
71
+ begin:
72
+ 'usage: driftseal begin "<intent>" [--accept "<observable outcome>"] [--verify "<command>"] [--decision <id>] [--force]',
73
+ verify: 'usage: driftseal verify [--allow-tracked-command]',
70
74
  end: 'usage: driftseal end [id] [options]',
71
75
  status: 'usage: driftseal status',
72
76
  log: 'usage: driftseal log [--last N] [--all]',
@@ -121,6 +125,15 @@ function writeOutput(value) {
121
125
  process.stdout.write(text);
122
126
  }
123
127
 
128
+ function writeErrorOutput(value) {
129
+ const text = String(value);
130
+ if (activeOutput) {
131
+ activeOutput.stderr += text;
132
+ return;
133
+ }
134
+ process.stderr.write(text);
135
+ }
136
+
124
137
  if (process.env._DRIFTSEAL_TEST_UMASK) {
125
138
  process.umask(Number.parseInt(process.env._DRIFTSEAL_TEST_UMASK, 8));
126
139
  }
@@ -168,15 +181,65 @@ function normalizeEvent(event, line) {
168
181
  if (!Array.isArray(event.decisions) && event.decisions !== undefined) {
169
182
  fail(`invalid decisions list on log line ${line}`);
170
183
  }
184
+ if (!Array.isArray(event.acceptance) && event.acceptance !== undefined) {
185
+ fail(`invalid acceptance list on log line ${line}`);
186
+ }
187
+ const acceptance = event.acceptance || [];
188
+ if (acceptance.some((criterion) => typeof criterion !== 'string' || criterion.trim().length === 0)) {
189
+ fail(`invalid acceptance criterion on log line ${line}`);
190
+ }
191
+ if (acceptance.length > 0 && (typeof event.verify !== 'string' || event.verify.trim().length === 0)) {
192
+ fail(`acceptance-bound intent has no verification command on log line ${line}`);
193
+ }
171
194
  const decisions = (event.decisions || []).map(normalizeDecisionId);
172
195
  if (new Set(decisions).size !== decisions.length) {
173
196
  fail(`duplicate linked decision on log line ${line}`);
174
197
  }
175
- return { ...event, decisions, head: normalizeHead(event.head) };
198
+ return { ...event, acceptance, decisions, head: normalizeHead(event.head) };
199
+ }
200
+
201
+ if (event.type === 'verify') {
202
+ if (
203
+ typeof event.verificationId !== 'string' ||
204
+ event.verificationId.length === 0 ||
205
+ typeof event.command !== 'string' ||
206
+ event.command.trim().length === 0 ||
207
+ typeof event.passed !== 'boolean' ||
208
+ (event.exitCode !== null && (!Number.isInteger(event.exitCode) || event.exitCode < 0)) ||
209
+ (event.signal !== null && typeof event.signal !== 'string') ||
210
+ !Number.isSafeInteger(event.durationMs) ||
211
+ event.durationMs < 0 ||
212
+ !Number.isSafeInteger(event.stdoutBytes) ||
213
+ event.stdoutBytes < 0 ||
214
+ !Number.isSafeInteger(event.stderrBytes) ||
215
+ event.stderrBytes < 0 ||
216
+ typeof event.outputHash !== 'string' ||
217
+ !/^[a-f0-9]{64}$/.test(event.outputHash) ||
218
+ (event.workspace !== null &&
219
+ (typeof event.workspace !== 'string' || !/^[a-f0-9]{64}$/.test(event.workspace))) ||
220
+ event.passed !== (event.exitCode === 0 && event.signal === null)
221
+ ) {
222
+ fail(`invalid verification event on log line ${line}`);
223
+ }
224
+ return { ...event, head: normalizeHead(event.head) };
176
225
  }
177
226
 
178
227
  if (event.type === 'end') {
179
228
  if (!END_STATUSES.includes(event.status)) fail(`invalid end event on log line ${line}`);
229
+ if (
230
+ event.verificationId !== undefined &&
231
+ event.verificationId !== null &&
232
+ (typeof event.verificationId !== 'string' || event.verificationId.length === 0)
233
+ ) {
234
+ fail(`invalid end verification id on log line ${line}`);
235
+ }
236
+ if (
237
+ event.workspace !== undefined &&
238
+ event.workspace !== null &&
239
+ (typeof event.workspace !== 'string' || !/^[a-f0-9]{64}$/.test(event.workspace))
240
+ ) {
241
+ fail(`invalid end workspace on log line ${line}`);
242
+ }
180
243
  return { ...event, head: normalizeHead(event.head) };
181
244
  }
182
245
 
@@ -817,6 +880,7 @@ function fold(events) {
817
880
  id: ev.id,
818
881
  tsBegin: ev.ts,
819
882
  intent: ev.intent,
883
+ acceptance: Array.isArray(ev.acceptance) ? ev.acceptance : [],
820
884
  verify: ev.verify || null,
821
885
  beginHead: ev.head || null,
822
886
  decisions: Array.isArray(ev.decisions) ? ev.decisions : [],
@@ -824,6 +888,8 @@ function fold(events) {
824
888
  decisionPrepares: [],
825
889
  decisionTerminals: [],
826
890
  decisionUpdates: [],
891
+ verificationAttempts: [],
892
+ verification: null,
827
893
  status: 'in_progress',
828
894
  tsEnd: null,
829
895
  note: null,
@@ -834,6 +900,20 @@ function fold(events) {
834
900
  reclaimedAt: null,
835
901
  });
836
902
  order.push(ev.id);
903
+ } else if (ev.type === 'verify') {
904
+ const rec = records.get(ev.id);
905
+ if (!rec) fail(`verification event references unknown intent id: ${ev.id}`);
906
+ if (rec.status !== 'in_progress') {
907
+ fail(`verification occurred after intent ${ev.id} was closed`);
908
+ }
909
+ if (rec.acceptance.length === 0 || !rec.verify) {
910
+ fail(`verification event references intent ${ev.id} without acceptance criteria`);
911
+ }
912
+ if (ev.command !== rec.verify) {
913
+ fail(`verification command does not match intent ${ev.id}`);
914
+ }
915
+ rec.verificationAttempts.push(ev);
916
+ rec.verification = ev;
837
917
  } else if (ev.type === 'reclaim' || ev.type === 'unreclaim') {
838
918
  const rec = records.get(ev.id);
839
919
  if (!rec) fail(`${ev.type} event references unknown intent id: ${ev.id}`);
@@ -877,6 +957,18 @@ function fold(events) {
877
957
  ) {
878
958
  fail(`linked intent ${ev.id} was closed without reconciling every declared decision`);
879
959
  }
960
+ if (ev.status === 'completed' && rec.acceptance.length > 0) {
961
+ if (!rec.verification || !rec.verification.passed) {
962
+ fail(`acceptance-bound intent ${ev.id} was completed without successful machine verification`);
963
+ }
964
+ if (
965
+ (ev.schemaVersion || 1) < 4 ||
966
+ ev.verificationId !== rec.verification.verificationId ||
967
+ (ev.workspace ?? null) !== rec.verification.workspace
968
+ ) {
969
+ fail(`acceptance-bound intent ${ev.id} was completed with stale machine verification`);
970
+ }
971
+ }
880
972
  rec.status = ev.status;
881
973
  rec.tsEnd = ev.ts;
882
974
  rec.note = ev.note || null;
@@ -1335,8 +1427,19 @@ function parseArgs(argv, spec, usageKey) {
1335
1427
  function render(rec) {
1336
1428
  const lines = [`[${rec.id}] ${rec.status}`];
1337
1429
  lines.push(` intent: ${rec.intent}`);
1430
+ for (const criterion of rec.acceptance) lines.push(` accept: ${criterion}`);
1338
1431
  if (rec.decisions.length > 0) lines.push(` decisions: ${rec.decisions.join(', ')}`);
1339
1432
  if (rec.verify) lines.push(` verify: ${rec.verify}`);
1433
+ if (rec.verification) {
1434
+ const state = rec.verification.passed ? 'passed' : 'failed';
1435
+ const workspace = rec.verification.workspace
1436
+ ? `, workspace ${rec.verification.workspace.slice(0, 12)}`
1437
+ : ', workspace unavailable';
1438
+ lines.push(
1439
+ ` machine-verification: ${state} (exit ${rec.verification.exitCode ?? '-'}, ` +
1440
+ `${rec.verification.durationMs} ms${workspace})`
1441
+ );
1442
+ }
1340
1443
  if (rec.verifyResult) lines.push(` verify-result: ${rec.verifyResult}`);
1341
1444
  if (rec.note) lines.push(` note: ${rec.note}`);
1342
1445
  if (rec.beginHead || rec.endHead) {
@@ -1347,12 +1450,31 @@ function render(rec) {
1347
1450
  return lines.join('\n');
1348
1451
  }
1349
1452
 
1453
+ function publicVerification(verification) {
1454
+ if (!verification) return null;
1455
+ return {
1456
+ id: verification.verificationId,
1457
+ passed: verification.passed,
1458
+ exitCode: verification.exitCode,
1459
+ signal: verification.signal,
1460
+ durationMs: verification.durationMs,
1461
+ outputHash: verification.outputHash,
1462
+ stdoutBytes: verification.stdoutBytes,
1463
+ stderrBytes: verification.stderrBytes,
1464
+ workspace: verification.workspace,
1465
+ head: verification.head,
1466
+ ranAt: verification.ts,
1467
+ };
1468
+ }
1469
+
1350
1470
  function publicIntent(rec) {
1351
1471
  if (!rec) return null;
1352
1472
  return {
1353
1473
  id: rec.id,
1354
1474
  intent: rec.intent,
1475
+ acceptance: [...rec.acceptance],
1355
1476
  verify: rec.verify,
1477
+ verification: publicVerification(rec.verification),
1356
1478
  decisions: [...rec.decisions],
1357
1479
  status: rec.status,
1358
1480
  note: rec.note,
@@ -1634,7 +1756,8 @@ ${intentLogLanguageParagraph(language)}
1634
1756
 
1635
1757
  1. **Write intent first**, before modifying, creating, or deleting files, or
1636
1758
  making any other non-Git change that may need a rollback:
1637
- \`driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"\`.
1759
+ \`driftseal begin "<what this round will accomplish>" --accept "<observable outcome>" --verify "<exact command that proves it>"\`.
1760
+ Repeat \`--accept\` when completion has multiple independently observable criteria.
1638
1761
  Add one \`--decision <id>\` for each existing decision this round may change.
1639
1762
  Git operations never need an intent and are not included in the intent log;
1640
1763
  Git maintains their history. This includes inspection, branch and worktree
@@ -1649,8 +1772,17 @@ ${intentLogLanguageParagraph(language)}
1649
1772
  and can be verified on its own.
1650
1773
  2. **Execute only the intent.** Scope change? Close the current intent
1651
1774
  (\`driftseal end -s partial|abandoned -n "<why>"\`) and \`driftseal begin\` a new one.
1652
- 3. **Verify, then close**: run the declared verification, then
1653
- \`driftseal end -s completed|partial|failed|abandoned -n "<what happened>" -r "<what the verification showed, written for the next agent>"\`.
1775
+ 3. **Reconcile, verify, then close**: for a linked intent, first reconcile every
1776
+ declared decision as described below. For an acceptance-bound intent, inspect the
1777
+ exact command shown by \`driftseal status\`, then run \`driftseal verify\` to execute it
1778
+ and bind its exit status to the current Git-visible workspace contents. A command
1779
+ sourced from the repository intent log is untrusted and requires
1780
+ \`--allow-tracked-command\` after inspection; locally parked commands do not.
1781
+ An intent without \`--accept\` uses its declared check directly. Then run
1782
+ \`driftseal end -s completed|partial|failed|abandoned -n "<what happened>" -r "<optional context for the next agent>"\`.
1783
+ DriftSeal rejects \`completed\` when machine verification failed, never ran, or
1784
+ the workspace changed after it. Ignored files are outside the workspace fingerprint.
1785
+ Outside a Git worktree, only the recorded exit status is available.
1654
1786
  Never report success without closing the intent.
1655
1787
  Before closing a linked intent as \`completed\` or \`partial\`, reconcile every
1656
1788
  declared decision with \`driftseal decision update <id> --status <status> --note "<why>"\`.
@@ -1682,8 +1814,30 @@ Log: \`.intent-log/events.jsonl\` (override with \`$DRIFTSEAL_HOME\`); ${localLo
1682
1814
  ${INTENT_PROTOCOL_END}`;
1683
1815
  }
1684
1816
 
1685
- function previousIntentProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE) {
1686
- const v11 = intentProtocolBlock(version, language)
1817
+ function previousIntentProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
1818
+ const v12 = intentProtocolBlock(version, language, localLog)
1819
+ .replace(
1820
+ ' `driftseal begin "<what this round will accomplish>" --accept "<observable outcome>" --verify "<exact command that proves it>"`.\n' +
1821
+ ' Repeat `--accept` when completion has multiple independently observable criteria.',
1822
+ ' `driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"`.'
1823
+ )
1824
+ .replace(
1825
+ '3. **Reconcile, verify, then close**: for a linked intent, first reconcile every\n' +
1826
+ ' declared decision as described below. For an acceptance-bound intent, inspect the\n' +
1827
+ ' exact command shown by `driftseal status`, then run `driftseal verify` to execute it\n' +
1828
+ ' and bind its exit status to the current Git-visible workspace contents. A command\n' +
1829
+ ' sourced from the repository intent log is untrusted and requires\n' +
1830
+ ' `--allow-tracked-command` after inspection; locally parked commands do not.\n' +
1831
+ ' An intent without `--accept` uses its declared check directly. Then run\n' +
1832
+ ' `driftseal end -s completed|partial|failed|abandoned -n "<what happened>" -r "<optional context for the next agent>"`.\n' +
1833
+ ' DriftSeal rejects `completed` when machine verification failed, never ran, or\n' +
1834
+ ' the workspace changed after it. Ignored files are outside the workspace fingerprint.\n' +
1835
+ ' Outside a Git worktree, only the recorded exit status is available.',
1836
+ '3. **Verify, then close**: run the declared verification, then\n' +
1837
+ ' `driftseal end -s completed|partial|failed|abandoned -n "<what happened>" -r "<what the verification showed, written for the next agent>"`.'
1838
+ );
1839
+ if (version >= 12) return v12;
1840
+ const v11 = v12
1687
1841
  .replace(
1688
1842
  '-r "<what the verification showed, written for the next agent>"',
1689
1843
  '-r "<verify output>"'
@@ -1876,9 +2030,9 @@ When an intent declares an existing decision with \`--decision <id>\`, use
1876
2030
  Commit \`.decision-log/\` with the code.`;
1877
2031
  }
1878
2032
 
1879
- function previousDecisionProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE) {
1880
- if (version >= 11) return decisionProtocolBlock(version, language);
1881
- const v10 = stripDecisionLogLanguage(decisionProtocolBlock(version, language), language);
2033
+ function previousDecisionProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
2034
+ if (version >= 11) return decisionProtocolBlock(version, language, localLog);
2035
+ const v10 = stripDecisionLogLanguage(decisionProtocolBlock(version, language, localLog), language);
1882
2036
  if (version >= 9) return v10;
1883
2037
  const v8 = v10.replace(
1884
2038
  '\nAfter a merge, colliding decision ids are remapped with `driftseal absorb`;\n' +
@@ -2202,8 +2356,8 @@ function skillInstallLocation(target, scope, root) {
2202
2356
  global: path.join(home, '.agents', 'skills'),
2203
2357
  },
2204
2358
  'kimi-code': {
2205
- project: path.join(root, '.kimi', 'skills'),
2206
- global: path.join(home, '.kimi', 'skills'),
2359
+ project: path.join(root, '.kimi-code', 'skills'),
2360
+ global: path.join(home, '.kimi-code', 'skills'),
2207
2361
  },
2208
2362
  opencode: {
2209
2363
  project: path.join(root, '.opencode', 'skills'),
@@ -2627,16 +2781,21 @@ function hookReminder(event, { readOnly = false } = {}) {
2627
2781
  if (event === 'prompt') {
2628
2782
  return (
2629
2783
  'DriftSeal reminder: if this round will modify files or anything else that may need a ' +
2630
- 'rollback, begin an intent first: driftseal begin "<intent>" --verify "<check>". ' +
2784
+ 'rollback, begin an intent first: driftseal begin "<intent>" --accept "<observable outcome>" ' +
2785
+ '--verify "<command>". ' +
2631
2786
  'Questions, read-only exploration, and single-step checks need no intent — skip this ' +
2632
2787
  'reminder when it does not apply.'
2633
2788
  );
2634
2789
  }
2635
2790
  const open = openIntent(fold(readEvents({ file, readOnly })));
2636
2791
  if (open) {
2792
+ const reconciliation = open.decisions.length > 0 ? 'reconcile every linked decision, then ' : '';
2793
+ const verification = open.acceptance.length > 0
2794
+ ? `${reconciliation}inspect and run driftseal verify, then close it with driftseal end`
2795
+ : `${reconciliation}run the declared verification, then close it with driftseal end`;
2637
2796
  return (
2638
2797
  `DriftSeal reminder: intent ${open.id} is still in_progress: "${open.intent}". ` +
2639
- 'If its work is done, run the declared verification and close it with driftseal end; ' +
2798
+ `If its work is done, ${verification}; ` +
2640
2799
  'if this turn was unrelated, ignore this reminder.'
2641
2800
  );
2642
2801
  }
@@ -2711,6 +2870,72 @@ function isGitWorkTree(cwd = process.cwd()) {
2711
2870
  return gitCapture(['rev-parse', '--is-inside-work-tree'], cwd) === 'true';
2712
2871
  }
2713
2872
 
2873
+ /**
2874
+ * Hash the material Git-visible workspace contents rather than trusting the
2875
+ * current commit alone. Any tracked or untracked (non-ignored) content change
2876
+ * makes the verification stale.
2877
+ * The intent event log is excluded because recording verification and closure
2878
+ * necessarily appends to it.
2879
+ */
2880
+ function workspaceFingerprint(cwd = process.cwd()) {
2881
+ if (!isGitWorkTree(cwd)) return null;
2882
+ const root = gitCaptureLine(['rev-parse', '--show-toplevel'], cwd);
2883
+ if (!root) return null;
2884
+ const listing = gitCaptureRaw(
2885
+ ['ls-files', '-z', '--cached', '--others', '--exclude-standard'],
2886
+ root
2887
+ );
2888
+ if (listing === null) return null;
2889
+
2890
+ const excludedFile = path.resolve(logFile());
2891
+ const excludedLockPrefixes = [logDir(), decisionDir()].map((directory) =>
2892
+ path.resolve(directory, '.driftseal.lock')
2893
+ );
2894
+ const files = [...new Set(listing.split('\0').filter(Boolean))].sort();
2895
+ const hash = crypto.createHash('sha256');
2896
+ for (const relative of files) {
2897
+ const target = path.resolve(root, relative);
2898
+ if (
2899
+ target === excludedFile ||
2900
+ excludedLockPrefixes.some(
2901
+ (prefix) => target === prefix || target.startsWith(`${prefix}${path.sep}`) || target.startsWith(`${prefix}.stale.`)
2902
+ )
2903
+ ) {
2904
+ continue;
2905
+ }
2906
+ hash.update(relative, 'utf8');
2907
+ hash.update('\0');
2908
+ let stat;
2909
+ try {
2910
+ stat = fs.lstatSync(target);
2911
+ } catch (err) {
2912
+ if (err.code === 'ENOENT') {
2913
+ hash.update('missing\0');
2914
+ continue;
2915
+ }
2916
+ throw err;
2917
+ }
2918
+ hash.update(String(stat.mode & 0o111));
2919
+ hash.update('\0');
2920
+ if (stat.isSymbolicLink()) {
2921
+ hash.update('symlink\0');
2922
+ hash.update(fs.readlinkSync(target));
2923
+ } else if (stat.isFile()) {
2924
+ hash.update('file\0');
2925
+ hash.update(fs.readFileSync(target));
2926
+ } else if (stat.isDirectory()) {
2927
+ hash.update('directory\0');
2928
+ hash.update(gitCapture(['rev-parse', 'HEAD'], target) || 'no-head');
2929
+ hash.update('\0');
2930
+ hash.update(gitCaptureRaw(['status', '--porcelain=v1', '-z'], target) || '');
2931
+ } else {
2932
+ hash.update('other\0');
2933
+ }
2934
+ hash.update('\0');
2935
+ }
2936
+ return hash.digest('hex');
2937
+ }
2938
+
2714
2939
  /**
2715
2940
  * Warn (without mutating the index or .gitignore) when local log mode is on
2716
2941
  * but the default log paths are still tracked by git. The log directories are
@@ -3577,9 +3802,103 @@ function absorbGit(baseFile, oursFile, theirsFile, { abandon, dryRun }) {
3577
3802
  });
3578
3803
  }
3579
3804
 
3805
+ function runMachineVerification({ allowTrackedCommand = false } = {}) {
3806
+ const snapshot = withMutationLocks([logDir()], () => {
3807
+ const intent = openIntent(fold(readEvents({ repairTail: true })));
3808
+ if (!intent) fail('no intent in progress; nothing to verify');
3809
+ if (intent.acceptance.length === 0) {
3810
+ fail(`intent ${intent.id} has no acceptance criteria; declare them with driftseal begin --accept`);
3811
+ }
3812
+ if (!intent.verify) fail(`intent ${intent.id} has no verification command`);
3813
+ const park = inProgressFile();
3814
+ const parked = park && fs.existsSync(park) ? parkedOpenIntent(park) : null;
3815
+ return {
3816
+ id: intent.id,
3817
+ command: intent.verify,
3818
+ fromTrackedLog: isParkableIntentLog() && (!parked || parked.id !== intent.id),
3819
+ };
3820
+ });
3821
+
3822
+ const displayedCommand = JSON.stringify(snapshot.command);
3823
+ printError(`verification command: ${displayedCommand}`);
3824
+ if (snapshot.fromTrackedLog && !allowTrackedCommand) {
3825
+ fail(
3826
+ `refusing to execute a verification command sourced from the repository intent log: ${displayedCommand}\n` +
3827
+ 'inspect the command, then re-run with --allow-tracked-command only if you trust it'
3828
+ );
3829
+ }
3830
+
3831
+ const started = process.hrtime.bigint();
3832
+ const result = spawnSync(snapshot.command, {
3833
+ cwd: process.cwd(),
3834
+ env: process.env,
3835
+ shell: true,
3836
+ encoding: 'utf8',
3837
+ stdio: ['ignore', 'pipe', 'pipe'],
3838
+ maxBuffer: 16 * 1024 * 1024,
3839
+ });
3840
+ const durationMs = Number((process.hrtime.bigint() - started) / 1000000n);
3841
+ const stdout = result.stdout || '';
3842
+ let stderr = result.stderr || '';
3843
+ if (result.error) {
3844
+ stderr += `${stderr && !stderr.endsWith('\n') ? '\n' : ''}${result.error.message}\n`;
3845
+ }
3846
+ if (stdout) {
3847
+ writeOutput(stdout);
3848
+ if (!stdout.endsWith('\n')) printLine();
3849
+ }
3850
+ if (stderr) {
3851
+ writeErrorOutput(stderr);
3852
+ if (!stderr.endsWith('\n')) printError();
3853
+ }
3854
+
3855
+ const exitCode = Number.isInteger(result.status) ? result.status : 1;
3856
+ const signal = typeof result.signal === 'string' ? result.signal : null;
3857
+ const passed = exitCode === 0 && signal === null;
3858
+ const outputHash = crypto
3859
+ .createHash('sha256')
3860
+ .update(stdout, 'utf8')
3861
+ .update('\0')
3862
+ .update(stderr, 'utf8')
3863
+ .digest('hex');
3864
+ const verificationEvent = {
3865
+ type: 'verify',
3866
+ id: snapshot.id,
3867
+ verificationId: crypto.randomUUID(),
3868
+ ts: new Date().toISOString(),
3869
+ command: snapshot.command,
3870
+ passed,
3871
+ exitCode,
3872
+ signal,
3873
+ durationMs,
3874
+ outputHash,
3875
+ stdoutBytes: Buffer.byteLength(stdout),
3876
+ stderrBytes: Buffer.byteLength(stderr),
3877
+ workspace: workspaceFingerprint(),
3878
+ head: gitCapture(['rev-parse', 'HEAD']),
3879
+ };
3880
+
3881
+ const intent = withMutationLocks([logDir()], () => {
3882
+ const events = readEvents({ repairTail: true });
3883
+ const current = openIntent(fold(events));
3884
+ if (!current || current.id !== snapshot.id || current.verify !== snapshot.command) {
3885
+ fail(`intent ${snapshot.id} changed while its verification command was running`);
3886
+ }
3887
+ events.push(appendEvent(verificationEvent));
3888
+ return fold(events).find((candidate) => candidate.id === snapshot.id);
3889
+ });
3890
+ printLine(`${snapshot.id} verification ${passed ? 'passed' : 'failed'} (exit ${exitCode})`);
3891
+ return {
3892
+ intent: publicIntent(intent),
3893
+ verification: publicVerification(intent.verification),
3894
+ exitCode,
3895
+ };
3896
+ }
3897
+
3580
3898
  const commands = {
3581
3899
  begin(argv) {
3582
3900
  const { positionals, flags } = parseArgs(argv, {
3901
+ accept: 'multiple',
3583
3902
  verify: '-v',
3584
3903
  decision: 'multiple',
3585
3904
  force: 'boolean',
@@ -3588,6 +3907,13 @@ const commands = {
3588
3907
  if (!intent) {
3589
3908
  fail(usageFor('begin'));
3590
3909
  }
3910
+ const acceptance = [...new Set((flags.accept || []).map((criterion) => criterion.trim()))];
3911
+ if (acceptance.some((criterion) => criterion.length === 0)) {
3912
+ fail('--accept requires a non-empty observable outcome');
3913
+ }
3914
+ if (acceptance.length > 0 && (!flags.verify || flags.verify.trim().length === 0)) {
3915
+ fail('--accept requires --verify with the exact machine verification command');
3916
+ }
3591
3917
  const requestedDecisions = flags.decision || [];
3592
3918
  const index = requestedDecisions.length > 0 ? decisionIndex() : [];
3593
3919
  const decisions = [
@@ -3628,6 +3954,7 @@ const commands = {
3628
3954
  id,
3629
3955
  ts: new Date().toISOString(),
3630
3956
  intent,
3957
+ acceptance,
3631
3958
  verify: flags.verify || null,
3632
3959
  decisions,
3633
3960
  head: gitCapture(['rev-parse', 'HEAD']),
@@ -3637,6 +3964,16 @@ const commands = {
3637
3964
  return publicIntent(record);
3638
3965
  },
3639
3966
 
3967
+ verify(argv) {
3968
+ const { positionals, flags } = parseArgs(
3969
+ argv,
3970
+ { 'allow-tracked-command': 'boolean' },
3971
+ 'verify'
3972
+ );
3973
+ if (positionals.length > 0) fail(usageFor('verify'));
3974
+ return runMachineVerification({ allowTrackedCommand: flags['allow-tracked-command'] === true });
3975
+ },
3976
+
3640
3977
  end(argv) {
3641
3978
  const { positionals, flags } = parseArgs(argv, {
3642
3979
  status: '-s',
@@ -3661,6 +3998,25 @@ const commands = {
3661
3998
  if (!target) fail('no intent in progress; nothing to end');
3662
3999
  }
3663
4000
 
4001
+ let completionWorkspace = null;
4002
+ let completionVerificationId = null;
4003
+ if (status === 'completed' && target.acceptance.length > 0) {
4004
+ if (!target.verification || !target.verification.passed) {
4005
+ fail(
4006
+ `cannot complete acceptance-bound intent ${target.id} without successful machine verification; ` +
4007
+ 'run: driftseal verify'
4008
+ );
4009
+ }
4010
+ completionWorkspace = workspaceFingerprint();
4011
+ if (completionWorkspace !== target.verification.workspace) {
4012
+ fail(
4013
+ `cannot complete acceptance-bound intent ${target.id}: workspace changed after machine verification; ` +
4014
+ 'run: driftseal verify'
4015
+ );
4016
+ }
4017
+ completionVerificationId = target.verification.verificationId;
4018
+ }
4019
+
3664
4020
  if (['failed', 'abandoned'].includes(status)) {
3665
4021
  const terminalStatus = closeIntentAsEscape(
3666
4022
  events,
@@ -3715,6 +4071,8 @@ const commands = {
3715
4071
  status,
3716
4072
  note: flags.note || null,
3717
4073
  verifyResult: flags['verify-result'] || null,
4074
+ verificationId: completionVerificationId,
4075
+ workspace: completionWorkspace,
3718
4076
  head: gitCapture(['rev-parse', 'HEAD']),
3719
4077
  }));
3720
4078
  const record = fold(events).find((candidate) => candidate.id === target.id);
@@ -4059,7 +4417,10 @@ const commands = {
4059
4417
  knownManagedBlocks: [
4060
4418
  ...sourceLanguages.flatMap((source) => [
4061
4419
  protocolEol(intentProtocolBlock(PROTOCOL_VERSION, source), eol),
4420
+ protocolEol(previousIntentProtocolBlock(12, source), eol),
4421
+ protocolEol(previousIntentProtocolBlock(12, source, true), eol),
4062
4422
  protocolEol(previousIntentProtocolBlock(11, source), eol),
4423
+ protocolEol(previousIntentProtocolBlock(11, source, true), eol),
4063
4424
  ]),
4064
4425
  protocolEol(previousIntentProtocolBlock(2), eol),
4065
4426
  protocolEol(previousIntentProtocolBlock(3), eol),
@@ -4083,7 +4444,10 @@ const commands = {
4083
4444
  knownManagedBlocks: [
4084
4445
  ...sourceLanguages.flatMap((source) => [
4085
4446
  protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, source), eol),
4447
+ protocolEol(previousDecisionProtocolBlock(12, source), eol),
4448
+ protocolEol(previousDecisionProtocolBlock(12, source, true), eol),
4086
4449
  protocolEol(previousDecisionProtocolBlock(11, source), eol),
4450
+ protocolEol(previousDecisionProtocolBlock(11, source, true), eol),
4087
4451
  ]),
4088
4452
  protocolEol(previousDecisionProtocolBlock(2), eol),
4089
4453
  protocolEol(previousDecisionProtocolBlock(3), eol),
@@ -4146,7 +4510,11 @@ const commands = {
4146
4510
  Intent-level write-ahead log for agent sessions.
4147
4511
 
4148
4512
  usage:
4149
- driftseal begin "<intent>" [--verify "<how to verify>"] [--decision <id>] [--force]
4513
+ driftseal begin "<intent>" [--accept "<observable outcome>"] [--verify "<command>"]
4514
+ [--decision <id>] [--force]
4515
+ driftseal verify [--allow-tracked-command]
4516
+ run the declared command and bind its result
4517
+ to the current Git-visible workspace contents
4150
4518
  driftseal end [id] [--status completed|partial|failed|abandoned] [--note "..."] [--verify-result "..."]
4151
4519
  driftseal status show the intent currently in progress (re-anchor after drift)
4152
4520
  driftseal log [--last N] [--all] show intent history (--all includes reclaimed records)
@@ -4214,7 +4582,7 @@ function requestedEndStatus(argv) {
4214
4582
  * subcommand, mirroring the value-taking entries of each parseArgs spec.
4215
4583
  */
4216
4584
  const VALUE_TAKING_FLAGS = {
4217
- begin: ['--verify', '-v', '--decision'],
4585
+ begin: ['--accept', '--verify', '-v', '--decision'],
4218
4586
  end: ['--status', '-s', '--note', '-n', '--verify-result', '-r'],
4219
4587
  log: ['--last', '-n'],
4220
4588
  reclaim: ['--reason', '-r', '--older-than'],
@@ -4319,7 +4687,11 @@ function dispatch(argv) {
4319
4687
  const data = withMutationLocks(resources, () => fn(rest));
4320
4688
  return { data, exitCode: data && Number.isInteger(data.exitCode) ? data.exitCode : 0 };
4321
4689
  }
4322
- return { data: fn(rest), exitCode: 0 };
4690
+ const data = fn(rest);
4691
+ return {
4692
+ data,
4693
+ exitCode: data && Number.isInteger(data.exitCode) ? data.exitCode : 0,
4694
+ };
4323
4695
  } catch (err) {
4324
4696
  if (err instanceof HelpRequested) {
4325
4697
  printLine(usageFor(err.usageKey) || usageFor(cmd) || 'run: driftseal help');
@@ -4406,13 +4778,17 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
4406
4778
  status() {
4407
4779
  return call(['status']);
4408
4780
  },
4409
- begin({ intent, verify, decisions = [], force = false }) {
4781
+ begin({ intent, acceptance = [], verify, decisions = [], force = false }) {
4410
4782
  const argv = ['begin', intent];
4783
+ for (const criterion of acceptance) appendFlag(argv, '--accept', criterion);
4411
4784
  appendFlag(argv, '--verify', verify);
4412
4785
  for (const decision of decisions) appendFlag(argv, '--decision', decision);
4413
4786
  if (force) argv.push('--force');
4414
4787
  return call(argv);
4415
4788
  },
4789
+ verify({ allowTrackedCommand = false } = {}) {
4790
+ return call(['verify', ...(allowTrackedCommand ? ['--allow-tracked-command'] : [])]);
4791
+ },
4416
4792
  end({ id, status, note, verifyResult } = {}) {
4417
4793
  const argv = ['end'];
4418
4794
  if (id) argv.push(String(id));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftseal",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Seal intent, verification, and decisions into an auditable workflow for agentic coding",
5
5
  "keywords": [
6
6
  "driftseal",