mustflow 2.18.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.
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`.
@@ -358,7 +358,7 @@ Development servers, watch modes, browser UIs, interactive commands, and backgro
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
 
@@ -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);
@@ -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;
@@ -103,6 +105,17 @@ function forceTerminateProcessTreeNonBlocking(pid) {
103
105
  function getKillMethod() {
104
106
  return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
105
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
+ }
106
119
  function createBufferedReporter() {
107
120
  const stdout = [];
108
121
  const stderr = [];
@@ -212,19 +225,6 @@ async function runBuiltinArgvInProcess(commandArgv, cwd, lang) {
212
225
  };
213
226
  }
214
227
  }
215
- function runArgvCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
216
- return spawnSync(command?.executable ?? '', command?.args ?? [], {
217
- cwd,
218
- encoding: 'utf8',
219
- input: '',
220
- maxBuffer: maxOutputBytes,
221
- env,
222
- shell: command?.shell ?? false,
223
- stdio: ['ignore', 'pipe', 'pipe'],
224
- timeout: timeoutSeconds * 1000,
225
- windowsHide: true,
226
- });
227
- }
228
228
  function writeStreamChunk(reporter, stream, chunk) {
229
229
  if (stream === 'stdout') {
230
230
  if (reporter.writeStdout) {
@@ -240,7 +240,12 @@ function writeStreamChunk(reporter, stream, chunk) {
240
240
  }
241
241
  reporter.stderr(chunk.toString());
242
242
  }
243
- function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
243
+ function createOutputLimitError(stream, maxOutputBytes) {
244
+ return Object.assign(new Error(`${stream} exceeded max_output_bytes (${maxOutputBytes})`), {
245
+ code: OUTPUT_LIMIT_ERROR_CODE,
246
+ });
247
+ }
248
+ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
244
249
  return new Promise((resolve) => {
245
250
  const stdout = new BoundedOutputBuffer(stdoutTailBytes);
246
251
  const stderr = new BoundedOutputBuffer(stderrTailBytes);
@@ -248,11 +253,14 @@ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBy
248
253
  let timedOut = false;
249
254
  let childError;
250
255
  let childPid;
256
+ let stdoutBytes = 0;
257
+ let stderrBytes = 0;
251
258
  let timeout;
252
- const child = spawn(command?.executable ?? '', command?.args ?? [], {
259
+ let termination = null;
260
+ const child = spawn(command.executable, command.args ?? [], {
253
261
  cwd,
254
262
  env,
255
- shell: command?.shell ?? false,
263
+ shell: command.shell,
256
264
  stdio: ['ignore', 'pipe', 'pipe'],
257
265
  windowsHide: true,
258
266
  detached: process.platform !== 'win32',
@@ -273,88 +281,40 @@ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBy
273
281
  stdout: stdout.toSnapshot(),
274
282
  stderr: stderr.toSnapshot(),
275
283
  pid: childPid,
284
+ termination,
276
285
  });
277
286
  };
278
- child.stdout?.on('data', (chunk) => {
279
- stdout.append(chunk);
280
- writeStreamChunk(reporter, 'stdout', chunk);
281
- });
282
- child.stderr?.on('data', (chunk) => {
283
- stderr.append(chunk);
284
- writeStreamChunk(reporter, 'stderr', chunk);
285
- });
286
- child.once('error', (error) => {
287
- childError = error;
288
- });
289
- child.once('close', (status, signal) => {
290
- finish(status, signal);
291
- });
292
- timeout = setTimeout(() => {
293
- timedOut = true;
287
+ const stopForOutputLimit = (stream) => {
288
+ if (settled || childError) {
289
+ return;
290
+ }
291
+ childError = createOutputLimitError(stream, maxOutputBytes);
294
292
  child.stdout?.destroy();
295
293
  child.stderr?.destroy();
296
294
  child.unref();
297
295
  terminateProcessTreeNonBlocking(childPid);
298
296
  forceTerminateProcessTreeNonBlocking(childPid);
299
297
  finish(null, null);
300
- }, timeoutSeconds * 1000);
301
- });
302
- }
303
- function runShellCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
304
- return spawnSync(command ?? '', {
305
- cwd,
306
- encoding: 'utf8',
307
- input: '',
308
- maxBuffer: maxOutputBytes,
309
- env,
310
- shell: true,
311
- stdio: ['ignore', 'pipe', 'pipe'],
312
- timeout: timeoutSeconds * 1000,
313
- windowsHide: true,
314
- });
315
- }
316
- function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
317
- return new Promise((resolve) => {
318
- const stdout = new BoundedOutputBuffer(stdoutTailBytes);
319
- const stderr = new BoundedOutputBuffer(stderrTailBytes);
320
- let settled = false;
321
- let timedOut = false;
322
- let childError;
323
- let childPid;
324
- let timeout;
325
- const child = spawn(command ?? '', {
326
- cwd,
327
- env,
328
- shell: true,
329
- stdio: ['ignore', 'pipe', 'pipe'],
330
- windowsHide: true,
331
- detached: process.platform !== 'win32',
332
- });
333
- childPid = child.pid;
334
- const finish = (status, signal) => {
335
- if (settled) {
336
- return;
337
- }
338
- settled = true;
339
- if (timeout) {
340
- clearTimeout(timeout);
341
- }
342
- resolve({
343
- status: timedOut ? null : status,
344
- signal: timedOut ? null : signal,
345
- error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
346
- stdout: stdout.toSnapshot(),
347
- stderr: stderr.toSnapshot(),
348
- pid: childPid,
349
- });
350
298
  };
351
299
  child.stdout?.on('data', (chunk) => {
352
300
  stdout.append(chunk);
353
- 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
+ }
354
308
  });
355
309
  child.stderr?.on('data', (chunk) => {
356
310
  stderr.append(chunk);
357
- 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
+ }
358
318
  });
359
319
  child.once('error', (error) => {
360
320
  childError = error;
@@ -367,22 +327,39 @@ function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailB
367
327
  child.stdout?.destroy();
368
328
  child.stderr?.destroy();
369
329
  child.unref();
330
+ termination = createPendingTimeoutTermination(getKillMethod());
370
331
  terminateProcessTreeNonBlocking(childPid);
371
332
  forceTerminateProcessTreeNonBlocking(childPid);
372
333
  finish(null, null);
373
334
  }, timeoutSeconds * 1000);
374
335
  });
375
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
+ }
376
343
  function getRunStatus(error, exitCode, successExitCodes) {
377
344
  const errorWithCode = error;
378
345
  if (errorWithCode?.code === 'ETIMEDOUT') {
379
346
  return 'timed_out';
380
347
  }
348
+ if (isOutputLimitExceededError(error)) {
349
+ return 'output_limit_exceeded';
350
+ }
381
351
  if (error) {
382
352
  return 'start_failed';
383
353
  }
384
354
  return exitCode !== null && successExitCodes.includes(exitCode) ? 'passed' : 'failed';
385
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
+ }
386
363
  function getRunPlanDetail(plan, lang, fallbackKey) {
387
364
  return plan.detail ?? t(lang, fallbackKey);
388
365
  }
@@ -556,17 +533,11 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
556
533
  }
557
534
  }
558
535
  if (plan.commandArgv) {
559
- if (!json) {
560
- streamedOutput = true;
561
- return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
562
- }
563
- return runArgvCommand(plan.argvCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
536
+ streamedOutput = !json;
537
+ return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
564
538
  }
565
- if (!json) {
566
- streamedOutput = true;
567
- return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
568
- }
569
- 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);
570
541
  });
571
542
  const childDurationMs = performance.now() - childStartedAtMs;
572
543
  const finishedAt = new Date();
@@ -574,9 +545,13 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
574
545
  const exitCode = typeof result.status === 'number' ? result.status : null;
575
546
  const runStatus = getRunStatus(result.error, exitCode, plan.successExitCodes);
576
547
  let killMethod = null;
548
+ let termination = null;
577
549
  if (runStatus === 'timed_out') {
578
- killMethod = getKillMethod();
579
- terminateProcessTree(result.pid);
550
+ termination = result.termination ?? createPendingTimeoutTermination(getKillMethod());
551
+ killMethod = termination.method;
552
+ if (!result.termination && result.pid) {
553
+ terminateProcessTree(result.pid);
554
+ }
580
555
  }
581
556
  const receipt = profiler.measure('receipt_create', () => createRunReceipt({
582
557
  intent: intentName,
@@ -600,6 +575,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
600
575
  signal: result.signal,
601
576
  error: result.error?.message ?? null,
602
577
  killMethod,
578
+ termination,
603
579
  stdout: result.stdout,
604
580
  stderr: result.stderr,
605
581
  writeDrift,
@@ -614,6 +590,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
614
590
  },
615
591
  stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
616
592
  stderrTailBytes: runReceiptPolicy.stderrTailBytes,
593
+ receiptPath: createRunReceiptRelativePath(),
617
594
  }));
618
595
  if (options.writeLatestReceipt !== false) {
619
596
  profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));