mustflow 2.18.0 → 2.18.3

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 (38) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/explain-verify.js +2 -2
  3. package/dist/cli/commands/run/builtin-dispatch.js +92 -0
  4. package/dist/cli/commands/run/executor.js +112 -0
  5. package/dist/cli/commands/run/output.js +59 -0
  6. package/dist/cli/commands/run/process-tree.js +91 -0
  7. package/dist/cli/commands/run/receipt.js +42 -0
  8. package/dist/cli/commands/run.js +22 -414
  9. package/dist/cli/commands/verify/args.js +262 -0
  10. package/dist/cli/commands/verify.js +106 -263
  11. package/dist/cli/i18n/en.js +3 -1
  12. package/dist/cli/i18n/es.js +3 -1
  13. package/dist/cli/i18n/fr.js +3 -1
  14. package/dist/cli/i18n/hi.js +3 -1
  15. package/dist/cli/i18n/ko.js +3 -1
  16. package/dist/cli/i18n/zh.js +3 -1
  17. package/dist/cli/index.js +6 -72
  18. package/dist/cli/lib/command-registry.js +27 -0
  19. package/dist/cli/lib/repo-map.js +10 -3
  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 +1 -0
  23. package/dist/core/command-contract-validation.js +57 -7
  24. package/dist/core/completion-verdict.js +2 -1
  25. package/dist/core/public-json-contracts.js +1 -1
  26. package/dist/core/run-receipt.js +20 -13
  27. package/dist/core/source-anchors.js +96 -24
  28. package/dist/core/verification-evidence.js +4 -1
  29. package/package.json +1 -1
  30. package/schemas/README.md +1 -1
  31. package/schemas/run-receipt.schema.json +26 -3
  32. package/schemas/verify-report.schema.json +1 -1
  33. package/schemas/verify-run-manifest.schema.json +1 -1
  34. package/templates/default/i18n.toml +7 -1
  35. package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
  36. package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
  37. package/templates/default/locales/en/.mustflow/skills/source-anchor-authoring/SKILL.md +147 -0
  38. package/templates/default/manifest.toml +8 -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`.
@@ -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);
@@ -0,0 +1,92 @@
1
+ import { canRunMustflowBuiltinInProcess, isMustflowBinName } from '../../../core/command-classification.js';
2
+ import { getPackageVersion } from '../../lib/package-info.js';
3
+ import { createBufferedReporter } from './output.js';
4
+ /**
5
+ * mf:anchor cli.run.builtin-inprocess
6
+ * purpose: Dispatch selected mustflow built-in commands without spawning a nested CLI process.
7
+ * search: builtin intent, in-process command, nested mf run, run receipt
8
+ * invariant: Only commands classified by command-classification can use this path.
9
+ * risk: config, state
10
+ */
11
+ async function runKnownBuiltinCommand(args, reporter, lang) {
12
+ const [command, ...commandArgs] = args;
13
+ if ((command === '--version' || command === '-v' || command === 'version') && commandArgs.length === 0) {
14
+ reporter.stdout(getPackageVersion());
15
+ return 0;
16
+ }
17
+ if (!canRunMustflowBuiltinInProcess(command)) {
18
+ return undefined;
19
+ }
20
+ if (command === 'check') {
21
+ return (await import('../check.js')).runCheck(commandArgs, reporter, lang);
22
+ }
23
+ if (command === 'classify') {
24
+ return (await import('../classify.js')).runClassify(commandArgs, reporter, lang);
25
+ }
26
+ if (command === 'context') {
27
+ return (await import('../context.js')).runContext(commandArgs, reporter, lang);
28
+ }
29
+ if (command === 'doctor') {
30
+ return (await import('../doctor.js')).runDoctor(commandArgs, reporter, lang);
31
+ }
32
+ if (command === 'help') {
33
+ return (await import('../help.js')).runHelp(commandArgs, reporter, lang);
34
+ }
35
+ if (command === 'impact') {
36
+ return (await import('../impact.js')).runImpact(commandArgs, reporter, lang);
37
+ }
38
+ if (command === 'line-endings') {
39
+ return (await import('../line-endings.js')).runLineEndings(commandArgs, reporter, lang);
40
+ }
41
+ if (command === 'map') {
42
+ return (await import('../map.js')).runMap(commandArgs, reporter, lang);
43
+ }
44
+ if (command === 'status') {
45
+ return (await import('../status.js')).runStatus(commandArgs, reporter, lang);
46
+ }
47
+ if (command === 'update') {
48
+ return (await import('../update.js')).runUpdate(commandArgs, reporter, lang);
49
+ }
50
+ if (command === 'version-sources') {
51
+ return (await import('../version-sources.js')).runVersionSources(commandArgs, reporter, lang);
52
+ }
53
+ return undefined;
54
+ }
55
+ async function withWorkingDirectory(cwd, callback) {
56
+ const previousCwd = process.cwd();
57
+ process.chdir(cwd);
58
+ try {
59
+ return await callback();
60
+ }
61
+ finally {
62
+ process.chdir(previousCwd);
63
+ }
64
+ }
65
+ export async function runBuiltinArgvInProcess(commandArgv, cwd, lang) {
66
+ const [command = '', ...builtinArgs] = commandArgv;
67
+ if (!isMustflowBinName(command)) {
68
+ return undefined;
69
+ }
70
+ const output = createBufferedReporter();
71
+ try {
72
+ const status = await withWorkingDirectory(cwd, () => runKnownBuiltinCommand(builtinArgs, output.reporter, lang));
73
+ if (status === undefined) {
74
+ return undefined;
75
+ }
76
+ return {
77
+ status,
78
+ signal: null,
79
+ stdout: output.stdout(),
80
+ stderr: output.stderr(),
81
+ };
82
+ }
83
+ catch (error) {
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ return {
86
+ status: 1,
87
+ signal: null,
88
+ stdout: output.stdout(),
89
+ stderr: `${output.stderr()}${message}\n`,
90
+ };
91
+ }
92
+ }
@@ -0,0 +1,112 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { BoundedOutputBuffer } from '../../../core/bounded-output.js';
3
+ import { createPendingTimeoutTermination, forceTerminateProcessTreeNonBlocking, getKillMethod, terminateProcessTreeNonBlocking, } from './process-tree.js';
4
+ import { createOutputLimitError, isOutputLimitExceededError, writeStreamChunk } from './output.js';
5
+ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
6
+ return new Promise((resolve) => {
7
+ const stdout = new BoundedOutputBuffer(stdoutTailBytes);
8
+ const stderr = new BoundedOutputBuffer(stderrTailBytes);
9
+ let settled = false;
10
+ let timedOut = false;
11
+ let childError;
12
+ let childPid;
13
+ let stdoutBytes = 0;
14
+ let stderrBytes = 0;
15
+ let timeout;
16
+ let termination = null;
17
+ const child = spawn(command.executable, command.args ?? [], {
18
+ cwd,
19
+ env,
20
+ shell: command.shell,
21
+ stdio: ['ignore', 'pipe', 'pipe'],
22
+ windowsHide: true,
23
+ detached: process.platform !== 'win32',
24
+ });
25
+ childPid = child.pid;
26
+ const finish = (status, signal) => {
27
+ if (settled) {
28
+ return;
29
+ }
30
+ settled = true;
31
+ if (timeout) {
32
+ clearTimeout(timeout);
33
+ }
34
+ resolve({
35
+ status: timedOut ? null : status,
36
+ signal: timedOut ? null : signal,
37
+ error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
38
+ stdout: stdout.toSnapshot(),
39
+ stderr: stderr.toSnapshot(),
40
+ pid: childPid,
41
+ termination,
42
+ });
43
+ };
44
+ const stopForOutputLimit = (stream) => {
45
+ if (settled || childError) {
46
+ return;
47
+ }
48
+ childError = createOutputLimitError(stream, maxOutputBytes);
49
+ child.stdout?.destroy();
50
+ child.stderr?.destroy();
51
+ child.unref();
52
+ terminateProcessTreeNonBlocking(childPid);
53
+ forceTerminateProcessTreeNonBlocking(childPid);
54
+ finish(null, null);
55
+ };
56
+ child.stdout?.on('data', (chunk) => {
57
+ stdout.append(chunk);
58
+ stdoutBytes += chunk.byteLength;
59
+ if (streamOutput) {
60
+ writeStreamChunk(reporter, 'stdout', chunk);
61
+ }
62
+ if (enforceOutputLimit && stdoutBytes > maxOutputBytes) {
63
+ stopForOutputLimit('stdout');
64
+ }
65
+ });
66
+ child.stderr?.on('data', (chunk) => {
67
+ stderr.append(chunk);
68
+ stderrBytes += chunk.byteLength;
69
+ if (streamOutput) {
70
+ writeStreamChunk(reporter, 'stderr', chunk);
71
+ }
72
+ if (enforceOutputLimit && stderrBytes > maxOutputBytes) {
73
+ stopForOutputLimit('stderr');
74
+ }
75
+ });
76
+ child.once('error', (error) => {
77
+ childError = error;
78
+ });
79
+ child.once('close', (status, signal) => {
80
+ finish(status, signal);
81
+ });
82
+ timeout = setTimeout(() => {
83
+ timedOut = true;
84
+ child.stdout?.destroy();
85
+ child.stderr?.destroy();
86
+ child.unref();
87
+ termination = createPendingTimeoutTermination(getKillMethod());
88
+ terminateProcessTreeNonBlocking(childPid);
89
+ forceTerminateProcessTreeNonBlocking(childPid);
90
+ finish(null, null);
91
+ }, timeoutSeconds * 1000);
92
+ });
93
+ }
94
+ export function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
95
+ return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
96
+ }
97
+ export function runShellCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
98
+ return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
99
+ }
100
+ export function getRunStatus(error, exitCode, successExitCodes) {
101
+ const errorWithCode = error;
102
+ if (errorWithCode?.code === 'ETIMEDOUT') {
103
+ return 'timed_out';
104
+ }
105
+ if (isOutputLimitExceededError(error)) {
106
+ return 'output_limit_exceeded';
107
+ }
108
+ if (error) {
109
+ return 'start_failed';
110
+ }
111
+ return exitCode !== null && successExitCodes.includes(exitCode) ? 'passed' : 'failed';
112
+ }
@@ -0,0 +1,59 @@
1
+ const OUTPUT_LIMIT_ERROR_CODE = 'ENOBUFS';
2
+ const OUTPUT_LIMIT_ERROR_MESSAGE = /\bmaxBuffer\b.*\bexceeded\b/i;
3
+ export function emitOutput(reporter, output, stream) {
4
+ if (!output) {
5
+ return;
6
+ }
7
+ const text = (typeof output === 'object' && 'tail' in output ? output.tail : output.toString()).trimEnd();
8
+ if (text.length === 0) {
9
+ return;
10
+ }
11
+ reporter[stream](text);
12
+ }
13
+ export function createBufferedReporter() {
14
+ const stdout = [];
15
+ const stderr = [];
16
+ return {
17
+ reporter: {
18
+ stdout(message) {
19
+ stdout.push(`${message}\n`);
20
+ },
21
+ stderr(message) {
22
+ stderr.push(`${message}\n`);
23
+ },
24
+ },
25
+ stdout() {
26
+ return stdout.join('');
27
+ },
28
+ stderr() {
29
+ return stderr.join('');
30
+ },
31
+ };
32
+ }
33
+ export function writeStreamChunk(reporter, stream, chunk) {
34
+ if (stream === 'stdout') {
35
+ if (reporter.writeStdout) {
36
+ reporter.writeStdout(chunk);
37
+ return;
38
+ }
39
+ reporter.stdout(chunk.toString());
40
+ return;
41
+ }
42
+ if (reporter.writeStderr) {
43
+ reporter.writeStderr(chunk);
44
+ return;
45
+ }
46
+ reporter.stderr(chunk.toString());
47
+ }
48
+ export function createOutputLimitError(stream, maxOutputBytes) {
49
+ return Object.assign(new Error(`${stream} exceeded max_output_bytes (${maxOutputBytes})`), {
50
+ code: OUTPUT_LIMIT_ERROR_CODE,
51
+ });
52
+ }
53
+ export function isOutputLimitExceededError(error) {
54
+ if (!error) {
55
+ return false;
56
+ }
57
+ const errorWithCode = error;
58
+ return errorWithCode.code === OUTPUT_LIMIT_ERROR_CODE || OUTPUT_LIMIT_ERROR_MESSAGE.test(error.message);
59
+ }
@@ -0,0 +1,91 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ function signalProcessTree(pid, signal) {
3
+ if (!pid || pid <= 0) {
4
+ return;
5
+ }
6
+ if (process.platform === 'win32') {
7
+ spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
8
+ stdio: 'ignore',
9
+ windowsHide: true,
10
+ });
11
+ if (signal === 'SIGKILL') {
12
+ try {
13
+ process.kill(pid, signal);
14
+ }
15
+ catch {
16
+ // taskkill may already have terminated the direct child.
17
+ }
18
+ }
19
+ return;
20
+ }
21
+ try {
22
+ process.kill(-pid, signal);
23
+ }
24
+ catch {
25
+ try {
26
+ process.kill(pid, signal);
27
+ }
28
+ catch {
29
+ // The child may already be gone after Node's spawn timeout handling.
30
+ }
31
+ }
32
+ }
33
+ function signalProcessTreeNonBlocking(pid, signal) {
34
+ if (!pid || pid <= 0) {
35
+ return;
36
+ }
37
+ if (process.platform === 'win32') {
38
+ const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
39
+ stdio: 'ignore',
40
+ windowsHide: true,
41
+ detached: true,
42
+ });
43
+ killer.unref();
44
+ if (signal === 'SIGKILL') {
45
+ try {
46
+ process.kill(pid, signal);
47
+ }
48
+ catch {
49
+ // taskkill may already have terminated the direct child.
50
+ }
51
+ }
52
+ return;
53
+ }
54
+ try {
55
+ process.kill(-pid, signal);
56
+ }
57
+ catch {
58
+ try {
59
+ process.kill(pid, signal);
60
+ }
61
+ catch {
62
+ // The child may already be gone after the timeout fired.
63
+ }
64
+ }
65
+ }
66
+ export function terminateProcessTree(pid) {
67
+ signalProcessTree(pid, 'SIGTERM');
68
+ }
69
+ export function forceTerminateProcessTree(pid) {
70
+ signalProcessTree(pid, 'SIGKILL');
71
+ }
72
+ export function terminateProcessTreeNonBlocking(pid) {
73
+ signalProcessTreeNonBlocking(pid, 'SIGTERM');
74
+ }
75
+ export function forceTerminateProcessTreeNonBlocking(pid) {
76
+ signalProcessTreeNonBlocking(pid, 'SIGKILL');
77
+ }
78
+ export function getKillMethod() {
79
+ return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
80
+ }
81
+ export function createPendingTimeoutTermination(method) {
82
+ return {
83
+ reason: 'timeout',
84
+ method,
85
+ graceful_signal: 'SIGTERM',
86
+ forced_signal: 'SIGKILL',
87
+ forced_kill_attempted: true,
88
+ confirmed: false,
89
+ cleanup_pending: true,
90
+ };
91
+ }
@@ -0,0 +1,42 @@
1
+ import { createRunReceipt, createRunReceiptRelativePath, } from '../../../core/run-receipt.js';
2
+ export function assembleRunReceipt(input) {
3
+ return createRunReceipt({
4
+ intent: input.intentName,
5
+ status: input.runStatus,
6
+ timedOut: input.runStatus === 'timed_out',
7
+ startedAt: input.startedAt,
8
+ finishedAt: input.finishedAt,
9
+ projectRoot: input.projectRoot,
10
+ cwd: input.plan.cwd,
11
+ lifecycle: input.plan.lifecycle ?? 'oneshot',
12
+ runPolicy: input.plan.runPolicy ?? 'agent_allowed',
13
+ mode: input.plan.mode,
14
+ argv: input.plan.commandArgv,
15
+ cmd: input.plan.shellCommand,
16
+ envPolicy: input.plan.envPolicy,
17
+ envAllowlist: input.plan.envAllowlist,
18
+ timeoutSeconds: input.plan.timeoutSeconds,
19
+ maxOutputBytes: input.plan.maxOutputBytes,
20
+ successExitCodes: input.plan.successExitCodes,
21
+ exitCode: input.exitCode,
22
+ signal: input.result.signal,
23
+ error: input.result.error?.message ?? null,
24
+ killMethod: input.killMethod,
25
+ termination: input.termination,
26
+ stdout: input.result.stdout,
27
+ stderr: input.result.stderr,
28
+ writeDrift: input.writeDrift,
29
+ executorOverheadMs: input.executorOverheadMs,
30
+ phaseTimings: input.phaseTimings,
31
+ selectionSummary: {
32
+ strategy: input.plan.testTargets.length > 0 ? 'project_test_selection' : 'direct_intent',
33
+ changed_file_count: 0,
34
+ changed_surface_counts: {},
35
+ selected_target_count: Math.max(1, input.plan.testTargets.length),
36
+ fallback_used: false,
37
+ },
38
+ stdoutTailBytes: input.stdoutTailBytes,
39
+ stderrTailBytes: input.stderrTailBytes,
40
+ receiptPath: createRunReceiptRelativePath(),
41
+ });
42
+ }