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 +38 -5
- package/README.zh-CN.md +37 -5
- package/bin/driftseal-mcp.js +53 -2
- package/bin/driftseal.js +400 -24
- package/package.json +1 -1
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
|
|
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,
|
|
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 "<
|
|
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
|
-
|
|
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 "<
|
|
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
|
|
package/bin/driftseal-mcp.js
CHANGED
|
@@ -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.
|
|
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
|
|
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>" [--
|
|
10
|
+
* 1. driftseal begin "<intent>" [--accept "<observable outcome>"] [--verify "<command>"]
|
|
11
11
|
* 2. execute the intent
|
|
12
|
-
* 3. driftseal
|
|
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 =
|
|
41
|
-
const PROTOCOL_VERSION =
|
|
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:
|
|
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
|
|
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. **
|
|
1653
|
-
|
|
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
|
|
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>" --
|
|
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
|
-
|
|
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>" [--
|
|
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
|
-
|
|
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));
|