driftseal 1.2.1 → 1.3.2
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 +48 -4
- package/README.zh-CN.md +43 -4
- package/bin/driftseal-mcp.js +53 -2
- package/bin/driftseal.js +614 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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,51 @@ 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. DriftSeal records local provenance when an intent is opened:
|
|
214
|
+
the default Git workflow parks the intent in Git metadata, while non-Git and
|
|
215
|
+
custom `DRIFTSEAL_HOME` workflows keep a small local marker outside the intent
|
|
216
|
+
log. Those locally created intents run normally. If an open intent arrives only
|
|
217
|
+
through an intent log, without matching local provenance, DriftSeal cannot confirm
|
|
218
|
+
who chose its command. It prints the command to stderr and refuses to execute it
|
|
219
|
+
until you inspect it and explicitly run
|
|
220
|
+
`driftseal verify --allow-tracked-command`. The programmatic API and MCP tool
|
|
221
|
+
expose the equivalent `allowTrackedCommand` opt-in. Local provenance state is
|
|
222
|
+
removed when the intent closes. Non-Git markers are bound to the local log
|
|
223
|
+
file's identity, so copying a marker with the log does not transfer trust. If
|
|
224
|
+
local provenance is lost or no longer matches, verification fails safe and
|
|
225
|
+
requires the same explicit opt-in.
|
|
226
|
+
|
|
227
|
+
The verification event records the command's exit status, duration, output
|
|
228
|
+
digest and byte counts, Git HEAD, and a fingerprint of every tracked or
|
|
229
|
+
untracked non-ignored workspace file except the intent event log. A successful
|
|
230
|
+
result becomes stale if those workspace contents change. DriftSeal therefore
|
|
231
|
+
rejects `completed` until the command passes again on the current workspace.
|
|
232
|
+
Command output is spooled to temporary files instead of a fixed in-memory
|
|
233
|
+
buffer, then replayed after the command exits and removed. Output size therefore
|
|
234
|
+
has no DriftSeal-defined limit, though it remains bounded by available disk space.
|
|
235
|
+
Ignored files are deliberately outside this fingerprint. Outside a Git
|
|
236
|
+
worktree the fingerprint is unavailable, so the gate proves only the command's
|
|
237
|
+
recorded exit status and cannot detect later content changes.
|
|
238
|
+
|
|
239
|
+
This proves that the declared command passed on recorded contents; it does not
|
|
240
|
+
prove that the acceptance criterion or test is adequate. Existing intents
|
|
241
|
+
without `--accept` retain the manual verification workflow for compatibility.
|
|
242
|
+
Use protected CI, independent review, or human approval when the verifier was
|
|
243
|
+
written by the same agent, the outcome is subjective, or the change is high risk.
|
|
203
244
|
|
|
204
245
|
```sh
|
|
205
246
|
driftseal end \
|
|
@@ -224,7 +265,8 @@ also need no intent. Any other non-Git content change starts a new work round.
|
|
|
224
265
|
|
|
225
266
|
| Command | Purpose |
|
|
226
267
|
| --- | --- |
|
|
227
|
-
| `driftseal begin "<intent>" [-v "<
|
|
268
|
+
| `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. |
|
|
269
|
+
| `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 without matching local provenance require the explicit opt-in. |
|
|
228
270
|
| `driftseal end [id] [-s status] [-n note] [-r verify-result]` | Close an intent honestly. |
|
|
229
271
|
| `driftseal status` | Show the intent currently in progress. |
|
|
230
272
|
| `driftseal log [-n N] [--all]` | Review intent history (`--all` includes reclaimed records). |
|
|
@@ -248,7 +290,9 @@ When `begin` declares one or more `--decision <id>` links, every linked
|
|
|
248
290
|
decision must be reconciled with `driftseal decision update` before that intent can
|
|
249
291
|
close as `completed` or `partial`. The update changes the current status when
|
|
250
292
|
requested and appends a timestamped history entry tied to the intent. Intents
|
|
251
|
-
without decision links keep the ordinary workflow.
|
|
293
|
+
without decision links keep the ordinary workflow. For acceptance-bound linked
|
|
294
|
+
intents, perform every decision update before `driftseal verify`, because a decision
|
|
295
|
+
update changes the workspace fingerprint: reconcile, verify, then end.
|
|
252
296
|
|
|
253
297
|
## Reclaiming noise records
|
|
254
298
|
|
package/README.zh-CN.md
CHANGED
|
@@ -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,45 @@ 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
|
+
不是被动的日志数据。创建 intent 时,DriftSeal 会记录本地 provenance:默认 Git
|
|
203
|
+
流程把 open intent park 在 Git metadata 中;非 Git 环境和自定义
|
|
204
|
+
`DRIFTSEAL_HOME` 则在 intent log 之外保存一个很小的本地标记。这些本地创建的
|
|
205
|
+
intent 可以直接验证。如果 open intent 只有 log 记录、没有匹配的本地 provenance,
|
|
206
|
+
DriftSeal 就无法确认是谁选择了其中的命令。此时它会先把命令输出到 stderr 并拒绝
|
|
207
|
+
执行;只有检查并信任该命令后,才能显式运行
|
|
208
|
+
`driftseal verify --allow-tracked-command`。Programmatic API 和 MCP tool 中对应的显式
|
|
209
|
+
开关是 `allowTrackedCommand`。intent 关闭时,本地 provenance 会被清理;如果它提前
|
|
210
|
+
丢失,DriftSeal 会按安全方向处理,仍要求显式 opt in。非 Git marker 还会绑定本地
|
|
211
|
+
log 文件的 identity,因此把 marker 和 log 一起复制到别处也不会转移信任。
|
|
212
|
+
|
|
213
|
+
验证事件会记录 exit status、耗时、输出摘要及字节数、Git HEAD,以及当前所有
|
|
214
|
+
tracked 和未被 ignore 的 untracked 文件的内容指纹(intent event log 除外)。
|
|
215
|
+
验证后只要这些内容发生变化,成功证据就会过期,必须重新运行;否则 DriftSeal
|
|
216
|
+
会拒绝把 intent 关闭为 `completed`。命令输出会先写入临时 spool 文件,而不是
|
|
217
|
+
受固定大小的内存 buffer 限制;命令退出后再回放并删除。因此 DriftSeal 不再限制
|
|
218
|
+
输出大小,但实际容量仍受可用磁盘空间约束。被 ignore 的文件不在指纹范围内。
|
|
219
|
+
如果当前目录不是 Git worktree,指纹不可用;此时 gate 只能证明记录到的 exit
|
|
220
|
+
status,无法发现之后发生的内容变化。
|
|
221
|
+
|
|
222
|
+
这只能证明预先声明的命令在记录的内容上通过,不能证明 acceptance criterion
|
|
223
|
+
或测试本身足够可靠。为兼容旧记录,没有 `--accept` 的 intent 仍沿用手动验证流程。
|
|
224
|
+
如果验证器也由同一个 agent 编写、结果带有主观判断,或改动风险较高,应再使用
|
|
225
|
+
受保护的 CI、独立 review 或人工确认。
|
|
226
|
+
|
|
227
|
+
最后记录实际结果:
|
|
193
228
|
|
|
194
229
|
```sh
|
|
195
230
|
driftseal end \
|
|
@@ -211,7 +246,8 @@ intent;会被提交且无法重建的内容改动(比如编辑 `.gitignore`
|
|
|
211
246
|
|
|
212
247
|
| Command | 用途 |
|
|
213
248
|
| --- | --- |
|
|
214
|
-
| `driftseal begin "<intent>" [-v "<
|
|
249
|
+
| `driftseal begin "<intent>" [--accept "<outcome>"] [-v "<command>"] [--decision id] [--force]` | 开启一轮工作。可重复使用 `--accept` 声明可观察的完成条件;一旦声明,就必须同时提供验证命令。 |
|
|
250
|
+
| `driftseal verify [--allow-tracked-command]` | 执行 acceptance-bound intent 预先声明的命令,并把机器证据绑定到当前 Git 可见的工作区内容;没有匹配本地 provenance 的命令必须显式 opt in。 |
|
|
215
251
|
| `driftseal end [id] [-s status] [-n note] [-r verify-result]` | 诚实地关闭 intent。 |
|
|
216
252
|
| `driftseal status` | 查看当前进行中的 intent。 |
|
|
217
253
|
| `driftseal log [-n N] [--all]` | 查看 intent 历史(`--all` 包含已回收的记录)。 |
|
|
@@ -234,7 +270,10 @@ intent;会被提交且无法重建的内容改动(比如编辑 `.gitignore`
|
|
|
234
270
|
如果 `begin` 通过一个或多个 `--decision <id>` 声明了关联,那么 intent
|
|
235
271
|
以 `completed` 或 `partial` 关闭前,必须用 `driftseal decision update` reconcile
|
|
236
272
|
每一条关联 decision。update 可以改变当前 status,并会追加一条包含时间和
|
|
237
|
-
intent ID 的 history。没有关联 decision 的 intent
|
|
273
|
+
intent ID 的 history。没有关联 decision 的 intent 仍沿用普通流程。对于
|
|
274
|
+
acceptance-bound linked intent,所有 decision update 都必须发生在
|
|
275
|
+
`driftseal verify` 之前,因为 update 会改变 workspace fingerprint;顺序应当是
|
|
276
|
+
reconcile、verify、end。
|
|
238
277
|
|
|
239
278
|
## 回收已无意义的记录
|
|
240
279
|
|
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,8 @@ const path = require('path');
|
|
|
25
27
|
const crypto = require('crypto');
|
|
26
28
|
const os = require('os');
|
|
27
29
|
const { isDeepStrictEqual } = require('util');
|
|
28
|
-
const {
|
|
30
|
+
const { StringDecoder } = require('string_decoder');
|
|
31
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
29
32
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
30
33
|
|
|
31
34
|
const END_STATUSES = ['completed', 'partial', 'failed', 'abandoned'];
|
|
@@ -37,8 +40,8 @@ const DECISION_STATUSES = [
|
|
|
37
40
|
'deprecated',
|
|
38
41
|
'superseded',
|
|
39
42
|
];
|
|
40
|
-
const EVENT_SCHEMA_VERSION =
|
|
41
|
-
const PROTOCOL_VERSION =
|
|
43
|
+
const EVENT_SCHEMA_VERSION = 4;
|
|
44
|
+
const PROTOCOL_VERSION = 13;
|
|
42
45
|
const DEFAULT_LOG_LANGUAGE = 'en';
|
|
43
46
|
const IN_PROGRESS_GIT_PATH = 'driftseal-in-progress.jsonl';
|
|
44
47
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
@@ -46,6 +49,10 @@ const LOCK_INIT_STALE_MS = 5 * 1000;
|
|
|
46
49
|
const READ_ONLY_NOTICE = '(read-only: another mutation holds the lock; tail repair skipped)';
|
|
47
50
|
const READ_ONLY_LOCK_WAIT_MS = Number(process.env._DRIFTSEAL_TEST_READ_ONLY_LOCK_WAIT_MS) || 1500;
|
|
48
51
|
const MAX_DECISION_SLUG_LENGTH = 180;
|
|
52
|
+
const VERIFICATION_OUTPUT_CHUNK_BYTES = 64 * 1024;
|
|
53
|
+
const CAPTURE_OUTPUT_EDGE_CHARACTERS = 32 * 1024;
|
|
54
|
+
const CAPTURE_OUTPUT_OMISSION = '\n... [driftseal captured output truncated] ...\n';
|
|
55
|
+
const LOCAL_INTENT_PROVENANCE_FILE = '.driftseal-local-intent.json';
|
|
49
56
|
|
|
50
57
|
class DriftSealError extends Error {
|
|
51
58
|
constructor(message) {
|
|
@@ -66,7 +73,9 @@ class HelpRequested extends DriftSealError {
|
|
|
66
73
|
/** Single source of truth for per-command usage lines. */
|
|
67
74
|
function usageFor(key) {
|
|
68
75
|
const lines = {
|
|
69
|
-
begin:
|
|
76
|
+
begin:
|
|
77
|
+
'usage: driftseal begin "<intent>" [--accept "<observable outcome>"] [--verify "<command>"] [--decision <id>] [--force]',
|
|
78
|
+
verify: 'usage: driftseal verify [--allow-tracked-command]',
|
|
70
79
|
end: 'usage: driftseal end [id] [options]',
|
|
71
80
|
status: 'usage: driftseal status',
|
|
72
81
|
log: 'usage: driftseal log [--last N] [--all]',
|
|
@@ -94,33 +103,73 @@ function usageFor(key) {
|
|
|
94
103
|
|
|
95
104
|
let activeOutput = null;
|
|
96
105
|
|
|
106
|
+
function createBoundedOutputCapture() {
|
|
107
|
+
return { head: '', tail: '', length: 0, headComplete: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function splitsSurrogatePair(text, index) {
|
|
111
|
+
if (index <= 0 || index >= text.length) return false;
|
|
112
|
+
const before = text.charCodeAt(index - 1);
|
|
113
|
+
const after = text.charCodeAt(index);
|
|
114
|
+
return before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function appendBoundedOutput(capture, value) {
|
|
118
|
+
let text = String(value);
|
|
119
|
+
capture.length += text.length;
|
|
120
|
+
|
|
121
|
+
if (!capture.headComplete) {
|
|
122
|
+
let take = Math.min(CAPTURE_OUTPUT_EDGE_CHARACTERS - capture.head.length, text.length);
|
|
123
|
+
if (splitsSurrogatePair(text, take)) take -= 1;
|
|
124
|
+
capture.head += text.slice(0, take);
|
|
125
|
+
text = text.slice(take);
|
|
126
|
+
if (text.length > 0) capture.headComplete = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (text.length === 0) return;
|
|
130
|
+
const combined = capture.tail + text;
|
|
131
|
+
let start = Math.max(0, combined.length - CAPTURE_OUTPUT_EDGE_CHARACTERS);
|
|
132
|
+
if (splitsSurrogatePair(combined, start)) start += 1;
|
|
133
|
+
capture.tail = combined.slice(start);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderBoundedOutput(capture) {
|
|
137
|
+
if (capture.length === capture.head.length + capture.tail.length) {
|
|
138
|
+
return capture.head + capture.tail;
|
|
139
|
+
}
|
|
140
|
+
return capture.head + CAPTURE_OUTPUT_OMISSION + capture.tail;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function captureOutput(stream, value) {
|
|
144
|
+
if (!activeOutput) return false;
|
|
145
|
+
appendBoundedOutput(activeOutput[stream], value);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
97
149
|
function printLine(value = '') {
|
|
98
150
|
const text = String(value);
|
|
99
|
-
if (
|
|
100
|
-
activeOutput.stdout += text + '\n';
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
151
|
+
if (captureOutput('stdout', text + '\n')) return;
|
|
103
152
|
console.log(text);
|
|
104
153
|
}
|
|
105
154
|
|
|
106
155
|
function printError(value = '') {
|
|
107
156
|
const text = String(value);
|
|
108
|
-
if (
|
|
109
|
-
activeOutput.stderr += text + '\n';
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
157
|
+
if (captureOutput('stderr', text + '\n')) return;
|
|
112
158
|
console.error(text);
|
|
113
159
|
}
|
|
114
160
|
|
|
115
161
|
function writeOutput(value) {
|
|
116
162
|
const text = String(value);
|
|
117
|
-
if (
|
|
118
|
-
activeOutput.stdout += text;
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
163
|
+
if (captureOutput('stdout', text)) return;
|
|
121
164
|
process.stdout.write(text);
|
|
122
165
|
}
|
|
123
166
|
|
|
167
|
+
function writeErrorOutput(value) {
|
|
168
|
+
const text = String(value);
|
|
169
|
+
if (captureOutput('stderr', text)) return;
|
|
170
|
+
process.stderr.write(text);
|
|
171
|
+
}
|
|
172
|
+
|
|
124
173
|
if (process.env._DRIFTSEAL_TEST_UMASK) {
|
|
125
174
|
process.umask(Number.parseInt(process.env._DRIFTSEAL_TEST_UMASK, 8));
|
|
126
175
|
}
|
|
@@ -168,15 +217,65 @@ function normalizeEvent(event, line) {
|
|
|
168
217
|
if (!Array.isArray(event.decisions) && event.decisions !== undefined) {
|
|
169
218
|
fail(`invalid decisions list on log line ${line}`);
|
|
170
219
|
}
|
|
220
|
+
if (!Array.isArray(event.acceptance) && event.acceptance !== undefined) {
|
|
221
|
+
fail(`invalid acceptance list on log line ${line}`);
|
|
222
|
+
}
|
|
223
|
+
const acceptance = event.acceptance || [];
|
|
224
|
+
if (acceptance.some((criterion) => typeof criterion !== 'string' || criterion.trim().length === 0)) {
|
|
225
|
+
fail(`invalid acceptance criterion on log line ${line}`);
|
|
226
|
+
}
|
|
227
|
+
if (acceptance.length > 0 && (typeof event.verify !== 'string' || event.verify.trim().length === 0)) {
|
|
228
|
+
fail(`acceptance-bound intent has no verification command on log line ${line}`);
|
|
229
|
+
}
|
|
171
230
|
const decisions = (event.decisions || []).map(normalizeDecisionId);
|
|
172
231
|
if (new Set(decisions).size !== decisions.length) {
|
|
173
232
|
fail(`duplicate linked decision on log line ${line}`);
|
|
174
233
|
}
|
|
175
|
-
return { ...event, decisions, head: normalizeHead(event.head) };
|
|
234
|
+
return { ...event, acceptance, decisions, head: normalizeHead(event.head) };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (event.type === 'verify') {
|
|
238
|
+
if (
|
|
239
|
+
typeof event.verificationId !== 'string' ||
|
|
240
|
+
event.verificationId.length === 0 ||
|
|
241
|
+
typeof event.command !== 'string' ||
|
|
242
|
+
event.command.trim().length === 0 ||
|
|
243
|
+
typeof event.passed !== 'boolean' ||
|
|
244
|
+
(event.exitCode !== null && (!Number.isInteger(event.exitCode) || event.exitCode < 0)) ||
|
|
245
|
+
(event.signal !== null && typeof event.signal !== 'string') ||
|
|
246
|
+
!Number.isSafeInteger(event.durationMs) ||
|
|
247
|
+
event.durationMs < 0 ||
|
|
248
|
+
!Number.isSafeInteger(event.stdoutBytes) ||
|
|
249
|
+
event.stdoutBytes < 0 ||
|
|
250
|
+
!Number.isSafeInteger(event.stderrBytes) ||
|
|
251
|
+
event.stderrBytes < 0 ||
|
|
252
|
+
typeof event.outputHash !== 'string' ||
|
|
253
|
+
!/^[a-f0-9]{64}$/.test(event.outputHash) ||
|
|
254
|
+
(event.workspace !== null &&
|
|
255
|
+
(typeof event.workspace !== 'string' || !/^[a-f0-9]{64}$/.test(event.workspace))) ||
|
|
256
|
+
event.passed !== (event.exitCode === 0 && event.signal === null)
|
|
257
|
+
) {
|
|
258
|
+
fail(`invalid verification event on log line ${line}`);
|
|
259
|
+
}
|
|
260
|
+
return { ...event, head: normalizeHead(event.head) };
|
|
176
261
|
}
|
|
177
262
|
|
|
178
263
|
if (event.type === 'end') {
|
|
179
264
|
if (!END_STATUSES.includes(event.status)) fail(`invalid end event on log line ${line}`);
|
|
265
|
+
if (
|
|
266
|
+
event.verificationId !== undefined &&
|
|
267
|
+
event.verificationId !== null &&
|
|
268
|
+
(typeof event.verificationId !== 'string' || event.verificationId.length === 0)
|
|
269
|
+
) {
|
|
270
|
+
fail(`invalid end verification id on log line ${line}`);
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
event.workspace !== undefined &&
|
|
274
|
+
event.workspace !== null &&
|
|
275
|
+
(typeof event.workspace !== 'string' || !/^[a-f0-9]{64}$/.test(event.workspace))
|
|
276
|
+
) {
|
|
277
|
+
fail(`invalid end workspace on log line ${line}`);
|
|
278
|
+
}
|
|
180
279
|
return { ...event, head: normalizeHead(event.head) };
|
|
181
280
|
}
|
|
182
281
|
|
|
@@ -506,7 +605,12 @@ function parkedOpenIntent(park) {
|
|
|
506
605
|
|
|
507
606
|
function appendEvent(event) {
|
|
508
607
|
const park = inProgressFile();
|
|
509
|
-
if (!park)
|
|
608
|
+
if (!park) {
|
|
609
|
+
const stored = appendEventTo(logFile(), event);
|
|
610
|
+
if (event.type === 'begin') writeLocalIntentProvenance(stored);
|
|
611
|
+
if (event.type === 'end') clearLocalIntentProvenance(event.id);
|
|
612
|
+
return stored;
|
|
613
|
+
}
|
|
510
614
|
|
|
511
615
|
const open = parkedOpenIntent(park);
|
|
512
616
|
// A park with nothing open left in it belongs in the log; an interrupted end retries here.
|
|
@@ -528,9 +632,88 @@ function contentHash(content) {
|
|
|
528
632
|
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
529
633
|
}
|
|
530
634
|
|
|
531
|
-
function
|
|
635
|
+
function localIntentProvenanceFile() {
|
|
636
|
+
const root = gitWorktreeRoot();
|
|
637
|
+
const key = contentHash(path.resolve(logFile())).slice(0, 16);
|
|
638
|
+
if (!root) return path.join(logDir(), LOCAL_INTENT_PROVENANCE_FILE);
|
|
639
|
+
const gitPath = gitCapture(['rev-parse', '--git-path', `driftseal-local-intent-${key}.json`]);
|
|
640
|
+
return gitPath ? path.resolve(process.cwd(), gitPath) : null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function localIntentLogIdentity() {
|
|
644
|
+
try {
|
|
645
|
+
const stat = fs.statSync(logFile(), { bigint: true });
|
|
646
|
+
return contentHash(JSON.stringify([String(stat.dev), String(stat.ino), String(stat.birthtimeNs)]));
|
|
647
|
+
} catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function localIntentProvenanceFingerprint({ id, ts, verify }) {
|
|
653
|
+
return contentHash(JSON.stringify([id, ts, verify || null]));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function writeLocalIntentProvenance(event) {
|
|
657
|
+
const file = localIntentProvenanceFile();
|
|
658
|
+
if (!file) return;
|
|
659
|
+
ensureDirectoryDurable(path.dirname(file));
|
|
660
|
+
atomicWriteFile(
|
|
661
|
+
file,
|
|
662
|
+
JSON.stringify({
|
|
663
|
+
version: 1,
|
|
664
|
+
id: event.id,
|
|
665
|
+
fingerprint: localIntentProvenanceFingerprint(event),
|
|
666
|
+
logIdentity: localIntentLogIdentity(),
|
|
667
|
+
}) + '\n',
|
|
668
|
+
0o600
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function readLocalIntentProvenance() {
|
|
673
|
+
const file = localIntentProvenanceFile();
|
|
674
|
+
if (!file || !fs.existsSync(file)) return null;
|
|
675
|
+
try {
|
|
676
|
+
const provenance = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
677
|
+
if (
|
|
678
|
+
provenance.version !== 1 ||
|
|
679
|
+
typeof provenance.id !== 'string' ||
|
|
680
|
+
!/^[a-f0-9]{64}$/.test(provenance.fingerprint) ||
|
|
681
|
+
!/^[a-f0-9]{64}$/.test(provenance.logIdentity)
|
|
682
|
+
) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
return provenance;
|
|
686
|
+
} catch {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function hasMatchingLocalIntentProvenance(intent) {
|
|
692
|
+
const provenance = readLocalIntentProvenance();
|
|
693
|
+
return (
|
|
694
|
+
provenance !== null &&
|
|
695
|
+
provenance.id === intent.id &&
|
|
696
|
+
provenance.logIdentity === localIntentLogIdentity() &&
|
|
697
|
+
provenance.fingerprint ===
|
|
698
|
+
localIntentProvenanceFingerprint({
|
|
699
|
+
id: intent.id,
|
|
700
|
+
ts: intent.tsBegin,
|
|
701
|
+
verify: intent.verify,
|
|
702
|
+
})
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function clearLocalIntentProvenance(id) {
|
|
707
|
+
const file = localIntentProvenanceFile();
|
|
708
|
+
const provenance = readLocalIntentProvenance();
|
|
709
|
+
if (!file || !provenance || provenance.id !== id) return;
|
|
710
|
+
fs.unlinkSync(file);
|
|
711
|
+
fsyncDirectory(path.dirname(file));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function atomicWriteFile(target, content, createMode = 0o644) {
|
|
532
715
|
const existed = fs.existsSync(target);
|
|
533
|
-
const mode = existed ? fs.statSync(target).mode & 0o777 :
|
|
716
|
+
const mode = existed ? fs.statSync(target).mode & 0o777 : createMode;
|
|
534
717
|
const temp = path.join(
|
|
535
718
|
path.dirname(target),
|
|
536
719
|
`.${path.basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`
|
|
@@ -817,6 +1000,7 @@ function fold(events) {
|
|
|
817
1000
|
id: ev.id,
|
|
818
1001
|
tsBegin: ev.ts,
|
|
819
1002
|
intent: ev.intent,
|
|
1003
|
+
acceptance: Array.isArray(ev.acceptance) ? ev.acceptance : [],
|
|
820
1004
|
verify: ev.verify || null,
|
|
821
1005
|
beginHead: ev.head || null,
|
|
822
1006
|
decisions: Array.isArray(ev.decisions) ? ev.decisions : [],
|
|
@@ -824,6 +1008,8 @@ function fold(events) {
|
|
|
824
1008
|
decisionPrepares: [],
|
|
825
1009
|
decisionTerminals: [],
|
|
826
1010
|
decisionUpdates: [],
|
|
1011
|
+
verificationAttempts: [],
|
|
1012
|
+
verification: null,
|
|
827
1013
|
status: 'in_progress',
|
|
828
1014
|
tsEnd: null,
|
|
829
1015
|
note: null,
|
|
@@ -834,6 +1020,20 @@ function fold(events) {
|
|
|
834
1020
|
reclaimedAt: null,
|
|
835
1021
|
});
|
|
836
1022
|
order.push(ev.id);
|
|
1023
|
+
} else if (ev.type === 'verify') {
|
|
1024
|
+
const rec = records.get(ev.id);
|
|
1025
|
+
if (!rec) fail(`verification event references unknown intent id: ${ev.id}`);
|
|
1026
|
+
if (rec.status !== 'in_progress') {
|
|
1027
|
+
fail(`verification occurred after intent ${ev.id} was closed`);
|
|
1028
|
+
}
|
|
1029
|
+
if (rec.acceptance.length === 0 || !rec.verify) {
|
|
1030
|
+
fail(`verification event references intent ${ev.id} without acceptance criteria`);
|
|
1031
|
+
}
|
|
1032
|
+
if (ev.command !== rec.verify) {
|
|
1033
|
+
fail(`verification command does not match intent ${ev.id}`);
|
|
1034
|
+
}
|
|
1035
|
+
rec.verificationAttempts.push(ev);
|
|
1036
|
+
rec.verification = ev;
|
|
837
1037
|
} else if (ev.type === 'reclaim' || ev.type === 'unreclaim') {
|
|
838
1038
|
const rec = records.get(ev.id);
|
|
839
1039
|
if (!rec) fail(`${ev.type} event references unknown intent id: ${ev.id}`);
|
|
@@ -877,6 +1077,18 @@ function fold(events) {
|
|
|
877
1077
|
) {
|
|
878
1078
|
fail(`linked intent ${ev.id} was closed without reconciling every declared decision`);
|
|
879
1079
|
}
|
|
1080
|
+
if (ev.status === 'completed' && rec.acceptance.length > 0) {
|
|
1081
|
+
if (!rec.verification || !rec.verification.passed) {
|
|
1082
|
+
fail(`acceptance-bound intent ${ev.id} was completed without successful machine verification`);
|
|
1083
|
+
}
|
|
1084
|
+
if (
|
|
1085
|
+
(ev.schemaVersion || 1) < 4 ||
|
|
1086
|
+
ev.verificationId !== rec.verification.verificationId ||
|
|
1087
|
+
(ev.workspace ?? null) !== rec.verification.workspace
|
|
1088
|
+
) {
|
|
1089
|
+
fail(`acceptance-bound intent ${ev.id} was completed with stale machine verification`);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
880
1092
|
rec.status = ev.status;
|
|
881
1093
|
rec.tsEnd = ev.ts;
|
|
882
1094
|
rec.note = ev.note || null;
|
|
@@ -1335,8 +1547,19 @@ function parseArgs(argv, spec, usageKey) {
|
|
|
1335
1547
|
function render(rec) {
|
|
1336
1548
|
const lines = [`[${rec.id}] ${rec.status}`];
|
|
1337
1549
|
lines.push(` intent: ${rec.intent}`);
|
|
1550
|
+
for (const criterion of rec.acceptance) lines.push(` accept: ${criterion}`);
|
|
1338
1551
|
if (rec.decisions.length > 0) lines.push(` decisions: ${rec.decisions.join(', ')}`);
|
|
1339
1552
|
if (rec.verify) lines.push(` verify: ${rec.verify}`);
|
|
1553
|
+
if (rec.verification) {
|
|
1554
|
+
const state = rec.verification.passed ? 'passed' : 'failed';
|
|
1555
|
+
const workspace = rec.verification.workspace
|
|
1556
|
+
? `, workspace ${rec.verification.workspace.slice(0, 12)}`
|
|
1557
|
+
: ', workspace unavailable';
|
|
1558
|
+
lines.push(
|
|
1559
|
+
` machine-verification: ${state} (exit ${rec.verification.exitCode ?? '-'}, ` +
|
|
1560
|
+
`${rec.verification.durationMs} ms${workspace})`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1340
1563
|
if (rec.verifyResult) lines.push(` verify-result: ${rec.verifyResult}`);
|
|
1341
1564
|
if (rec.note) lines.push(` note: ${rec.note}`);
|
|
1342
1565
|
if (rec.beginHead || rec.endHead) {
|
|
@@ -1347,12 +1570,31 @@ function render(rec) {
|
|
|
1347
1570
|
return lines.join('\n');
|
|
1348
1571
|
}
|
|
1349
1572
|
|
|
1573
|
+
function publicVerification(verification) {
|
|
1574
|
+
if (!verification) return null;
|
|
1575
|
+
return {
|
|
1576
|
+
id: verification.verificationId,
|
|
1577
|
+
passed: verification.passed,
|
|
1578
|
+
exitCode: verification.exitCode,
|
|
1579
|
+
signal: verification.signal,
|
|
1580
|
+
durationMs: verification.durationMs,
|
|
1581
|
+
outputHash: verification.outputHash,
|
|
1582
|
+
stdoutBytes: verification.stdoutBytes,
|
|
1583
|
+
stderrBytes: verification.stderrBytes,
|
|
1584
|
+
workspace: verification.workspace,
|
|
1585
|
+
head: verification.head,
|
|
1586
|
+
ranAt: verification.ts,
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1350
1590
|
function publicIntent(rec) {
|
|
1351
1591
|
if (!rec) return null;
|
|
1352
1592
|
return {
|
|
1353
1593
|
id: rec.id,
|
|
1354
1594
|
intent: rec.intent,
|
|
1595
|
+
acceptance: [...rec.acceptance],
|
|
1355
1596
|
verify: rec.verify,
|
|
1597
|
+
verification: publicVerification(rec.verification),
|
|
1356
1598
|
decisions: [...rec.decisions],
|
|
1357
1599
|
status: rec.status,
|
|
1358
1600
|
note: rec.note,
|
|
@@ -1634,7 +1876,8 @@ ${intentLogLanguageParagraph(language)}
|
|
|
1634
1876
|
|
|
1635
1877
|
1. **Write intent first**, before modifying, creating, or deleting files, or
|
|
1636
1878
|
making any other non-Git change that may need a rollback:
|
|
1637
|
-
\`driftseal begin "<what this round will accomplish>" --verify "<command
|
|
1879
|
+
\`driftseal begin "<what this round will accomplish>" --accept "<observable outcome>" --verify "<exact command that proves it>"\`.
|
|
1880
|
+
Repeat \`--accept\` when completion has multiple independently observable criteria.
|
|
1638
1881
|
Add one \`--decision <id>\` for each existing decision this round may change.
|
|
1639
1882
|
Git operations never need an intent and are not included in the intent log;
|
|
1640
1883
|
Git maintains their history. This includes inspection, branch and worktree
|
|
@@ -1649,8 +1892,17 @@ ${intentLogLanguageParagraph(language)}
|
|
|
1649
1892
|
and can be verified on its own.
|
|
1650
1893
|
2. **Execute only the intent.** Scope change? Close the current intent
|
|
1651
1894
|
(\`driftseal end -s partial|abandoned -n "<why>"\`) and \`driftseal begin\` a new one.
|
|
1652
|
-
3. **
|
|
1653
|
-
|
|
1895
|
+
3. **Reconcile, verify, then close**: for a linked intent, first reconcile every
|
|
1896
|
+
declared decision as described below. For an acceptance-bound intent, inspect the
|
|
1897
|
+
exact command shown by \`driftseal status\`, then run \`driftseal verify\` to execute it
|
|
1898
|
+
and bind its exit status to the current Git-visible workspace contents. A command
|
|
1899
|
+
sourced from the repository intent log is untrusted and requires
|
|
1900
|
+
\`--allow-tracked-command\` after inspection; locally parked commands do not.
|
|
1901
|
+
An intent without \`--accept\` uses its declared check directly. Then run
|
|
1902
|
+
\`driftseal end -s completed|partial|failed|abandoned -n "<what happened>" -r "<optional context for the next agent>"\`.
|
|
1903
|
+
DriftSeal rejects \`completed\` when machine verification failed, never ran, or
|
|
1904
|
+
the workspace changed after it. Ignored files are outside the workspace fingerprint.
|
|
1905
|
+
Outside a Git worktree, only the recorded exit status is available.
|
|
1654
1906
|
Never report success without closing the intent.
|
|
1655
1907
|
Before closing a linked intent as \`completed\` or \`partial\`, reconcile every
|
|
1656
1908
|
declared decision with \`driftseal decision update <id> --status <status> --note "<why>"\`.
|
|
@@ -1682,8 +1934,30 @@ Log: \`.intent-log/events.jsonl\` (override with \`$DRIFTSEAL_HOME\`); ${localLo
|
|
|
1682
1934
|
${INTENT_PROTOCOL_END}`;
|
|
1683
1935
|
}
|
|
1684
1936
|
|
|
1685
|
-
function previousIntentProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE) {
|
|
1686
|
-
const
|
|
1937
|
+
function previousIntentProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
1938
|
+
const v12 = intentProtocolBlock(version, language, localLog)
|
|
1939
|
+
.replace(
|
|
1940
|
+
' `driftseal begin "<what this round will accomplish>" --accept "<observable outcome>" --verify "<exact command that proves it>"`.\n' +
|
|
1941
|
+
' Repeat `--accept` when completion has multiple independently observable criteria.',
|
|
1942
|
+
' `driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"`.'
|
|
1943
|
+
)
|
|
1944
|
+
.replace(
|
|
1945
|
+
'3. **Reconcile, verify, then close**: for a linked intent, first reconcile every\n' +
|
|
1946
|
+
' declared decision as described below. For an acceptance-bound intent, inspect the\n' +
|
|
1947
|
+
' exact command shown by `driftseal status`, then run `driftseal verify` to execute it\n' +
|
|
1948
|
+
' and bind its exit status to the current Git-visible workspace contents. A command\n' +
|
|
1949
|
+
' sourced from the repository intent log is untrusted and requires\n' +
|
|
1950
|
+
' `--allow-tracked-command` after inspection; locally parked commands do not.\n' +
|
|
1951
|
+
' An intent without `--accept` uses its declared check directly. Then run\n' +
|
|
1952
|
+
' `driftseal end -s completed|partial|failed|abandoned -n "<what happened>" -r "<optional context for the next agent>"`.\n' +
|
|
1953
|
+
' DriftSeal rejects `completed` when machine verification failed, never ran, or\n' +
|
|
1954
|
+
' the workspace changed after it. Ignored files are outside the workspace fingerprint.\n' +
|
|
1955
|
+
' Outside a Git worktree, only the recorded exit status is available.',
|
|
1956
|
+
'3. **Verify, then close**: run the declared verification, then\n' +
|
|
1957
|
+
' `driftseal end -s completed|partial|failed|abandoned -n "<what happened>" -r "<what the verification showed, written for the next agent>"`.'
|
|
1958
|
+
);
|
|
1959
|
+
if (version >= 12) return v12;
|
|
1960
|
+
const v11 = v12
|
|
1687
1961
|
.replace(
|
|
1688
1962
|
'-r "<what the verification showed, written for the next agent>"',
|
|
1689
1963
|
'-r "<verify output>"'
|
|
@@ -1876,9 +2150,9 @@ When an intent declares an existing decision with \`--decision <id>\`, use
|
|
|
1876
2150
|
Commit \`.decision-log/\` with the code.`;
|
|
1877
2151
|
}
|
|
1878
2152
|
|
|
1879
|
-
function previousDecisionProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE) {
|
|
1880
|
-
if (version >= 11) return decisionProtocolBlock(version, language);
|
|
1881
|
-
const v10 = stripDecisionLogLanguage(decisionProtocolBlock(version, language), language);
|
|
2153
|
+
function previousDecisionProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2154
|
+
if (version >= 11) return decisionProtocolBlock(version, language, localLog);
|
|
2155
|
+
const v10 = stripDecisionLogLanguage(decisionProtocolBlock(version, language, localLog), language);
|
|
1882
2156
|
if (version >= 9) return v10;
|
|
1883
2157
|
const v8 = v10.replace(
|
|
1884
2158
|
'\nAfter a merge, colliding decision ids are remapped with `driftseal absorb`;\n' +
|
|
@@ -2627,16 +2901,21 @@ function hookReminder(event, { readOnly = false } = {}) {
|
|
|
2627
2901
|
if (event === 'prompt') {
|
|
2628
2902
|
return (
|
|
2629
2903
|
'DriftSeal reminder: if this round will modify files or anything else that may need a ' +
|
|
2630
|
-
'rollback, begin an intent first: driftseal begin "<intent>" --
|
|
2904
|
+
'rollback, begin an intent first: driftseal begin "<intent>" --accept "<observable outcome>" ' +
|
|
2905
|
+
'--verify "<command>". ' +
|
|
2631
2906
|
'Questions, read-only exploration, and single-step checks need no intent — skip this ' +
|
|
2632
2907
|
'reminder when it does not apply.'
|
|
2633
2908
|
);
|
|
2634
2909
|
}
|
|
2635
2910
|
const open = openIntent(fold(readEvents({ file, readOnly })));
|
|
2636
2911
|
if (open) {
|
|
2912
|
+
const reconciliation = open.decisions.length > 0 ? 'reconcile every linked decision, then ' : '';
|
|
2913
|
+
const verification = open.acceptance.length > 0
|
|
2914
|
+
? `${reconciliation}inspect and run driftseal verify, then close it with driftseal end`
|
|
2915
|
+
: `${reconciliation}run the declared verification, then close it with driftseal end`;
|
|
2637
2916
|
return (
|
|
2638
2917
|
`DriftSeal reminder: intent ${open.id} is still in_progress: "${open.intent}". ` +
|
|
2639
|
-
|
|
2918
|
+
`If its work is done, ${verification}; ` +
|
|
2640
2919
|
'if this turn was unrelated, ignore this reminder.'
|
|
2641
2920
|
);
|
|
2642
2921
|
}
|
|
@@ -2711,6 +2990,72 @@ function isGitWorkTree(cwd = process.cwd()) {
|
|
|
2711
2990
|
return gitCapture(['rev-parse', '--is-inside-work-tree'], cwd) === 'true';
|
|
2712
2991
|
}
|
|
2713
2992
|
|
|
2993
|
+
/**
|
|
2994
|
+
* Hash the material Git-visible workspace contents rather than trusting the
|
|
2995
|
+
* current commit alone. Any tracked or untracked (non-ignored) content change
|
|
2996
|
+
* makes the verification stale.
|
|
2997
|
+
* The intent event log is excluded because recording verification and closure
|
|
2998
|
+
* necessarily appends to it.
|
|
2999
|
+
*/
|
|
3000
|
+
function workspaceFingerprint(cwd = process.cwd()) {
|
|
3001
|
+
if (!isGitWorkTree(cwd)) return null;
|
|
3002
|
+
const root = gitCaptureLine(['rev-parse', '--show-toplevel'], cwd);
|
|
3003
|
+
if (!root) return null;
|
|
3004
|
+
const listing = gitCaptureRaw(
|
|
3005
|
+
['ls-files', '-z', '--cached', '--others', '--exclude-standard'],
|
|
3006
|
+
root
|
|
3007
|
+
);
|
|
3008
|
+
if (listing === null) return null;
|
|
3009
|
+
|
|
3010
|
+
const excludedFile = path.resolve(logFile());
|
|
3011
|
+
const excludedLockPrefixes = [logDir(), decisionDir()].map((directory) =>
|
|
3012
|
+
path.resolve(directory, '.driftseal.lock')
|
|
3013
|
+
);
|
|
3014
|
+
const files = [...new Set(listing.split('\0').filter(Boolean))].sort();
|
|
3015
|
+
const hash = crypto.createHash('sha256');
|
|
3016
|
+
for (const relative of files) {
|
|
3017
|
+
const target = path.resolve(root, relative);
|
|
3018
|
+
if (
|
|
3019
|
+
target === excludedFile ||
|
|
3020
|
+
excludedLockPrefixes.some(
|
|
3021
|
+
(prefix) => target === prefix || target.startsWith(`${prefix}${path.sep}`) || target.startsWith(`${prefix}.stale.`)
|
|
3022
|
+
)
|
|
3023
|
+
) {
|
|
3024
|
+
continue;
|
|
3025
|
+
}
|
|
3026
|
+
hash.update(relative, 'utf8');
|
|
3027
|
+
hash.update('\0');
|
|
3028
|
+
let stat;
|
|
3029
|
+
try {
|
|
3030
|
+
stat = fs.lstatSync(target);
|
|
3031
|
+
} catch (err) {
|
|
3032
|
+
if (err.code === 'ENOENT') {
|
|
3033
|
+
hash.update('missing\0');
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
throw err;
|
|
3037
|
+
}
|
|
3038
|
+
hash.update(String(stat.mode & 0o111));
|
|
3039
|
+
hash.update('\0');
|
|
3040
|
+
if (stat.isSymbolicLink()) {
|
|
3041
|
+
hash.update('symlink\0');
|
|
3042
|
+
hash.update(fs.readlinkSync(target));
|
|
3043
|
+
} else if (stat.isFile()) {
|
|
3044
|
+
hash.update('file\0');
|
|
3045
|
+
hash.update(fs.readFileSync(target));
|
|
3046
|
+
} else if (stat.isDirectory()) {
|
|
3047
|
+
hash.update('directory\0');
|
|
3048
|
+
hash.update(gitCapture(['rev-parse', 'HEAD'], target) || 'no-head');
|
|
3049
|
+
hash.update('\0');
|
|
3050
|
+
hash.update(gitCaptureRaw(['status', '--porcelain=v1', '-z'], target) || '');
|
|
3051
|
+
} else {
|
|
3052
|
+
hash.update('other\0');
|
|
3053
|
+
}
|
|
3054
|
+
hash.update('\0');
|
|
3055
|
+
}
|
|
3056
|
+
return hash.digest('hex');
|
|
3057
|
+
}
|
|
3058
|
+
|
|
2714
3059
|
/**
|
|
2715
3060
|
* Warn (without mutating the index or .gitignore) when local log mode is on
|
|
2716
3061
|
* but the default log paths are still tracked by git. The log directories are
|
|
@@ -3577,9 +3922,173 @@ function absorbGit(baseFile, oursFile, theirsFile, { abandon, dryRun }) {
|
|
|
3577
3922
|
});
|
|
3578
3923
|
}
|
|
3579
3924
|
|
|
3925
|
+
function appendVerificationSpawnError(file, error) {
|
|
3926
|
+
const stat = fs.statSync(file);
|
|
3927
|
+
let prefix = '';
|
|
3928
|
+
if (stat.size > 0) {
|
|
3929
|
+
const fd = fs.openSync(file, 'r');
|
|
3930
|
+
const lastByte = Buffer.alloc(1);
|
|
3931
|
+
try {
|
|
3932
|
+
fs.readSync(fd, lastByte, 0, 1, stat.size - 1);
|
|
3933
|
+
} finally {
|
|
3934
|
+
fs.closeSync(fd);
|
|
3935
|
+
}
|
|
3936
|
+
if (lastByte[0] !== 0x0a) prefix = '\n';
|
|
3937
|
+
}
|
|
3938
|
+
fs.appendFileSync(file, `${prefix}${error.message}\n`, 'utf8');
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
function digestAndReplayVerificationOutput(file, writer, hash) {
|
|
3942
|
+
const fd = fs.openSync(file, 'r');
|
|
3943
|
+
const decoder = new StringDecoder('utf8');
|
|
3944
|
+
const buffer = Buffer.allocUnsafe(VERIFICATION_OUTPUT_CHUNK_BYTES);
|
|
3945
|
+
let bytes = 0;
|
|
3946
|
+
let lastCharacter = null;
|
|
3947
|
+
|
|
3948
|
+
const display = (text) => {
|
|
3949
|
+
if (!text) return;
|
|
3950
|
+
writer(text);
|
|
3951
|
+
lastCharacter = text.at(-1);
|
|
3952
|
+
};
|
|
3953
|
+
|
|
3954
|
+
try {
|
|
3955
|
+
while (true) {
|
|
3956
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
3957
|
+
if (count === 0) break;
|
|
3958
|
+
const chunk = buffer.subarray(0, count);
|
|
3959
|
+
hash.update(chunk);
|
|
3960
|
+
bytes += count;
|
|
3961
|
+
display(decoder.write(chunk));
|
|
3962
|
+
}
|
|
3963
|
+
display(decoder.end());
|
|
3964
|
+
} finally {
|
|
3965
|
+
fs.closeSync(fd);
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
return { bytes, endsWithNewline: lastCharacter === '\n' };
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
function executeVerificationCommand(command) {
|
|
3972
|
+
const spool = fs.mkdtempSync(path.join(os.tmpdir(), 'driftseal-verify-'));
|
|
3973
|
+
const stdoutFile = path.join(spool, 'stdout');
|
|
3974
|
+
const stderrFile = path.join(spool, 'stderr');
|
|
3975
|
+
let stdoutFd;
|
|
3976
|
+
let stderrFd;
|
|
3977
|
+
|
|
3978
|
+
try {
|
|
3979
|
+
stdoutFd = fs.openSync(stdoutFile, 'wx', 0o600);
|
|
3980
|
+
stderrFd = fs.openSync(stderrFile, 'wx', 0o600);
|
|
3981
|
+
const started = process.hrtime.bigint();
|
|
3982
|
+
let result;
|
|
3983
|
+
try {
|
|
3984
|
+
result = spawnSync(command, {
|
|
3985
|
+
cwd: process.cwd(),
|
|
3986
|
+
env: process.env,
|
|
3987
|
+
shell: true,
|
|
3988
|
+
stdio: ['ignore', stdoutFd, stderrFd],
|
|
3989
|
+
});
|
|
3990
|
+
} finally {
|
|
3991
|
+
fs.closeSync(stdoutFd);
|
|
3992
|
+
stdoutFd = undefined;
|
|
3993
|
+
fs.closeSync(stderrFd);
|
|
3994
|
+
stderrFd = undefined;
|
|
3995
|
+
}
|
|
3996
|
+
const durationMs = Number((process.hrtime.bigint() - started) / 1000000n);
|
|
3997
|
+
if (result.error) appendVerificationSpawnError(stderrFile, result.error);
|
|
3998
|
+
|
|
3999
|
+
const hash = crypto.createHash('sha256');
|
|
4000
|
+
const stdout = digestAndReplayVerificationOutput(stdoutFile, writeOutput, hash);
|
|
4001
|
+
if (stdout.bytes > 0 && !stdout.endsWithNewline) printLine();
|
|
4002
|
+
hash.update('\0');
|
|
4003
|
+
const stderr = digestAndReplayVerificationOutput(stderrFile, writeErrorOutput, hash);
|
|
4004
|
+
if (stderr.bytes > 0 && !stderr.endsWithNewline) printError();
|
|
4005
|
+
|
|
4006
|
+
return {
|
|
4007
|
+
result,
|
|
4008
|
+
durationMs,
|
|
4009
|
+
outputHash: hash.digest('hex'),
|
|
4010
|
+
stdoutBytes: stdout.bytes,
|
|
4011
|
+
stderrBytes: stderr.bytes,
|
|
4012
|
+
};
|
|
4013
|
+
} finally {
|
|
4014
|
+
if (stdoutFd !== undefined) fs.closeSync(stdoutFd);
|
|
4015
|
+
if (stderrFd !== undefined) fs.closeSync(stderrFd);
|
|
4016
|
+
fs.rmSync(spool, { recursive: true, force: true });
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
function runMachineVerification({ allowTrackedCommand = false } = {}) {
|
|
4021
|
+
const snapshot = withMutationLocks([logDir()], () => {
|
|
4022
|
+
const intent = openIntent(fold(readEvents({ repairTail: true })));
|
|
4023
|
+
if (!intent) fail('no intent in progress; nothing to verify');
|
|
4024
|
+
if (intent.acceptance.length === 0) {
|
|
4025
|
+
fail(`intent ${intent.id} has no acceptance criteria; declare them with driftseal begin --accept`);
|
|
4026
|
+
}
|
|
4027
|
+
if (!intent.verify) fail(`intent ${intent.id} has no verification command`);
|
|
4028
|
+
const park = inProgressFile();
|
|
4029
|
+
const parked = park ? parkedOpenIntent(park) : null;
|
|
4030
|
+
const locallyProvenanced = hasMatchingLocalIntentProvenance(intent);
|
|
4031
|
+
return {
|
|
4032
|
+
id: intent.id,
|
|
4033
|
+
command: intent.verify,
|
|
4034
|
+
requiresExplicitTrust:
|
|
4035
|
+
(!parked || parked.id !== intent.id) && !locallyProvenanced,
|
|
4036
|
+
};
|
|
4037
|
+
});
|
|
4038
|
+
|
|
4039
|
+
const displayedCommand = JSON.stringify(snapshot.command);
|
|
4040
|
+
printError(`verification command: ${displayedCommand}`);
|
|
4041
|
+
if (snapshot.requiresExplicitTrust && !allowTrackedCommand) {
|
|
4042
|
+
fail(
|
|
4043
|
+
`refusing to execute a verification command that DriftSeal cannot confirm was created locally: ${displayedCommand}\n` +
|
|
4044
|
+
'no matching local intent provenance was found; ' +
|
|
4045
|
+
'inspect the command, then re-run with --allow-tracked-command only if you trust it'
|
|
4046
|
+
);
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
const execution = executeVerificationCommand(snapshot.command);
|
|
4050
|
+
const { result, durationMs, outputHash, stdoutBytes, stderrBytes } = execution;
|
|
4051
|
+
const exitCode = Number.isInteger(result.status) ? result.status : 1;
|
|
4052
|
+
const signal = typeof result.signal === 'string' ? result.signal : null;
|
|
4053
|
+
const passed = exitCode === 0 && signal === null;
|
|
4054
|
+
const verificationEvent = {
|
|
4055
|
+
type: 'verify',
|
|
4056
|
+
id: snapshot.id,
|
|
4057
|
+
verificationId: crypto.randomUUID(),
|
|
4058
|
+
ts: new Date().toISOString(),
|
|
4059
|
+
command: snapshot.command,
|
|
4060
|
+
passed,
|
|
4061
|
+
exitCode,
|
|
4062
|
+
signal,
|
|
4063
|
+
durationMs,
|
|
4064
|
+
outputHash,
|
|
4065
|
+
stdoutBytes,
|
|
4066
|
+
stderrBytes,
|
|
4067
|
+
workspace: workspaceFingerprint(),
|
|
4068
|
+
head: gitCapture(['rev-parse', 'HEAD']),
|
|
4069
|
+
};
|
|
4070
|
+
|
|
4071
|
+
const intent = withMutationLocks([logDir()], () => {
|
|
4072
|
+
const events = readEvents({ repairTail: true });
|
|
4073
|
+
const current = openIntent(fold(events));
|
|
4074
|
+
if (!current || current.id !== snapshot.id || current.verify !== snapshot.command) {
|
|
4075
|
+
fail(`intent ${snapshot.id} changed while its verification command was running`);
|
|
4076
|
+
}
|
|
4077
|
+
events.push(appendEvent(verificationEvent));
|
|
4078
|
+
return fold(events).find((candidate) => candidate.id === snapshot.id);
|
|
4079
|
+
});
|
|
4080
|
+
printLine(`${snapshot.id} verification ${passed ? 'passed' : 'failed'} (exit ${exitCode})`);
|
|
4081
|
+
return {
|
|
4082
|
+
intent: publicIntent(intent),
|
|
4083
|
+
verification: publicVerification(intent.verification),
|
|
4084
|
+
exitCode,
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
4087
|
+
|
|
3580
4088
|
const commands = {
|
|
3581
4089
|
begin(argv) {
|
|
3582
4090
|
const { positionals, flags } = parseArgs(argv, {
|
|
4091
|
+
accept: 'multiple',
|
|
3583
4092
|
verify: '-v',
|
|
3584
4093
|
decision: 'multiple',
|
|
3585
4094
|
force: 'boolean',
|
|
@@ -3588,6 +4097,13 @@ const commands = {
|
|
|
3588
4097
|
if (!intent) {
|
|
3589
4098
|
fail(usageFor('begin'));
|
|
3590
4099
|
}
|
|
4100
|
+
const acceptance = [...new Set((flags.accept || []).map((criterion) => criterion.trim()))];
|
|
4101
|
+
if (acceptance.some((criterion) => criterion.length === 0)) {
|
|
4102
|
+
fail('--accept requires a non-empty observable outcome');
|
|
4103
|
+
}
|
|
4104
|
+
if (acceptance.length > 0 && (!flags.verify || flags.verify.trim().length === 0)) {
|
|
4105
|
+
fail('--accept requires --verify with the exact machine verification command');
|
|
4106
|
+
}
|
|
3591
4107
|
const requestedDecisions = flags.decision || [];
|
|
3592
4108
|
const index = requestedDecisions.length > 0 ? decisionIndex() : [];
|
|
3593
4109
|
const decisions = [
|
|
@@ -3628,6 +4144,7 @@ const commands = {
|
|
|
3628
4144
|
id,
|
|
3629
4145
|
ts: new Date().toISOString(),
|
|
3630
4146
|
intent,
|
|
4147
|
+
acceptance,
|
|
3631
4148
|
verify: flags.verify || null,
|
|
3632
4149
|
decisions,
|
|
3633
4150
|
head: gitCapture(['rev-parse', 'HEAD']),
|
|
@@ -3637,6 +4154,16 @@ const commands = {
|
|
|
3637
4154
|
return publicIntent(record);
|
|
3638
4155
|
},
|
|
3639
4156
|
|
|
4157
|
+
verify(argv) {
|
|
4158
|
+
const { positionals, flags } = parseArgs(
|
|
4159
|
+
argv,
|
|
4160
|
+
{ 'allow-tracked-command': 'boolean' },
|
|
4161
|
+
'verify'
|
|
4162
|
+
);
|
|
4163
|
+
if (positionals.length > 0) fail(usageFor('verify'));
|
|
4164
|
+
return runMachineVerification({ allowTrackedCommand: flags['allow-tracked-command'] === true });
|
|
4165
|
+
},
|
|
4166
|
+
|
|
3640
4167
|
end(argv) {
|
|
3641
4168
|
const { positionals, flags } = parseArgs(argv, {
|
|
3642
4169
|
status: '-s',
|
|
@@ -3661,6 +4188,25 @@ const commands = {
|
|
|
3661
4188
|
if (!target) fail('no intent in progress; nothing to end');
|
|
3662
4189
|
}
|
|
3663
4190
|
|
|
4191
|
+
let completionWorkspace = null;
|
|
4192
|
+
let completionVerificationId = null;
|
|
4193
|
+
if (status === 'completed' && target.acceptance.length > 0) {
|
|
4194
|
+
if (!target.verification || !target.verification.passed) {
|
|
4195
|
+
fail(
|
|
4196
|
+
`cannot complete acceptance-bound intent ${target.id} without successful machine verification; ` +
|
|
4197
|
+
'run: driftseal verify'
|
|
4198
|
+
);
|
|
4199
|
+
}
|
|
4200
|
+
completionWorkspace = workspaceFingerprint();
|
|
4201
|
+
if (completionWorkspace !== target.verification.workspace) {
|
|
4202
|
+
fail(
|
|
4203
|
+
`cannot complete acceptance-bound intent ${target.id}: workspace changed after machine verification; ` +
|
|
4204
|
+
'run: driftseal verify'
|
|
4205
|
+
);
|
|
4206
|
+
}
|
|
4207
|
+
completionVerificationId = target.verification.verificationId;
|
|
4208
|
+
}
|
|
4209
|
+
|
|
3664
4210
|
if (['failed', 'abandoned'].includes(status)) {
|
|
3665
4211
|
const terminalStatus = closeIntentAsEscape(
|
|
3666
4212
|
events,
|
|
@@ -3715,6 +4261,8 @@ const commands = {
|
|
|
3715
4261
|
status,
|
|
3716
4262
|
note: flags.note || null,
|
|
3717
4263
|
verifyResult: flags['verify-result'] || null,
|
|
4264
|
+
verificationId: completionVerificationId,
|
|
4265
|
+
workspace: completionWorkspace,
|
|
3718
4266
|
head: gitCapture(['rev-parse', 'HEAD']),
|
|
3719
4267
|
}));
|
|
3720
4268
|
const record = fold(events).find((candidate) => candidate.id === target.id);
|
|
@@ -4059,7 +4607,10 @@ const commands = {
|
|
|
4059
4607
|
knownManagedBlocks: [
|
|
4060
4608
|
...sourceLanguages.flatMap((source) => [
|
|
4061
4609
|
protocolEol(intentProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
4610
|
+
protocolEol(previousIntentProtocolBlock(12, source), eol),
|
|
4611
|
+
protocolEol(previousIntentProtocolBlock(12, source, true), eol),
|
|
4062
4612
|
protocolEol(previousIntentProtocolBlock(11, source), eol),
|
|
4613
|
+
protocolEol(previousIntentProtocolBlock(11, source, true), eol),
|
|
4063
4614
|
]),
|
|
4064
4615
|
protocolEol(previousIntentProtocolBlock(2), eol),
|
|
4065
4616
|
protocolEol(previousIntentProtocolBlock(3), eol),
|
|
@@ -4083,7 +4634,10 @@ const commands = {
|
|
|
4083
4634
|
knownManagedBlocks: [
|
|
4084
4635
|
...sourceLanguages.flatMap((source) => [
|
|
4085
4636
|
protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
4637
|
+
protocolEol(previousDecisionProtocolBlock(12, source), eol),
|
|
4638
|
+
protocolEol(previousDecisionProtocolBlock(12, source, true), eol),
|
|
4086
4639
|
protocolEol(previousDecisionProtocolBlock(11, source), eol),
|
|
4640
|
+
protocolEol(previousDecisionProtocolBlock(11, source, true), eol),
|
|
4087
4641
|
]),
|
|
4088
4642
|
protocolEol(previousDecisionProtocolBlock(2), eol),
|
|
4089
4643
|
protocolEol(previousDecisionProtocolBlock(3), eol),
|
|
@@ -4146,7 +4700,11 @@ const commands = {
|
|
|
4146
4700
|
Intent-level write-ahead log for agent sessions.
|
|
4147
4701
|
|
|
4148
4702
|
usage:
|
|
4149
|
-
driftseal begin "<intent>" [--
|
|
4703
|
+
driftseal begin "<intent>" [--accept "<observable outcome>"] [--verify "<command>"]
|
|
4704
|
+
[--decision <id>] [--force]
|
|
4705
|
+
driftseal verify [--allow-tracked-command]
|
|
4706
|
+
run the declared command and bind its result
|
|
4707
|
+
to the current Git-visible workspace contents
|
|
4150
4708
|
driftseal end [id] [--status completed|partial|failed|abandoned] [--note "..."] [--verify-result "..."]
|
|
4151
4709
|
driftseal status show the intent currently in progress (re-anchor after drift)
|
|
4152
4710
|
driftseal log [--last N] [--all] show intent history (--all includes reclaimed records)
|
|
@@ -4214,7 +4772,7 @@ function requestedEndStatus(argv) {
|
|
|
4214
4772
|
* subcommand, mirroring the value-taking entries of each parseArgs spec.
|
|
4215
4773
|
*/
|
|
4216
4774
|
const VALUE_TAKING_FLAGS = {
|
|
4217
|
-
begin: ['--verify', '-v', '--decision'],
|
|
4775
|
+
begin: ['--accept', '--verify', '-v', '--decision'],
|
|
4218
4776
|
end: ['--status', '-s', '--note', '-n', '--verify-result', '-r'],
|
|
4219
4777
|
log: ['--last', '-n'],
|
|
4220
4778
|
reclaim: ['--reason', '-r', '--older-than'],
|
|
@@ -4319,7 +4877,11 @@ function dispatch(argv) {
|
|
|
4319
4877
|
const data = withMutationLocks(resources, () => fn(rest));
|
|
4320
4878
|
return { data, exitCode: data && Number.isInteger(data.exitCode) ? data.exitCode : 0 };
|
|
4321
4879
|
}
|
|
4322
|
-
|
|
4880
|
+
const data = fn(rest);
|
|
4881
|
+
return {
|
|
4882
|
+
data,
|
|
4883
|
+
exitCode: data && Number.isInteger(data.exitCode) ? data.exitCode : 0,
|
|
4884
|
+
};
|
|
4323
4885
|
} catch (err) {
|
|
4324
4886
|
if (err instanceof HelpRequested) {
|
|
4325
4887
|
printLine(usageFor(err.usageKey) || usageFor(cmd) || 'run: driftseal help');
|
|
@@ -4355,7 +4917,15 @@ function runCommand(argv, { root = process.cwd(), isolateStorage = false, captur
|
|
|
4355
4917
|
const previousIntentHome = process.env.DRIFTSEAL_HOME;
|
|
4356
4918
|
const previousDecisionHome = process.env.DRIFTSEAL_DECISION_HOME;
|
|
4357
4919
|
const output = { stdout: '', stderr: '', data: null, exitCode: 0, readOnly: false };
|
|
4920
|
+
const captures = capture
|
|
4921
|
+
? { stdout: createBoundedOutputCapture(), stderr: createBoundedOutputCapture() }
|
|
4922
|
+
: null;
|
|
4358
4923
|
const previousOutput = activeOutput;
|
|
4924
|
+
const finalizeCapturedOutput = () => {
|
|
4925
|
+
if (!captures) return;
|
|
4926
|
+
output.stdout = renderBoundedOutput(captures.stdout);
|
|
4927
|
+
output.stderr = renderBoundedOutput(captures.stderr);
|
|
4928
|
+
};
|
|
4359
4929
|
|
|
4360
4930
|
try {
|
|
4361
4931
|
process.chdir(fixedRoot);
|
|
@@ -4363,14 +4933,16 @@ function runCommand(argv, { root = process.cwd(), isolateStorage = false, captur
|
|
|
4363
4933
|
delete process.env.DRIFTSEAL_HOME;
|
|
4364
4934
|
delete process.env.DRIFTSEAL_DECISION_HOME;
|
|
4365
4935
|
}
|
|
4366
|
-
if (capture) activeOutput =
|
|
4936
|
+
if (capture) activeOutput = captures;
|
|
4367
4937
|
const result = dispatch(argv);
|
|
4368
4938
|
output.data = result.data;
|
|
4369
4939
|
output.exitCode = result.exitCode;
|
|
4370
4940
|
output.readOnly = result.readOnly === true;
|
|
4941
|
+
finalizeCapturedOutput();
|
|
4371
4942
|
return output;
|
|
4372
4943
|
} catch (err) {
|
|
4373
4944
|
if (capture) {
|
|
4945
|
+
finalizeCapturedOutput();
|
|
4374
4946
|
err.stdout = output.stdout;
|
|
4375
4947
|
err.stderr = output.stderr;
|
|
4376
4948
|
}
|
|
@@ -4406,13 +4978,17 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
|
4406
4978
|
status() {
|
|
4407
4979
|
return call(['status']);
|
|
4408
4980
|
},
|
|
4409
|
-
begin({ intent, verify, decisions = [], force = false }) {
|
|
4981
|
+
begin({ intent, acceptance = [], verify, decisions = [], force = false }) {
|
|
4410
4982
|
const argv = ['begin', intent];
|
|
4983
|
+
for (const criterion of acceptance) appendFlag(argv, '--accept', criterion);
|
|
4411
4984
|
appendFlag(argv, '--verify', verify);
|
|
4412
4985
|
for (const decision of decisions) appendFlag(argv, '--decision', decision);
|
|
4413
4986
|
if (force) argv.push('--force');
|
|
4414
4987
|
return call(argv);
|
|
4415
4988
|
},
|
|
4989
|
+
verify({ allowTrackedCommand = false } = {}) {
|
|
4990
|
+
return call(['verify', ...(allowTrackedCommand ? ['--allow-tracked-command'] : [])]);
|
|
4991
|
+
},
|
|
4416
4992
|
end({ id, status, note, verifyResult } = {}) {
|
|
4417
4993
|
const argv = ['end'];
|
|
4418
4994
|
if (id) argv.push(String(id));
|