driftseal 1.3.0 → 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 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. A locally created open intent is parked in Git metadata and runs
214
- normally. If the open intent came from the repository's tracked intent log,
215
- DriftSeal prints the command to stderr and refuses to execute it until you inspect
216
- it and explicitly run `driftseal verify --allow-tracked-command`. The programmatic
217
- API and MCP tool expose the equivalent `allowTrackedCommand` opt-in.
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.
@@ -255,7 +266,7 @@ also need no intent. Any other non-Git content change starts a new work round.
255
266
  | Command | Purpose |
256
267
  | --- | --- |
257
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. |
258
- | `driftseal verify [--allow-tracked-command]` | Execute the acceptance-bound intent's predeclared command and bind machine evidence to the current Git-visible workspace contents. Commands sourced from the tracked intent log require the explicit opt-in. |
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. |
259
270
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | Close an intent honestly. |
260
271
  | `driftseal status` | Show the intent currently in progress. |
261
272
  | `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
- 不是被动的日志数据。当前 worktree 本地创建的 open intent park Git metadata
203
- 中,可以直接运行。如果 open intent 来自 repository 中被跟踪的 intent log,
204
- DriftSeal 会先把命令输出到 stderr,并拒绝执行;只有检查并信任该命令后,才能显式
205
- 运行 `driftseal verify --allow-tracked-command`。Programmatic API MCP tool 中对应的
206
- 显式开关是 `allowTrackedCommand`。
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`。被 ignore 的文件不在指纹范围内。
216
+ 会拒绝把 intent 关闭为 `completed`。命令输出会先写入临时 spool 文件,而不是
217
+ 受固定大小的内存 buffer 限制;命令退出后再回放并删除。因此 DriftSeal 不再限制
218
+ 输出大小,但实际容量仍受可用磁盘空间约束。被 ignore 的文件不在指纹范围内。
212
219
  如果当前目录不是 Git worktree,指纹不可用;此时 gate 只能证明记录到的 exit
213
220
  status,无法发现之后发生的内容变化。
214
221
 
@@ -240,7 +247,7 @@ intent;会被提交且无法重建的内容改动(比如编辑 `.gitignore`
240
247
  | Command | 用途 |
241
248
  | --- | --- |
242
249
  | `driftseal begin "<intent>" [--accept "<outcome>"] [-v "<command>"] [--decision id] [--force]` | 开启一轮工作。可重复使用 `--accept` 声明可观察的完成条件;一旦声明,就必须同时提供验证命令。 |
243
- | `driftseal verify [--allow-tracked-command]` | 执行 acceptance-bound intent 预先声明的命令,并把机器证据绑定到当前 Git 可见的工作区内容;来自 tracked intent log 的命令必须显式 opt in。 |
250
+ | `driftseal verify [--allow-tracked-command]` | 执行 acceptance-bound intent 预先声明的命令,并把机器证据绑定到当前 Git 可见的工作区内容;没有匹配本地 provenance 的命令必须显式 opt in。 |
244
251
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | 诚实地关闭 intent。 |
245
252
  | `driftseal status` | 查看当前进行中的 intent。 |
246
253
  | `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
 
@@ -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 (activeOutput) {
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 (activeOutput) {
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 (activeOutput) {
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 (activeOutput) {
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) return appendEventTo(logFile(), event);
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 atomicWriteFile(target, content) {
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 : 0o644;
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`
@@ -3802,6 +3922,101 @@ function absorbGit(baseFile, oursFile, theirsFile, { abandon, dryRun }) {
3802
3922
  });
3803
3923
  }
3804
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
+
3805
4020
  function runMachineVerification({ allowTrackedCommand = false } = {}) {
3806
4021
  const snapshot = withMutationLocks([logDir()], () => {
3807
4022
  const intent = openIntent(fold(readEvents({ repairTail: true })));
@@ -3811,56 +4026,31 @@ function runMachineVerification({ allowTrackedCommand = false } = {}) {
3811
4026
  }
3812
4027
  if (!intent.verify) fail(`intent ${intent.id} has no verification command`);
3813
4028
  const park = inProgressFile();
3814
- const parked = park && fs.existsSync(park) ? parkedOpenIntent(park) : null;
4029
+ const parked = park ? parkedOpenIntent(park) : null;
4030
+ const locallyProvenanced = hasMatchingLocalIntentProvenance(intent);
3815
4031
  return {
3816
4032
  id: intent.id,
3817
4033
  command: intent.verify,
3818
- fromTrackedLog: isParkableIntentLog() && (!parked || parked.id !== intent.id),
4034
+ requiresExplicitTrust:
4035
+ (!parked || parked.id !== intent.id) && !locallyProvenanced,
3819
4036
  };
3820
4037
  });
3821
4038
 
3822
4039
  const displayedCommand = JSON.stringify(snapshot.command);
3823
4040
  printError(`verification command: ${displayedCommand}`);
3824
- if (snapshot.fromTrackedLog && !allowTrackedCommand) {
4041
+ if (snapshot.requiresExplicitTrust && !allowTrackedCommand) {
3825
4042
  fail(
3826
- `refusing to execute a verification command sourced from the repository intent log: ${displayedCommand}\n` +
4043
+ `refusing to execute a verification command that DriftSeal cannot confirm was created locally: ${displayedCommand}\n` +
4044
+ 'no matching local intent provenance was found; ' +
3827
4045
  'inspect the command, then re-run with --allow-tracked-command only if you trust it'
3828
4046
  );
3829
4047
  }
3830
4048
 
3831
- const started = process.hrtime.bigint();
3832
- const result = spawnSync(snapshot.command, {
3833
- cwd: process.cwd(),
3834
- env: process.env,
3835
- shell: true,
3836
- encoding: 'utf8',
3837
- stdio: ['ignore', 'pipe', 'pipe'],
3838
- maxBuffer: 16 * 1024 * 1024,
3839
- });
3840
- const durationMs = Number((process.hrtime.bigint() - started) / 1000000n);
3841
- const stdout = result.stdout || '';
3842
- let stderr = result.stderr || '';
3843
- if (result.error) {
3844
- stderr += `${stderr && !stderr.endsWith('\n') ? '\n' : ''}${result.error.message}\n`;
3845
- }
3846
- if (stdout) {
3847
- writeOutput(stdout);
3848
- if (!stdout.endsWith('\n')) printLine();
3849
- }
3850
- if (stderr) {
3851
- writeErrorOutput(stderr);
3852
- if (!stderr.endsWith('\n')) printError();
3853
- }
3854
-
4049
+ const execution = executeVerificationCommand(snapshot.command);
4050
+ const { result, durationMs, outputHash, stdoutBytes, stderrBytes } = execution;
3855
4051
  const exitCode = Number.isInteger(result.status) ? result.status : 1;
3856
4052
  const signal = typeof result.signal === 'string' ? result.signal : null;
3857
4053
  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
4054
  const verificationEvent = {
3865
4055
  type: 'verify',
3866
4056
  id: snapshot.id,
@@ -3872,8 +4062,8 @@ function runMachineVerification({ allowTrackedCommand = false } = {}) {
3872
4062
  signal,
3873
4063
  durationMs,
3874
4064
  outputHash,
3875
- stdoutBytes: Buffer.byteLength(stdout),
3876
- stderrBytes: Buffer.byteLength(stderr),
4065
+ stdoutBytes,
4066
+ stderrBytes,
3877
4067
  workspace: workspaceFingerprint(),
3878
4068
  head: gitCapture(['rev-parse', 'HEAD']),
3879
4069
  };
@@ -4727,7 +4917,15 @@ function runCommand(argv, { root = process.cwd(), isolateStorage = false, captur
4727
4917
  const previousIntentHome = process.env.DRIFTSEAL_HOME;
4728
4918
  const previousDecisionHome = process.env.DRIFTSEAL_DECISION_HOME;
4729
4919
  const output = { stdout: '', stderr: '', data: null, exitCode: 0, readOnly: false };
4920
+ const captures = capture
4921
+ ? { stdout: createBoundedOutputCapture(), stderr: createBoundedOutputCapture() }
4922
+ : null;
4730
4923
  const previousOutput = activeOutput;
4924
+ const finalizeCapturedOutput = () => {
4925
+ if (!captures) return;
4926
+ output.stdout = renderBoundedOutput(captures.stdout);
4927
+ output.stderr = renderBoundedOutput(captures.stderr);
4928
+ };
4731
4929
 
4732
4930
  try {
4733
4931
  process.chdir(fixedRoot);
@@ -4735,14 +4933,16 @@ function runCommand(argv, { root = process.cwd(), isolateStorage = false, captur
4735
4933
  delete process.env.DRIFTSEAL_HOME;
4736
4934
  delete process.env.DRIFTSEAL_DECISION_HOME;
4737
4935
  }
4738
- if (capture) activeOutput = output;
4936
+ if (capture) activeOutput = captures;
4739
4937
  const result = dispatch(argv);
4740
4938
  output.data = result.data;
4741
4939
  output.exitCode = result.exitCode;
4742
4940
  output.readOnly = result.readOnly === true;
4941
+ finalizeCapturedOutput();
4743
4942
  return output;
4744
4943
  } catch (err) {
4745
4944
  if (capture) {
4945
+ finalizeCapturedOutput();
4746
4946
  err.stdout = output.stdout;
4747
4947
  err.stderr = output.stderr;
4748
4948
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftseal",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Seal intent, verification, and decisions into an auditable workflow for agentic coding",
5
5
  "keywords": [
6
6
  "driftseal",