mustflow 2.17.0 → 2.18.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.
Files changed (42) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/commands/classify.js +13 -3
  3. package/dist/cli/commands/dashboard.js +2 -1
  4. package/dist/cli/commands/explain-verify.js +2 -2
  5. package/dist/cli/commands/impact.js +13 -3
  6. package/dist/cli/commands/run.js +156 -104
  7. package/dist/cli/commands/verify.js +157 -45
  8. package/dist/cli/i18n/en.js +10 -1
  9. package/dist/cli/i18n/es.js +10 -1
  10. package/dist/cli/i18n/fr.js +10 -1
  11. package/dist/cli/i18n/hi.js +10 -1
  12. package/dist/cli/i18n/ko.js +10 -1
  13. package/dist/cli/i18n/zh.js +10 -1
  14. package/dist/cli/lib/git-changes.js +25 -2
  15. package/dist/cli/lib/local-index/constants.js +4 -1
  16. package/dist/cli/lib/local-index/index.js +22 -5
  17. package/dist/cli/lib/repo-map.js +90 -30
  18. package/dist/cli/lib/run-plan.js +25 -2
  19. package/dist/cli/lib/validation/index.js +2 -1
  20. package/dist/core/atomic-state-write.js +31 -0
  21. package/dist/core/bounded-output.js +23 -1
  22. package/dist/core/check-issues.js +3 -0
  23. package/dist/core/command-contract-rules.js +104 -2
  24. package/dist/core/command-contract-validation.js +71 -9
  25. package/dist/core/command-intent-eligibility.js +9 -1
  26. package/dist/core/command-output-limits.js +5 -0
  27. package/dist/core/completion-verdict.js +2 -1
  28. package/dist/core/contract-lint.js +10 -1
  29. package/dist/core/public-json-contracts.js +1 -1
  30. package/dist/core/run-receipt.js +20 -13
  31. package/dist/core/source-anchors.js +96 -24
  32. package/dist/core/verification-evidence.js +4 -1
  33. package/package.json +1 -1
  34. package/schemas/README.md +4 -4
  35. package/schemas/change-verification-report.schema.json +2 -1
  36. package/schemas/contract-lint-report.schema.json +2 -1
  37. package/schemas/explain-report.schema.json +1 -0
  38. package/schemas/latest-run-pointer.schema.json +1 -0
  39. package/schemas/run-receipt.schema.json +26 -3
  40. package/schemas/verify-report.schema.json +2 -1
  41. package/schemas/verify-run-manifest.schema.json +2 -1
  42. package/templates/default/manifest.toml +1 -1
package/README.md CHANGED
@@ -124,7 +124,7 @@ mustflow installs and validates an agent workflow for user projects.
124
124
  - Classifies changed files, public surfaces, and validation reasons with `mf classify`.
125
125
  - Prints execution-free verification plans with `mf verify --plan-only --json`, including a machine-readable verification decision graph and read-only local-index lock explanations when available.
126
126
  - Runs only allowed one-shot commands within a timeout via `mf run <intent>` or `mf verify` when the selected intent is runnable.
127
- - Writes command receipts to `.mustflow/state/runs/latest.json`.
127
+ - Writes command receipts under `.mustflow/state/runs/run-*` and atomically updates `.mustflow/state/runs/latest.json`.
128
128
  - Generates a concise repository navigation map, `REPO_MAP.md`, with `mf map`.
129
129
  - Indexes and searches mustflow docs, skills, skill routes, command rules, command-effect locks, file fingerprints, and opt-in source anchor metadata with SQLite via `mf index` and `mf search`. The local SQLite file is a rebuildable lookup cache, not a memory store, audit log, command transcript store, command-authority source, or source-content database.
130
130
  - Tracks agent-created or agent-modified documentation needing prose review with `mf docs review`.
@@ -354,11 +354,11 @@ Runnable work is declared in `.mustflow/config/commands.toml` so agents do not g
354
354
  - `run_policy = "agent_allowed"`
355
355
  - `stdin = "closed"`
356
356
 
357
- Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly.
357
+ Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly. `mf run` also rejects obvious long-running `argv` shapes, such as shell-wrapper background payloads, interpreter loops, package-manager development scripts, watchers, and development servers declared as one-shot commands.
358
358
 
359
359
  Use `mf verify --reason <event> --plan-only --json` to inspect matching verification intents, command eligibility, remaining gaps, and missing runnable coverage without executing commands. Use `mf run <intent> --dry-run --json` to inspect one resolved command intent without spawning a process or writing a run receipt. Plan-only verification includes a `decision_graph` that connects changed surfaces, classification reasons, command candidates, eligibility checks, effects, and gaps. When `.mustflow/cache/mustflow.sqlite` is fresh, scheduled entries also include read-only `effectGraph` metadata for write locks and lock conflicts. These graph rows are marked `explanation_only` and never grant command authority; `.mustflow/config/commands.toml` remains the only runnable command source.
360
360
 
361
- Each executed command run writes the latest run record to `.mustflow/state/runs/latest.json`. The record includes the intent name, working directory, timeout, exit code, timeout status, and the tail of stdout and stderr.
361
+ Each executed command run writes a run record under `.mustflow/state/runs/run-*` and atomically updates `.mustflow/state/runs/latest.json`. The record includes the intent name, working directory, timeout, exit code, timeout status, and the tail of stdout and stderr.
362
362
 
363
363
  ## Language and profiles
364
364
 
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createChangeClassificationReport, } from '../../core/change-classification.js';
4
4
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
5
- import { readGitChangedFiles } from '../lib/git-changes.js';
5
+ import { requireGitChangedFiles } from '../lib/git-changes.js';
6
6
  import { t } from '../lib/i18n.js';
7
7
  import { resolveMustflowRoot } from '../lib/project-root.js';
8
8
  const CLASSIFY_SCHEMA_VERSION = '1';
@@ -67,7 +67,7 @@ function parseClassifyArgs(args) {
67
67
  return { json, changed, writePath, paths };
68
68
  }
69
69
  export function createClassifyOutput(projectRoot, source, paths) {
70
- const files = source === 'changed' ? readGitChangedFiles(projectRoot) : paths;
70
+ const files = source === 'changed' ? requireGitChangedFiles(projectRoot) : paths;
71
71
  return {
72
72
  schema_version: CLASSIFY_SCHEMA_VERSION,
73
73
  command: 'classify',
@@ -136,7 +136,17 @@ export function runClassify(args, reporter, lang = 'en') {
136
136
  return 1;
137
137
  }
138
138
  const projectRoot = resolveMustflowRoot();
139
- const output = createClassifyOutput(projectRoot, parsed.changed ? 'changed' : 'paths', parsed.paths);
139
+ let output;
140
+ try {
141
+ output = createClassifyOutput(projectRoot, parsed.changed ? 'changed' : 'paths', parsed.paths);
142
+ }
143
+ catch (error) {
144
+ const message = error instanceof Error && error.message === 'git_changed_files_unavailable'
145
+ ? t(lang, 'classify.error.changed_files_unavailable')
146
+ : t(lang, 'cli.common.invalidInput');
147
+ printUsageError(reporter, message, 'mf classify --help', getClassifyHelp(lang), lang);
148
+ return 1;
149
+ }
140
150
  if (parsed.writePath) {
141
151
  try {
142
152
  writeClassifyOutput(projectRoot, parsed.writePath, output);
@@ -682,7 +682,8 @@ async function renderStatusResponse(projectRoot) {
682
682
  const activeDocuments = listDocReviewEntries(projectRoot);
683
683
  const rawCommandContract = readDashboardCommandContract(projectRoot);
684
684
  const commandContract = await renderCommandContractResponse(projectRoot, rawCommandContract);
685
- const gitChangedFiles = readGitChangedFiles(projectRoot);
685
+ const gitChangedFilesResult = readGitChangedFiles(projectRoot);
686
+ const gitChangedFiles = gitChangedFilesResult.ok ? gitChangedFilesResult.files : [];
686
687
  const packageMetadata = readPackageMetadata();
687
688
  const verification = createDashboardVerificationSnapshot(projectRoot, rawCommandContract, commandContract.intents, gitChangedFiles, manifest.changedFiles, manifest.missingFiles);
688
689
  const readModel = await readLatestLocalVerificationReadModelQueries(projectRoot);
@@ -5,7 +5,7 @@ import { createVerificationPlan, } from '../../core/verification-plan.js';
5
5
  import { createVerificationSchedule } from '../../core/verification-scheduler.js';
6
6
  import { t } from '../lib/i18n.js';
7
7
  import { readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
8
- import { planErrorMessageKey, readInputFromPlan } from './verify.js';
8
+ import { planErrorMessageKey, readInputFromClassificationReport } from './verify.js';
9
9
  export function parseExplainVerifyArgs(args) {
10
10
  let reason;
11
11
  let fromPlan;
@@ -69,7 +69,7 @@ export function explainVerifyPlanErrorMessage(error, lang) {
69
69
  return t(lang, planErrorMessageKey(code));
70
70
  }
71
71
  export function readExplainVerifyPlanReasons(projectRoot, planPath) {
72
- return readInputFromPlan(projectRoot, planPath).reasons;
72
+ return readInputFromClassificationReport(projectRoot, planPath).reasons;
73
73
  }
74
74
  export async function getVerifyExplainOutput(schemaVersion, projectRoot, reasons, inputReason, planSource) {
75
75
  const contract = readCommandContract(projectRoot);
@@ -3,7 +3,7 @@ import { createChangeClassificationReport } from '../../core/change-classificati
3
3
  import { summarizeVersionImpact } from '../../core/version-impact.js';
4
4
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
5
5
  import { isRecord } from '../lib/command-contract.js';
6
- import { readGitChangedFiles } from '../lib/git-changes.js';
6
+ import { requireGitChangedFiles } from '../lib/git-changes.js';
7
7
  import { t } from '../lib/i18n.js';
8
8
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
9
  import { readTomlFile } from '../lib/toml.js';
@@ -56,7 +56,7 @@ function readPreferences(projectRoot) {
56
56
  }
57
57
  function createImpactOutput(projectRoot, parsed) {
58
58
  const source = parsed.changed ? 'changed' : 'paths';
59
- const files = parsed.changed ? readGitChangedFiles(projectRoot) : parsed.paths;
59
+ const files = parsed.changed ? requireGitChangedFiles(projectRoot) : parsed.paths;
60
60
  const classificationReport = createChangeClassificationReport(source, files);
61
61
  const versionSources = detectVersionSources(projectRoot);
62
62
  return {
@@ -110,7 +110,17 @@ export function runImpact(args, reporter, lang = 'en') {
110
110
  printUsageError(reporter, t(lang, 'impact.error.missingInput'), 'mf impact --help', getImpactHelp(lang), lang);
111
111
  return 1;
112
112
  }
113
- const output = createImpactOutput(resolveMustflowRoot(), parsed);
113
+ let output;
114
+ try {
115
+ output = createImpactOutput(resolveMustflowRoot(), parsed);
116
+ }
117
+ catch (error) {
118
+ const message = error instanceof Error && error.message === 'git_changed_files_unavailable'
119
+ ? t(lang, 'impact.error.changed_files_unavailable')
120
+ : t(lang, 'cli.common.invalidInput');
121
+ printUsageError(reporter, message, 'mf impact --help', getImpactHelp(lang), lang);
122
+ return 1;
123
+ }
114
124
  if (parsed.json) {
115
125
  reporter.stdout(JSON.stringify(output, null, 2));
116
126
  return 0;
@@ -10,10 +10,12 @@ import { t } from '../lib/i18n.js';
10
10
  import { getPackageVersion } from '../lib/package-info.js';
11
11
  import { resolveMustflowRoot } from '../lib/project-root.js';
12
12
  import { createRunPlan, createRunPreview, isMustflowBuiltinIntent, renderRunPreviewText, } from '../lib/run-plan.js';
13
- import { createRunReceipt, writeRunReceipt } from '../../core/run-receipt.js';
13
+ import { createRunReceipt, createRunReceiptRelativePath, writeRunReceipt, } from '../../core/run-receipt.js';
14
14
  import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
15
15
  import { RunProfiler } from '../../core/run-profile.js';
16
16
  import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
17
+ const OUTPUT_LIMIT_ERROR_CODE = 'ENOBUFS';
18
+ const OUTPUT_LIMIT_ERROR_MESSAGE = /\bmaxBuffer\b.*\bexceeded\b/i;
17
19
  function emitOutput(reporter, output, stream) {
18
20
  if (!output) {
19
21
  return;
@@ -24,7 +26,7 @@ function emitOutput(reporter, output, stream) {
24
26
  }
25
27
  reporter[stream](text);
26
28
  }
27
- function terminateProcessTree(pid) {
29
+ function signalProcessTree(pid, signal) {
28
30
  if (!pid || pid <= 0) {
29
31
  return;
30
32
  }
@@ -33,23 +35,87 @@ function terminateProcessTree(pid) {
33
35
  stdio: 'ignore',
34
36
  windowsHide: true,
35
37
  });
38
+ if (signal === 'SIGKILL') {
39
+ try {
40
+ process.kill(pid, signal);
41
+ }
42
+ catch {
43
+ // taskkill may already have terminated the direct child.
44
+ }
45
+ }
36
46
  return;
37
47
  }
38
48
  try {
39
- process.kill(-pid, 'SIGTERM');
49
+ process.kill(-pid, signal);
40
50
  }
41
51
  catch {
42
52
  try {
43
- process.kill(pid, 'SIGTERM');
53
+ process.kill(pid, signal);
44
54
  }
45
55
  catch {
46
56
  // The child may already be gone after Node's spawn timeout handling.
47
57
  }
48
58
  }
49
59
  }
60
+ function signalProcessTreeNonBlocking(pid, signal) {
61
+ if (!pid || pid <= 0) {
62
+ return;
63
+ }
64
+ if (process.platform === 'win32') {
65
+ const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
66
+ stdio: 'ignore',
67
+ windowsHide: true,
68
+ detached: true,
69
+ });
70
+ killer.unref();
71
+ if (signal === 'SIGKILL') {
72
+ try {
73
+ process.kill(pid, signal);
74
+ }
75
+ catch {
76
+ // taskkill may already have terminated the direct child.
77
+ }
78
+ }
79
+ return;
80
+ }
81
+ try {
82
+ process.kill(-pid, signal);
83
+ }
84
+ catch {
85
+ try {
86
+ process.kill(pid, signal);
87
+ }
88
+ catch {
89
+ // The child may already be gone after the timeout fired.
90
+ }
91
+ }
92
+ }
93
+ function terminateProcessTree(pid) {
94
+ signalProcessTree(pid, 'SIGTERM');
95
+ }
96
+ function forceTerminateProcessTree(pid) {
97
+ signalProcessTree(pid, 'SIGKILL');
98
+ }
99
+ function terminateProcessTreeNonBlocking(pid) {
100
+ signalProcessTreeNonBlocking(pid, 'SIGTERM');
101
+ }
102
+ function forceTerminateProcessTreeNonBlocking(pid) {
103
+ signalProcessTreeNonBlocking(pid, 'SIGKILL');
104
+ }
50
105
  function getKillMethod() {
51
106
  return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
52
107
  }
108
+ function createPendingTimeoutTermination(method) {
109
+ return {
110
+ reason: 'timeout',
111
+ method,
112
+ graceful_signal: 'SIGTERM',
113
+ forced_signal: 'SIGKILL',
114
+ forced_kill_attempted: true,
115
+ confirmed: false,
116
+ cleanup_pending: true,
117
+ };
118
+ }
53
119
  function createBufferedReporter() {
54
120
  const stdout = [];
55
121
  const stderr = [];
@@ -159,35 +225,27 @@ async function runBuiltinArgvInProcess(commandArgv, cwd, lang) {
159
225
  };
160
226
  }
161
227
  }
162
- function runArgvCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
163
- return spawnSync(command?.executable ?? '', command?.args ?? [], {
164
- cwd,
165
- encoding: 'utf8',
166
- input: '',
167
- maxBuffer: maxOutputBytes,
168
- env,
169
- shell: command?.shell ?? false,
170
- stdio: ['ignore', 'pipe', 'pipe'],
171
- timeout: timeoutSeconds * 1000,
172
- windowsHide: true,
173
- });
174
- }
175
228
  function writeStreamChunk(reporter, stream, chunk) {
176
229
  if (stream === 'stdout') {
177
230
  if (reporter.writeStdout) {
178
231
  reporter.writeStdout(chunk);
179
232
  return;
180
233
  }
181
- reporter.stdout(chunk.toString().trimEnd());
234
+ reporter.stdout(chunk.toString());
182
235
  return;
183
236
  }
184
237
  if (reporter.writeStderr) {
185
238
  reporter.writeStderr(chunk);
186
239
  return;
187
240
  }
188
- reporter.stderr(chunk.toString().trimEnd());
241
+ reporter.stderr(chunk.toString());
242
+ }
243
+ function createOutputLimitError(stream, maxOutputBytes) {
244
+ return Object.assign(new Error(`${stream} exceeded max_output_bytes (${maxOutputBytes})`), {
245
+ code: OUTPUT_LIMIT_ERROR_CODE,
246
+ });
189
247
  }
190
- function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
248
+ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
191
249
  return new Promise((resolve) => {
192
250
  const stdout = new BoundedOutputBuffer(stdoutTailBytes);
193
251
  const stderr = new BoundedOutputBuffer(stderrTailBytes);
@@ -195,11 +253,14 @@ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBy
195
253
  let timedOut = false;
196
254
  let childError;
197
255
  let childPid;
256
+ let stdoutBytes = 0;
257
+ let stderrBytes = 0;
198
258
  let timeout;
199
- const child = spawn(command?.executable ?? '', command?.args ?? [], {
259
+ let termination = null;
260
+ const child = spawn(command.executable, command.args ?? [], {
200
261
  cwd,
201
262
  env,
202
- shell: command?.shell ?? false,
263
+ shell: command.shell,
203
264
  stdio: ['ignore', 'pipe', 'pipe'],
204
265
  windowsHide: true,
205
266
  detached: process.platform !== 'win32',
@@ -214,89 +275,46 @@ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBy
214
275
  clearTimeout(timeout);
215
276
  }
216
277
  resolve({
217
- status,
218
- signal,
278
+ status: timedOut ? null : status,
279
+ signal: timedOut ? null : signal,
219
280
  error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
220
281
  stdout: stdout.toSnapshot(),
221
282
  stderr: stderr.toSnapshot(),
222
283
  pid: childPid,
284
+ termination,
223
285
  });
224
286
  };
225
- child.stdout?.on('data', (chunk) => {
226
- stdout.append(chunk);
227
- writeStreamChunk(reporter, 'stdout', chunk);
228
- });
229
- child.stderr?.on('data', (chunk) => {
230
- stderr.append(chunk);
231
- writeStreamChunk(reporter, 'stderr', chunk);
232
- });
233
- child.once('error', (error) => {
234
- childError = error;
235
- });
236
- child.once('close', (status, signal) => {
237
- finish(status, signal);
238
- });
239
- timeout = setTimeout(() => {
240
- timedOut = true;
241
- terminateProcessTree(childPid);
242
- }, timeoutSeconds * 1000);
243
- });
244
- }
245
- function runShellCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
246
- return spawnSync(command ?? '', {
247
- cwd,
248
- encoding: 'utf8',
249
- input: '',
250
- maxBuffer: maxOutputBytes,
251
- env,
252
- shell: true,
253
- stdio: ['ignore', 'pipe', 'pipe'],
254
- timeout: timeoutSeconds * 1000,
255
- windowsHide: true,
256
- });
257
- }
258
- function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
259
- return new Promise((resolve) => {
260
- const stdout = new BoundedOutputBuffer(stdoutTailBytes);
261
- const stderr = new BoundedOutputBuffer(stderrTailBytes);
262
- let settled = false;
263
- let timedOut = false;
264
- let childError;
265
- let childPid;
266
- let timeout;
267
- const child = spawn(command ?? '', {
268
- cwd,
269
- env,
270
- shell: true,
271
- stdio: ['ignore', 'pipe', 'pipe'],
272
- windowsHide: true,
273
- detached: process.platform !== 'win32',
274
- });
275
- childPid = child.pid;
276
- const finish = (status, signal) => {
277
- if (settled) {
287
+ const stopForOutputLimit = (stream) => {
288
+ if (settled || childError) {
278
289
  return;
279
290
  }
280
- settled = true;
281
- if (timeout) {
282
- clearTimeout(timeout);
283
- }
284
- resolve({
285
- status,
286
- signal,
287
- error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
288
- stdout: stdout.toSnapshot(),
289
- stderr: stderr.toSnapshot(),
290
- pid: childPid,
291
- });
291
+ childError = createOutputLimitError(stream, maxOutputBytes);
292
+ child.stdout?.destroy();
293
+ child.stderr?.destroy();
294
+ child.unref();
295
+ terminateProcessTreeNonBlocking(childPid);
296
+ forceTerminateProcessTreeNonBlocking(childPid);
297
+ finish(null, null);
292
298
  };
293
299
  child.stdout?.on('data', (chunk) => {
294
300
  stdout.append(chunk);
295
- writeStreamChunk(reporter, 'stdout', chunk);
301
+ stdoutBytes += chunk.byteLength;
302
+ if (streamOutput) {
303
+ writeStreamChunk(reporter, 'stdout', chunk);
304
+ }
305
+ if (enforceOutputLimit && stdoutBytes > maxOutputBytes) {
306
+ stopForOutputLimit('stdout');
307
+ }
296
308
  });
297
309
  child.stderr?.on('data', (chunk) => {
298
310
  stderr.append(chunk);
299
- writeStreamChunk(reporter, 'stderr', chunk);
311
+ stderrBytes += chunk.byteLength;
312
+ if (streamOutput) {
313
+ writeStreamChunk(reporter, 'stderr', chunk);
314
+ }
315
+ if (enforceOutputLimit && stderrBytes > maxOutputBytes) {
316
+ stopForOutputLimit('stderr');
317
+ }
300
318
  });
301
319
  child.once('error', (error) => {
302
320
  childError = error;
@@ -306,20 +324,42 @@ function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailB
306
324
  });
307
325
  timeout = setTimeout(() => {
308
326
  timedOut = true;
309
- terminateProcessTree(childPid);
327
+ child.stdout?.destroy();
328
+ child.stderr?.destroy();
329
+ child.unref();
330
+ termination = createPendingTimeoutTermination(getKillMethod());
331
+ terminateProcessTreeNonBlocking(childPid);
332
+ forceTerminateProcessTreeNonBlocking(childPid);
333
+ finish(null, null);
310
334
  }, timeoutSeconds * 1000);
311
335
  });
312
336
  }
337
+ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
338
+ return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
339
+ }
340
+ function runShellCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
341
+ return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
342
+ }
313
343
  function getRunStatus(error, exitCode, successExitCodes) {
314
344
  const errorWithCode = error;
315
345
  if (errorWithCode?.code === 'ETIMEDOUT') {
316
346
  return 'timed_out';
317
347
  }
348
+ if (isOutputLimitExceededError(error)) {
349
+ return 'output_limit_exceeded';
350
+ }
318
351
  if (error) {
319
352
  return 'start_failed';
320
353
  }
321
354
  return exitCode !== null && successExitCodes.includes(exitCode) ? 'passed' : 'failed';
322
355
  }
356
+ function isOutputLimitExceededError(error) {
357
+ if (!error) {
358
+ return false;
359
+ }
360
+ const errorWithCode = error;
361
+ return errorWithCode.code === OUTPUT_LIMIT_ERROR_CODE || OUTPUT_LIMIT_ERROR_MESSAGE.test(error.message);
362
+ }
323
363
  function getRunPlanDetail(plan, lang, fallbackKey) {
324
364
  return plan.detail ?? t(lang, fallbackKey);
325
365
  }
@@ -356,12 +396,24 @@ function reportRunPlanFailure(plan, reporter, lang) {
356
396
  detail: getRunPlanDetail(plan, lang, 'run.error.blockedShellBackgroundDetail'),
357
397
  });
358
398
  break;
399
+ case 'blocked_long_running_command_pattern':
400
+ message = t(lang, 'run.error.blockedLongRunningCommand', {
401
+ intent: plan.intentName,
402
+ detail: getRunPlanDetail(plan, lang, 'run.error.blockedLongRunningCommandDetail'),
403
+ });
404
+ break;
359
405
  case 'cwd_outside_project':
360
406
  message = t(lang, 'run.error.cwdOutsideProject', {
361
407
  intent: plan.intentName,
362
408
  detail: getRunPlanDetail(plan, lang, 'run.error.cwdOutsideProjectDetail'),
363
409
  });
364
410
  break;
411
+ case 'max_output_bytes_exceeds_limit':
412
+ message = t(lang, 'run.error.maxOutputBytes', {
413
+ intent: plan.intentName,
414
+ detail: getRunPlanDetail(plan, lang, 'run.error.maxOutputBytesDetail'),
415
+ });
416
+ break;
365
417
  case 'intent_not_table':
366
418
  default:
367
419
  message = t(lang, 'run.error.unknownIntent', { intent: plan.intentName });
@@ -481,17 +533,11 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
481
533
  }
482
534
  }
483
535
  if (plan.commandArgv) {
484
- if (!json) {
485
- streamedOutput = true;
486
- return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
487
- }
488
- return runArgvCommand(plan.argvCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
489
- }
490
- if (!json) {
491
- streamedOutput = true;
492
- return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
536
+ streamedOutput = !json;
537
+ return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
493
538
  }
494
- return runShellCommand(plan.shellCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
539
+ streamedOutput = !json;
540
+ return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
495
541
  });
496
542
  const childDurationMs = performance.now() - childStartedAtMs;
497
543
  const finishedAt = new Date();
@@ -499,9 +545,13 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
499
545
  const exitCode = typeof result.status === 'number' ? result.status : null;
500
546
  const runStatus = getRunStatus(result.error, exitCode, plan.successExitCodes);
501
547
  let killMethod = null;
548
+ let termination = null;
502
549
  if (runStatus === 'timed_out') {
503
- killMethod = getKillMethod();
504
- terminateProcessTree(result.pid);
550
+ termination = result.termination ?? createPendingTimeoutTermination(getKillMethod());
551
+ killMethod = termination.method;
552
+ if (!result.termination && result.pid) {
553
+ terminateProcessTree(result.pid);
554
+ }
505
555
  }
506
556
  const receipt = profiler.measure('receipt_create', () => createRunReceipt({
507
557
  intent: intentName,
@@ -525,6 +575,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
525
575
  signal: result.signal,
526
576
  error: result.error?.message ?? null,
527
577
  killMethod,
578
+ termination,
528
579
  stdout: result.stdout,
529
580
  stderr: result.stderr,
530
581
  writeDrift,
@@ -539,6 +590,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
539
590
  },
540
591
  stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
541
592
  stderrTailBytes: runReceiptPolicy.stderrTailBytes,
593
+ receiptPath: createRunReceiptRelativePath(),
542
594
  }));
543
595
  if (options.writeLatestReceipt !== false) {
544
596
  profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));