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 +2 -2
- package/dist/cli/commands/explain-verify.js +2 -2
- package/dist/cli/commands/run.js +74 -97
- package/dist/cli/commands/verify.js +148 -44
- package/dist/cli/i18n/en.js +3 -1
- package/dist/cli/i18n/es.js +3 -1
- package/dist/cli/i18n/fr.js +3 -1
- package/dist/cli/i18n/hi.js +3 -1
- package/dist/cli/i18n/ko.js +3 -1
- package/dist/cli/i18n/zh.js +3 -1
- package/dist/cli/lib/repo-map.js +10 -3
- package/dist/core/atomic-state-write.js +31 -0
- package/dist/core/bounded-output.js +23 -1
- package/dist/core/check-issues.js +1 -0
- package/dist/core/command-contract-validation.js +57 -7
- package/dist/core/completion-verdict.js +2 -1
- package/dist/core/public-json-contracts.js +1 -1
- package/dist/core/run-receipt.js +20 -13
- package/dist/core/source-anchors.js +96 -24
- package/dist/core/verification-evidence.js +4 -1
- package/package.json +1 -1
- package/schemas/README.md +1 -1
- package/schemas/run-receipt.schema.json +26 -3
- package/schemas/verify-report.schema.json +1 -1
- package/schemas/verify-run-manifest.schema.json +1 -1
- 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
|
|
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
|
|
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,
|
|
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
|
|
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);
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
259
|
+
let termination = null;
|
|
260
|
+
const child = spawn(command.executable, command.args ?? [], {
|
|
253
261
|
cwd,
|
|
254
262
|
env,
|
|
255
|
-
shell: command
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
560
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
579
|
-
|
|
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));
|