driftseal 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -15
- package/README.zh-CN.md +31 -13
- package/bin/driftseal.js +364 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -210,17 +210,28 @@ driftseal verify
|
|
|
210
210
|
`driftseal verify` passes the exact stored string to the operating-system shell.
|
|
211
211
|
The command can therefore read or modify files, access the network, or run any
|
|
212
212
|
other program available to the current user. Treat it as executable code, not as
|
|
213
|
-
passive log data.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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.
|
|
218
226
|
|
|
219
227
|
The verification event records the command's exit status, duration, output
|
|
220
228
|
digest and byte counts, Git HEAD, and a fingerprint of every tracked or
|
|
221
229
|
untracked non-ignored workspace file except the intent event log. A successful
|
|
222
230
|
result becomes stale if those workspace contents change. DriftSeal therefore
|
|
223
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.
|
|
224
235
|
Ignored files are deliberately outside this fingerprint. Outside a Git
|
|
225
236
|
worktree the fingerprint is unavailable, so the gate proves only the command's
|
|
226
237
|
recorded exit status and cannot detect later content changes.
|
|
@@ -240,22 +251,39 @@ driftseal end \
|
|
|
240
251
|
|
|
241
252
|
If the scope changes, close the current intent as `partial` or `abandoned`, then start a new one. After context loss, use `driftseal status` and `driftseal log --last 3` to re-anchor.
|
|
242
253
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
254
|
+
Record an intent for changes intended to persist in the project: edits to code,
|
|
255
|
+
configuration, documentation, dependencies, and equivalent project files. The
|
|
256
|
+
boundary does not depend on Git: inside a worktree it includes content intended
|
|
257
|
+
for commit, while outside Git it includes durable project files. Everything else
|
|
258
|
+
is exempt. Git operations are entirely outside the intent log because Git
|
|
259
|
+
maintains their history; inspection, branch and worktree management, staging,
|
|
260
|
+
commits, merges, rebases, cherry-picks, tags, and pushes never need an intent of
|
|
261
|
+
their own, though they still require normal authorization and safety checks.
|
|
262
|
+
Single-step builds and checks, such as compiling or running tests, need no
|
|
263
|
+
intent. Auxiliary file or shell operations whose results remain outside durable
|
|
264
|
+
project content — an `rsync` scratch copy, temp scaffolding — need none either.
|
|
265
|
+
State changes to a remote machine or the local environment are also exempt when
|
|
266
|
+
they do not write durable project content into this workspace. When an external
|
|
267
|
+
operation does bring durable content into the project, record the intent for
|
|
268
|
+
that project-content change, not for the external operation itself.
|
|
269
|
+
|
|
270
|
+
In multi-agent work the scope belongs to the worktree, not the writer. One
|
|
271
|
+
worktree holds one open intent; every agent or subagent changing durable project
|
|
272
|
+
content there re-anchors and continues that matching intent. Agents in separate
|
|
273
|
+
worktrees hold separate intents. A configured project root outside Git follows
|
|
274
|
+
the same single-intent rule. An agent that only receives another agent's changes
|
|
275
|
+
through Git or into a shared worktree records no receiving intent and lets
|
|
276
|
+
`verify` expose misalignment. Handoff files are exempt while ignored or
|
|
277
|
+
otherwise kept outside durable project content and require an intent when
|
|
278
|
+
promoted into it. Taking over work in the same root is a re-anchor, not a
|
|
279
|
+
boundary: resume the open intent when its objective still matches the task.
|
|
252
280
|
|
|
253
281
|
## Commands
|
|
254
282
|
|
|
255
283
|
| Command | Purpose |
|
|
256
284
|
| --- | --- |
|
|
257
285
|
| `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
|
|
286
|
+
| `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. |
|
|
259
287
|
| `driftseal end [id] [-s status] [-n note] [-r verify-result]` | Close an intent honestly. |
|
|
260
288
|
| `driftseal status` | Show the intent currently in progress. |
|
|
261
289
|
| `driftseal log [-n N] [--all]` | Review intent history (`--all` includes reclaimed records). |
|
package/README.zh-CN.md
CHANGED
|
@@ -199,16 +199,23 @@ driftseal verify
|
|
|
199
199
|
|
|
200
200
|
`driftseal verify` 会把日志中保存的完整字符串交给操作系统 shell。这个命令可以
|
|
201
201
|
读写文件、访问网络,也可以运行当前用户有权执行的任何程序;因此它是可执行代码,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 一起复制到别处也不会转移信任。
|
|
207
212
|
|
|
208
213
|
验证事件会记录 exit status、耗时、输出摘要及字节数、Git HEAD,以及当前所有
|
|
209
214
|
tracked 和未被 ignore 的 untracked 文件的内容指纹(intent event log 除外)。
|
|
210
215
|
验证后只要这些内容发生变化,成功证据就会过期,必须重新运行;否则 DriftSeal
|
|
211
|
-
会拒绝把 intent 关闭为 `completed
|
|
216
|
+
会拒绝把 intent 关闭为 `completed`。命令输出会先写入临时 spool 文件,而不是
|
|
217
|
+
受固定大小的内存 buffer 限制;命令退出后再回放并删除。因此 DriftSeal 不再限制
|
|
218
|
+
输出大小,但实际容量仍受可用磁盘空间约束。被 ignore 的文件不在指纹范围内。
|
|
212
219
|
如果当前目录不是 Git worktree,指纹不可用;此时 gate 只能证明记录到的 exit
|
|
213
220
|
status,无法发现之后发生的内容变化。
|
|
214
221
|
|
|
@@ -228,19 +235,30 @@ driftseal end \
|
|
|
228
235
|
|
|
229
236
|
如果范围发生变化,先把当前 intent 以 `partial` 或 `abandoned` 关闭,再开启新的 intent。发生 context loss 后,用 `driftseal status` 和 `driftseal log --last 3` 重新锚定当前目标。
|
|
230
237
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
intent
|
|
236
|
-
|
|
238
|
+
需要记录 intent 的是准备作为项目内容长期保留的改动,包括代码、配置、文档、依赖及同类
|
|
239
|
+
项目文件。这个边界不取决于 Git:在 worktree 中,它包括准备提交的内容;在非 Git 项目中,
|
|
240
|
+
它包括要长期保留的项目文件。其余操作一律免记。Git 会自行维护操作历史,因此查看状态、
|
|
241
|
+
管理 branch 或 worktree、stage、commit、merge、rebase、cherry-pick、tag 和 push 都
|
|
242
|
+
不需要单独开启 intent,但仍须遵守正常的授权与安全要求。编译、跑测试等单步构建或检查也
|
|
243
|
+
不需要 intent。结果不会成为持久项目内容的辅助文件或 shell 操作——比如 `rsync` 临时拷贝、
|
|
244
|
+
临时脚手架——同样免记。远程机器或本机环境的状态变更,只要不把持久项目内容写入当前
|
|
245
|
+
workspace,也不属于 intent log;如果外部操作确实把持久内容带进项目,记录的是落入项目的
|
|
246
|
+
内容改动,而不是外部操作本身。
|
|
247
|
+
|
|
248
|
+
多 agent 协作时,intent 的作用域属于 worktree,而不是写入者。一个 worktree 只能有一个
|
|
249
|
+
open intent;在那里改动持久项目内容的所有 agent 和 subagent 都要先重新锚定,再继续同一个
|
|
250
|
+
匹配的 intent。不同 worktree 各自持有 intent;非 Git 项目的 configured root 同样只持有
|
|
251
|
+
一个。只通过 Git 或共享 worktree 接收其他 agent 变更的一方不另记 receiving intent,交给
|
|
252
|
+
`verify` 暴露不一致。handoff 文件在被 ignore 或以其他方式排除在持久项目内容之外时免记,
|
|
253
|
+
一旦转为正式项目内容就需要 intent。在同一个 root 中途接手工作属于重新锚定而非边界:目标
|
|
254
|
+
仍然匹配就继续沿用 open intent。
|
|
237
255
|
|
|
238
256
|
## 命令速览
|
|
239
257
|
|
|
240
258
|
| Command | 用途 |
|
|
241
259
|
| --- | --- |
|
|
242
260
|
| `driftseal begin "<intent>" [--accept "<outcome>"] [-v "<command>"] [--decision id] [--force]` | 开启一轮工作。可重复使用 `--accept` 声明可观察的完成条件;一旦声明,就必须同时提供验证命令。 |
|
|
243
|
-
| `driftseal verify [--allow-tracked-command]` | 执行 acceptance-bound intent 预先声明的命令,并把机器证据绑定到当前 Git
|
|
261
|
+
| `driftseal verify [--allow-tracked-command]` | 执行 acceptance-bound intent 预先声明的命令,并把机器证据绑定到当前 Git 可见的工作区内容;没有匹配本地 provenance 的命令必须显式 opt in。 |
|
|
244
262
|
| `driftseal end [id] [-s status] [-n note] [-r verify-result]` | 诚实地关闭 intent。 |
|
|
245
263
|
| `driftseal status` | 查看当前进行中的 intent。 |
|
|
246
264
|
| `driftseal log [-n N] [--all]` | 查看 intent 历史(`--all` 包含已回收的记录)。 |
|
package/bin/driftseal.js
CHANGED
|
@@ -27,6 +27,7 @@ const path = require('path');
|
|
|
27
27
|
const crypto = require('crypto');
|
|
28
28
|
const os = require('os');
|
|
29
29
|
const { isDeepStrictEqual } = require('util');
|
|
30
|
+
const { StringDecoder } = require('string_decoder');
|
|
30
31
|
const { execFileSync, spawnSync } = require('child_process');
|
|
31
32
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
32
33
|
|
|
@@ -40,7 +41,7 @@ const DECISION_STATUSES = [
|
|
|
40
41
|
'superseded',
|
|
41
42
|
];
|
|
42
43
|
const EVENT_SCHEMA_VERSION = 4;
|
|
43
|
-
const PROTOCOL_VERSION =
|
|
44
|
+
const PROTOCOL_VERSION = 14;
|
|
44
45
|
const DEFAULT_LOG_LANGUAGE = 'en';
|
|
45
46
|
const IN_PROGRESS_GIT_PATH = 'driftseal-in-progress.jsonl';
|
|
46
47
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
@@ -48,6 +49,10 @@ const LOCK_INIT_STALE_MS = 5 * 1000;
|
|
|
48
49
|
const READ_ONLY_NOTICE = '(read-only: another mutation holds the lock; tail repair skipped)';
|
|
49
50
|
const READ_ONLY_LOCK_WAIT_MS = Number(process.env._DRIFTSEAL_TEST_READ_ONLY_LOCK_WAIT_MS) || 1500;
|
|
50
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';
|
|
51
56
|
|
|
52
57
|
class DriftSealError extends Error {
|
|
53
58
|
constructor(message) {
|
|
@@ -98,39 +103,70 @@ function usageFor(key) {
|
|
|
98
103
|
|
|
99
104
|
let activeOutput = null;
|
|
100
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
|
+
|
|
101
149
|
function printLine(value = '') {
|
|
102
150
|
const text = String(value);
|
|
103
|
-
if (
|
|
104
|
-
activeOutput.stdout += text + '\n';
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
151
|
+
if (captureOutput('stdout', text + '\n')) return;
|
|
107
152
|
console.log(text);
|
|
108
153
|
}
|
|
109
154
|
|
|
110
155
|
function printError(value = '') {
|
|
111
156
|
const text = String(value);
|
|
112
|
-
if (
|
|
113
|
-
activeOutput.stderr += text + '\n';
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
157
|
+
if (captureOutput('stderr', text + '\n')) return;
|
|
116
158
|
console.error(text);
|
|
117
159
|
}
|
|
118
160
|
|
|
119
161
|
function writeOutput(value) {
|
|
120
162
|
const text = String(value);
|
|
121
|
-
if (
|
|
122
|
-
activeOutput.stdout += text;
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
163
|
+
if (captureOutput('stdout', text)) return;
|
|
125
164
|
process.stdout.write(text);
|
|
126
165
|
}
|
|
127
166
|
|
|
128
167
|
function writeErrorOutput(value) {
|
|
129
168
|
const text = String(value);
|
|
130
|
-
if (
|
|
131
|
-
activeOutput.stderr += text;
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
169
|
+
if (captureOutput('stderr', text)) return;
|
|
134
170
|
process.stderr.write(text);
|
|
135
171
|
}
|
|
136
172
|
|
|
@@ -569,7 +605,12 @@ function parkedOpenIntent(park) {
|
|
|
569
605
|
|
|
570
606
|
function appendEvent(event) {
|
|
571
607
|
const park = inProgressFile();
|
|
572
|
-
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
|
+
}
|
|
573
614
|
|
|
574
615
|
const open = parkedOpenIntent(park);
|
|
575
616
|
// A park with nothing open left in it belongs in the log; an interrupted end retries here.
|
|
@@ -591,9 +632,88 @@ function contentHash(content) {
|
|
|
591
632
|
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
592
633
|
}
|
|
593
634
|
|
|
594
|
-
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) {
|
|
595
715
|
const existed = fs.existsSync(target);
|
|
596
|
-
const mode = existed ? fs.statSync(target).mode & 0o777 :
|
|
716
|
+
const mode = existed ? fs.statSync(target).mode & 0o777 : createMode;
|
|
597
717
|
const temp = path.join(
|
|
598
718
|
path.dirname(target),
|
|
599
719
|
`.${path.basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`
|
|
@@ -1754,20 +1874,31 @@ MCP and lifecycle hooks are optional adapters.
|
|
|
1754
1874
|
|
|
1755
1875
|
${intentLogLanguageParagraph(language)}
|
|
1756
1876
|
|
|
1757
|
-
1. **Write intent first**, before
|
|
1758
|
-
making any other non-Git change that may need a rollback:
|
|
1877
|
+
1. **Write intent first**, before changing durable project content:
|
|
1759
1878
|
\`driftseal begin "<what this round will accomplish>" --accept "<observable outcome>" --verify "<exact command that proves it>"\`.
|
|
1760
1879
|
Repeat \`--accept\` when completion has multiple independently observable criteria.
|
|
1761
1880
|
Add one \`--decision <id>\` for each existing decision this round may change.
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1881
|
+
Record intents for changes intended to persist in the project: edits to code,
|
|
1882
|
+
configuration, documentation, dependencies, and equivalent project files,
|
|
1883
|
+
whether or not the project is inside a Git worktree. Everything else is
|
|
1884
|
+
exempt: Git operations (Git maintains their history — inspection, branch
|
|
1885
|
+
and worktree management, staging, commits, merges, rebases, cherry-picks,
|
|
1886
|
+
tags, and pushes); single-step commands that only build or check work
|
|
1887
|
+
already done, such as compiling or running tests; auxiliary file or shell
|
|
1888
|
+
operations whose results remain outside durable project content (for example
|
|
1889
|
+
an rsync scratch copy or temp scaffolding); and state changes to a remote
|
|
1890
|
+
machine or the local environment that do not write durable project content
|
|
1891
|
+
into this workspace. When an external operation does bring durable content
|
|
1892
|
+
into the project, record an intent for that project-content change, not for
|
|
1893
|
+
the external operation itself.
|
|
1894
|
+
In multi-agent work, one open intent belongs to one worktree, or to one
|
|
1895
|
+
configured project root outside Git. Every agent or subagent that changes
|
|
1896
|
+
durable project content in the same root first re-anchors and continues its
|
|
1897
|
+
matching open intent; agents working in separate worktrees hold separate
|
|
1898
|
+
intents. An agent that only receives another agent's changes through Git or
|
|
1899
|
+
into a shared worktree records no receiving intent and lets \`verify\` expose
|
|
1900
|
+
misalignment; handoff files are exempt while ignored or otherwise kept
|
|
1901
|
+
outside durable project content and require an intent when promoted into it.
|
|
1771
1902
|
Size an intent to the smallest unit that leaves the tree self-consistent
|
|
1772
1903
|
and can be verified on its own.
|
|
1773
1904
|
2. **Execute only the intent.** Scope change? Close the current intent
|
|
@@ -1794,12 +1925,15 @@ ${intentLogLanguageParagraph(language)}
|
|
|
1794
1925
|
by the next linked \`decision update\` or successful \`end\`. Closing as
|
|
1795
1926
|
\`failed\` or \`abandoned\` cancels pending recovery for that intent.
|
|
1796
1927
|
Git operations remain subject to normal authorization and safety requirements
|
|
1797
|
-
even though they do not require an intent. Any
|
|
1798
|
-
preparing a Git operation
|
|
1928
|
+
even though they do not require an intent. Any content change made while
|
|
1929
|
+
preparing a Git operation still requires an intent when it meets the
|
|
1930
|
+
durable-project-content rule in step 1.
|
|
1799
1931
|
4. **Re-anchor after context loss**: run \`driftseal status\` and \`driftseal log --last 3\` before
|
|
1800
1932
|
doing anything else. The open intent is the source of truth: resume it when its
|
|
1801
1933
|
objective still matches the current task; otherwise close it (\`partial\` or
|
|
1802
|
-
\`abandoned\`, with a note) and \`begin\` a new one.
|
|
1934
|
+
\`abandoned\`, with a note) and \`begin\` a new one. Taking over work in the
|
|
1935
|
+
same root from another agent is the same re-anchor: resume the open intent
|
|
1936
|
+
when its objective still matches.
|
|
1803
1937
|
|
|
1804
1938
|
**Log access goes only through DriftSeal.** Never read, edit, move, or delete
|
|
1805
1939
|
\`.intent-log/events.jsonl\` (or anything under \`$DRIFTSEAL_HOME\`) directly; use
|
|
@@ -1815,7 +1949,75 @@ ${INTENT_PROTOCOL_END}`;
|
|
|
1815
1949
|
}
|
|
1816
1950
|
|
|
1817
1951
|
function previousIntentProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
1818
|
-
const
|
|
1952
|
+
const v13 = intentProtocolBlock(version, language, localLog)
|
|
1953
|
+
.replace(
|
|
1954
|
+
'1. **Write intent first**, before changing durable project content:\n' +
|
|
1955
|
+
' `driftseal begin "<what this round will accomplish>" --accept "<observable outcome>" --verify "<exact command that proves it>"`.\n' +
|
|
1956
|
+
' Repeat `--accept` when completion has multiple independently observable criteria.\n' +
|
|
1957
|
+
' Add one `--decision <id>` for each existing decision this round may change.\n' +
|
|
1958
|
+
' Record intents for changes intended to persist in the project: edits to code,\n' +
|
|
1959
|
+
' configuration, documentation, dependencies, and equivalent project files,\n' +
|
|
1960
|
+
' whether or not the project is inside a Git worktree. Everything else is\n' +
|
|
1961
|
+
' exempt: Git operations (Git maintains their history — inspection, branch\n' +
|
|
1962
|
+
' and worktree management, staging, commits, merges, rebases, cherry-picks,\n' +
|
|
1963
|
+
' tags, and pushes); single-step commands that only build or check work\n' +
|
|
1964
|
+
' already done, such as compiling or running tests; auxiliary file or shell\n' +
|
|
1965
|
+
' operations whose results remain outside durable project content (for example\n' +
|
|
1966
|
+
' an rsync scratch copy or temp scaffolding); and state changes to a remote\n' +
|
|
1967
|
+
' machine or the local environment that do not write durable project content\n' +
|
|
1968
|
+
' into this workspace. When an external operation does bring durable content\n' +
|
|
1969
|
+
' into the project, record an intent for that project-content change, not for\n' +
|
|
1970
|
+
' the external operation itself.\n' +
|
|
1971
|
+
' In multi-agent work, one open intent belongs to one worktree, or to one\n' +
|
|
1972
|
+
' configured project root outside Git. Every agent or subagent that changes\n' +
|
|
1973
|
+
' durable project content in the same root first re-anchors and continues its\n' +
|
|
1974
|
+
' matching open intent; agents working in separate worktrees hold separate\n' +
|
|
1975
|
+
" intents. An agent that only receives another agent's changes through Git or\n" +
|
|
1976
|
+
' into a shared worktree records no receiving intent and lets `verify` expose\n' +
|
|
1977
|
+
' misalignment; handoff files are exempt while ignored or otherwise kept\n' +
|
|
1978
|
+
' outside durable project content and require an intent when promoted into it.\n' +
|
|
1979
|
+
' Size an intent to the smallest unit that leaves the tree self-consistent\n' +
|
|
1980
|
+
' and can be verified on its own.',
|
|
1981
|
+
'1. **Write intent first**, before modifying, creating, or deleting files, or\n' +
|
|
1982
|
+
' making any other non-Git change that may need a rollback:\n' +
|
|
1983
|
+
' `driftseal begin "<what this round will accomplish>" --accept "<observable outcome>" --verify "<exact command that proves it>"`.\n' +
|
|
1984
|
+
' Repeat `--accept` when completion has multiple independently observable criteria.\n' +
|
|
1985
|
+
' Add one `--decision <id>` for each existing decision this round may change.\n' +
|
|
1986
|
+
' Git operations never need an intent and are not included in the intent log;\n' +
|
|
1987
|
+
' Git maintains their history. This includes inspection, branch and worktree\n' +
|
|
1988
|
+
' management, staging, commits, merges, rebases, cherry-picks, tags, and pushes.\n' +
|
|
1989
|
+
' A command whose result can be reconstructed from Git state (for example a\n' +
|
|
1990
|
+
' patch file regenerated from a commit range, or a scratch harness that\n' +
|
|
1991
|
+
' re-runs) needs no intent; content that will be committed and cannot be\n' +
|
|
1992
|
+
' reconstructed (for example a .gitignore edit) does.\n' +
|
|
1993
|
+
' Single-step commands that only build or check work already done, such as\n' +
|
|
1994
|
+
' compiling or running tests, also need no intent.\n' +
|
|
1995
|
+
' Size an intent to the smallest unit that leaves the tree self-consistent\n' +
|
|
1996
|
+
' and can be verified on its own.'
|
|
1997
|
+
)
|
|
1998
|
+
.replace(
|
|
1999
|
+
' Git operations remain subject to normal authorization and safety requirements\n' +
|
|
2000
|
+
' even though they do not require an intent. Any content change made while\n' +
|
|
2001
|
+
' preparing a Git operation still requires an intent when it meets the\n' +
|
|
2002
|
+
' durable-project-content rule in step 1.',
|
|
2003
|
+
' Git operations remain subject to normal authorization and safety requirements\n' +
|
|
2004
|
+
' even though they do not require an intent. Any non-Git content change made while\n' +
|
|
2005
|
+
' preparing a Git operation does require a new intent, per the step 1 test.'
|
|
2006
|
+
)
|
|
2007
|
+
.replace(
|
|
2008
|
+
'4. **Re-anchor after context loss**: run `driftseal status` and `driftseal log --last 3` before\n' +
|
|
2009
|
+
' doing anything else. The open intent is the source of truth: resume it when its\n' +
|
|
2010
|
+
' objective still matches the current task; otherwise close it (`partial` or\n' +
|
|
2011
|
+
' `abandoned`, with a note) and `begin` a new one. Taking over work in the\n' +
|
|
2012
|
+
' same root from another agent is the same re-anchor: resume the open intent\n' +
|
|
2013
|
+
' when its objective still matches.',
|
|
2014
|
+
'4. **Re-anchor after context loss**: run `driftseal status` and `driftseal log --last 3` before\n' +
|
|
2015
|
+
' doing anything else. The open intent is the source of truth: resume it when its\n' +
|
|
2016
|
+
' objective still matches the current task; otherwise close it (`partial` or\n' +
|
|
2017
|
+
' `abandoned`, with a note) and `begin` a new one.'
|
|
2018
|
+
);
|
|
2019
|
+
if (version >= 13) return v13;
|
|
2020
|
+
const v12 = v13
|
|
1819
2021
|
.replace(
|
|
1820
2022
|
' `driftseal begin "<what this round will accomplish>" --accept "<observable outcome>" --verify "<exact command that proves it>"`.\n' +
|
|
1821
2023
|
' Repeat `--accept` when completion has multiple independently observable criteria.',
|
|
@@ -2780,11 +2982,13 @@ function hookReminder(event, { readOnly = false } = {}) {
|
|
|
2780
2982
|
if (!file) return null;
|
|
2781
2983
|
if (event === 'prompt') {
|
|
2782
2984
|
return (
|
|
2783
|
-
'DriftSeal reminder: if this round will
|
|
2784
|
-
'
|
|
2985
|
+
'DriftSeal reminder: if this round will change durable project content in this workspace ' +
|
|
2986
|
+
'(code, configuration, documentation, dependencies), ' +
|
|
2987
|
+
'begin an intent first: driftseal begin "<intent>" --accept "<observable outcome>" ' +
|
|
2785
2988
|
'--verify "<command>". ' +
|
|
2786
|
-
'Questions, read-only exploration,
|
|
2787
|
-
'
|
|
2989
|
+
'Questions, read-only exploration, single-step checks, temporary work outside durable ' +
|
|
2990
|
+
'project content, and external state changes that do not write project content here need ' +
|
|
2991
|
+
'no intent — skip this reminder when it does not apply.'
|
|
2788
2992
|
);
|
|
2789
2993
|
}
|
|
2790
2994
|
const open = openIntent(fold(readEvents({ file, readOnly })));
|
|
@@ -3802,6 +4006,101 @@ function absorbGit(baseFile, oursFile, theirsFile, { abandon, dryRun }) {
|
|
|
3802
4006
|
});
|
|
3803
4007
|
}
|
|
3804
4008
|
|
|
4009
|
+
function appendVerificationSpawnError(file, error) {
|
|
4010
|
+
const stat = fs.statSync(file);
|
|
4011
|
+
let prefix = '';
|
|
4012
|
+
if (stat.size > 0) {
|
|
4013
|
+
const fd = fs.openSync(file, 'r');
|
|
4014
|
+
const lastByte = Buffer.alloc(1);
|
|
4015
|
+
try {
|
|
4016
|
+
fs.readSync(fd, lastByte, 0, 1, stat.size - 1);
|
|
4017
|
+
} finally {
|
|
4018
|
+
fs.closeSync(fd);
|
|
4019
|
+
}
|
|
4020
|
+
if (lastByte[0] !== 0x0a) prefix = '\n';
|
|
4021
|
+
}
|
|
4022
|
+
fs.appendFileSync(file, `${prefix}${error.message}\n`, 'utf8');
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
function digestAndReplayVerificationOutput(file, writer, hash) {
|
|
4026
|
+
const fd = fs.openSync(file, 'r');
|
|
4027
|
+
const decoder = new StringDecoder('utf8');
|
|
4028
|
+
const buffer = Buffer.allocUnsafe(VERIFICATION_OUTPUT_CHUNK_BYTES);
|
|
4029
|
+
let bytes = 0;
|
|
4030
|
+
let lastCharacter = null;
|
|
4031
|
+
|
|
4032
|
+
const display = (text) => {
|
|
4033
|
+
if (!text) return;
|
|
4034
|
+
writer(text);
|
|
4035
|
+
lastCharacter = text.at(-1);
|
|
4036
|
+
};
|
|
4037
|
+
|
|
4038
|
+
try {
|
|
4039
|
+
while (true) {
|
|
4040
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
4041
|
+
if (count === 0) break;
|
|
4042
|
+
const chunk = buffer.subarray(0, count);
|
|
4043
|
+
hash.update(chunk);
|
|
4044
|
+
bytes += count;
|
|
4045
|
+
display(decoder.write(chunk));
|
|
4046
|
+
}
|
|
4047
|
+
display(decoder.end());
|
|
4048
|
+
} finally {
|
|
4049
|
+
fs.closeSync(fd);
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
return { bytes, endsWithNewline: lastCharacter === '\n' };
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
function executeVerificationCommand(command) {
|
|
4056
|
+
const spool = fs.mkdtempSync(path.join(os.tmpdir(), 'driftseal-verify-'));
|
|
4057
|
+
const stdoutFile = path.join(spool, 'stdout');
|
|
4058
|
+
const stderrFile = path.join(spool, 'stderr');
|
|
4059
|
+
let stdoutFd;
|
|
4060
|
+
let stderrFd;
|
|
4061
|
+
|
|
4062
|
+
try {
|
|
4063
|
+
stdoutFd = fs.openSync(stdoutFile, 'wx', 0o600);
|
|
4064
|
+
stderrFd = fs.openSync(stderrFile, 'wx', 0o600);
|
|
4065
|
+
const started = process.hrtime.bigint();
|
|
4066
|
+
let result;
|
|
4067
|
+
try {
|
|
4068
|
+
result = spawnSync(command, {
|
|
4069
|
+
cwd: process.cwd(),
|
|
4070
|
+
env: process.env,
|
|
4071
|
+
shell: true,
|
|
4072
|
+
stdio: ['ignore', stdoutFd, stderrFd],
|
|
4073
|
+
});
|
|
4074
|
+
} finally {
|
|
4075
|
+
fs.closeSync(stdoutFd);
|
|
4076
|
+
stdoutFd = undefined;
|
|
4077
|
+
fs.closeSync(stderrFd);
|
|
4078
|
+
stderrFd = undefined;
|
|
4079
|
+
}
|
|
4080
|
+
const durationMs = Number((process.hrtime.bigint() - started) / 1000000n);
|
|
4081
|
+
if (result.error) appendVerificationSpawnError(stderrFile, result.error);
|
|
4082
|
+
|
|
4083
|
+
const hash = crypto.createHash('sha256');
|
|
4084
|
+
const stdout = digestAndReplayVerificationOutput(stdoutFile, writeOutput, hash);
|
|
4085
|
+
if (stdout.bytes > 0 && !stdout.endsWithNewline) printLine();
|
|
4086
|
+
hash.update('\0');
|
|
4087
|
+
const stderr = digestAndReplayVerificationOutput(stderrFile, writeErrorOutput, hash);
|
|
4088
|
+
if (stderr.bytes > 0 && !stderr.endsWithNewline) printError();
|
|
4089
|
+
|
|
4090
|
+
return {
|
|
4091
|
+
result,
|
|
4092
|
+
durationMs,
|
|
4093
|
+
outputHash: hash.digest('hex'),
|
|
4094
|
+
stdoutBytes: stdout.bytes,
|
|
4095
|
+
stderrBytes: stderr.bytes,
|
|
4096
|
+
};
|
|
4097
|
+
} finally {
|
|
4098
|
+
if (stdoutFd !== undefined) fs.closeSync(stdoutFd);
|
|
4099
|
+
if (stderrFd !== undefined) fs.closeSync(stderrFd);
|
|
4100
|
+
fs.rmSync(spool, { recursive: true, force: true });
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
|
|
3805
4104
|
function runMachineVerification({ allowTrackedCommand = false } = {}) {
|
|
3806
4105
|
const snapshot = withMutationLocks([logDir()], () => {
|
|
3807
4106
|
const intent = openIntent(fold(readEvents({ repairTail: true })));
|
|
@@ -3811,56 +4110,31 @@ function runMachineVerification({ allowTrackedCommand = false } = {}) {
|
|
|
3811
4110
|
}
|
|
3812
4111
|
if (!intent.verify) fail(`intent ${intent.id} has no verification command`);
|
|
3813
4112
|
const park = inProgressFile();
|
|
3814
|
-
const parked = park
|
|
4113
|
+
const parked = park ? parkedOpenIntent(park) : null;
|
|
4114
|
+
const locallyProvenanced = hasMatchingLocalIntentProvenance(intent);
|
|
3815
4115
|
return {
|
|
3816
4116
|
id: intent.id,
|
|
3817
4117
|
command: intent.verify,
|
|
3818
|
-
|
|
4118
|
+
requiresExplicitTrust:
|
|
4119
|
+
(!parked || parked.id !== intent.id) && !locallyProvenanced,
|
|
3819
4120
|
};
|
|
3820
4121
|
});
|
|
3821
4122
|
|
|
3822
4123
|
const displayedCommand = JSON.stringify(snapshot.command);
|
|
3823
4124
|
printError(`verification command: ${displayedCommand}`);
|
|
3824
|
-
if (snapshot.
|
|
4125
|
+
if (snapshot.requiresExplicitTrust && !allowTrackedCommand) {
|
|
3825
4126
|
fail(
|
|
3826
|
-
`refusing to execute a verification command
|
|
4127
|
+
`refusing to execute a verification command that DriftSeal cannot confirm was created locally: ${displayedCommand}\n` +
|
|
4128
|
+
'no matching local intent provenance was found; ' +
|
|
3827
4129
|
'inspect the command, then re-run with --allow-tracked-command only if you trust it'
|
|
3828
4130
|
);
|
|
3829
4131
|
}
|
|
3830
4132
|
|
|
3831
|
-
const
|
|
3832
|
-
const result
|
|
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
|
-
|
|
4133
|
+
const execution = executeVerificationCommand(snapshot.command);
|
|
4134
|
+
const { result, durationMs, outputHash, stdoutBytes, stderrBytes } = execution;
|
|
3855
4135
|
const exitCode = Number.isInteger(result.status) ? result.status : 1;
|
|
3856
4136
|
const signal = typeof result.signal === 'string' ? result.signal : null;
|
|
3857
4137
|
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
4138
|
const verificationEvent = {
|
|
3865
4139
|
type: 'verify',
|
|
3866
4140
|
id: snapshot.id,
|
|
@@ -3872,8 +4146,8 @@ function runMachineVerification({ allowTrackedCommand = false } = {}) {
|
|
|
3872
4146
|
signal,
|
|
3873
4147
|
durationMs,
|
|
3874
4148
|
outputHash,
|
|
3875
|
-
stdoutBytes
|
|
3876
|
-
stderrBytes
|
|
4149
|
+
stdoutBytes,
|
|
4150
|
+
stderrBytes,
|
|
3877
4151
|
workspace: workspaceFingerprint(),
|
|
3878
4152
|
head: gitCapture(['rev-parse', 'HEAD']),
|
|
3879
4153
|
};
|
|
@@ -4417,6 +4691,8 @@ const commands = {
|
|
|
4417
4691
|
knownManagedBlocks: [
|
|
4418
4692
|
...sourceLanguages.flatMap((source) => [
|
|
4419
4693
|
protocolEol(intentProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
4694
|
+
protocolEol(previousIntentProtocolBlock(13, source), eol),
|
|
4695
|
+
protocolEol(previousIntentProtocolBlock(13, source, true), eol),
|
|
4420
4696
|
protocolEol(previousIntentProtocolBlock(12, source), eol),
|
|
4421
4697
|
protocolEol(previousIntentProtocolBlock(12, source, true), eol),
|
|
4422
4698
|
protocolEol(previousIntentProtocolBlock(11, source), eol),
|
|
@@ -4444,6 +4720,8 @@ const commands = {
|
|
|
4444
4720
|
knownManagedBlocks: [
|
|
4445
4721
|
...sourceLanguages.flatMap((source) => [
|
|
4446
4722
|
protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
4723
|
+
protocolEol(previousDecisionProtocolBlock(13, source), eol),
|
|
4724
|
+
protocolEol(previousDecisionProtocolBlock(13, source, true), eol),
|
|
4447
4725
|
protocolEol(previousDecisionProtocolBlock(12, source), eol),
|
|
4448
4726
|
protocolEol(previousDecisionProtocolBlock(12, source, true), eol),
|
|
4449
4727
|
protocolEol(previousDecisionProtocolBlock(11, source), eol),
|
|
@@ -4727,7 +5005,15 @@ function runCommand(argv, { root = process.cwd(), isolateStorage = false, captur
|
|
|
4727
5005
|
const previousIntentHome = process.env.DRIFTSEAL_HOME;
|
|
4728
5006
|
const previousDecisionHome = process.env.DRIFTSEAL_DECISION_HOME;
|
|
4729
5007
|
const output = { stdout: '', stderr: '', data: null, exitCode: 0, readOnly: false };
|
|
5008
|
+
const captures = capture
|
|
5009
|
+
? { stdout: createBoundedOutputCapture(), stderr: createBoundedOutputCapture() }
|
|
5010
|
+
: null;
|
|
4730
5011
|
const previousOutput = activeOutput;
|
|
5012
|
+
const finalizeCapturedOutput = () => {
|
|
5013
|
+
if (!captures) return;
|
|
5014
|
+
output.stdout = renderBoundedOutput(captures.stdout);
|
|
5015
|
+
output.stderr = renderBoundedOutput(captures.stderr);
|
|
5016
|
+
};
|
|
4731
5017
|
|
|
4732
5018
|
try {
|
|
4733
5019
|
process.chdir(fixedRoot);
|
|
@@ -4735,14 +5021,16 @@ function runCommand(argv, { root = process.cwd(), isolateStorage = false, captur
|
|
|
4735
5021
|
delete process.env.DRIFTSEAL_HOME;
|
|
4736
5022
|
delete process.env.DRIFTSEAL_DECISION_HOME;
|
|
4737
5023
|
}
|
|
4738
|
-
if (capture) activeOutput =
|
|
5024
|
+
if (capture) activeOutput = captures;
|
|
4739
5025
|
const result = dispatch(argv);
|
|
4740
5026
|
output.data = result.data;
|
|
4741
5027
|
output.exitCode = result.exitCode;
|
|
4742
5028
|
output.readOnly = result.readOnly === true;
|
|
5029
|
+
finalizeCapturedOutput();
|
|
4743
5030
|
return output;
|
|
4744
5031
|
} catch (err) {
|
|
4745
5032
|
if (capture) {
|
|
5033
|
+
finalizeCapturedOutput();
|
|
4746
5034
|
err.stdout = output.stdout;
|
|
4747
5035
|
err.stderr = output.stderr;
|
|
4748
5036
|
}
|