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
@@ -1,388 +1,20 @@
1
- import { spawn, spawnSync } from 'node:child_process';
2
1
  import { performance } from 'node:perf_hooks';
3
- import { canRunMustflowBuiltinInProcess, isMustflowBinName } from '../../core/command-classification.js';
4
2
  import { createCommandEnv } from '../../core/command-env.js';
5
- import { BoundedOutputBuffer } from '../../core/bounded-output.js';
6
3
  import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
7
4
  import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
8
5
  import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
9
6
  import { t } from '../lib/i18n.js';
10
- import { getPackageVersion } from '../lib/package-info.js';
11
7
  import { resolveMustflowRoot } from '../lib/project-root.js';
12
8
  import { createRunPlan, createRunPreview, isMustflowBuiltinIntent, renderRunPreviewText, } from '../lib/run-plan.js';
13
- import { createRunReceipt, writeRunReceipt } from '../../core/run-receipt.js';
9
+ import { writeRunReceipt, } from '../../core/run-receipt.js';
14
10
  import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
15
11
  import { RunProfiler } from '../../core/run-profile.js';
16
12
  import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
17
- function emitOutput(reporter, output, stream) {
18
- if (!output) {
19
- return;
20
- }
21
- const text = (typeof output === 'object' && 'tail' in output ? output.tail : output.toString()).trimEnd();
22
- if (text.length === 0) {
23
- return;
24
- }
25
- reporter[stream](text);
26
- }
27
- function signalProcessTree(pid, signal) {
28
- if (!pid || pid <= 0) {
29
- return;
30
- }
31
- if (process.platform === 'win32') {
32
- spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
33
- stdio: 'ignore',
34
- windowsHide: true,
35
- });
36
- if (signal === 'SIGKILL') {
37
- try {
38
- process.kill(pid, signal);
39
- }
40
- catch {
41
- // taskkill may already have terminated the direct child.
42
- }
43
- }
44
- return;
45
- }
46
- try {
47
- process.kill(-pid, signal);
48
- }
49
- catch {
50
- try {
51
- process.kill(pid, signal);
52
- }
53
- catch {
54
- // The child may already be gone after Node's spawn timeout handling.
55
- }
56
- }
57
- }
58
- function signalProcessTreeNonBlocking(pid, signal) {
59
- if (!pid || pid <= 0) {
60
- return;
61
- }
62
- if (process.platform === 'win32') {
63
- const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
64
- stdio: 'ignore',
65
- windowsHide: true,
66
- detached: true,
67
- });
68
- killer.unref();
69
- if (signal === 'SIGKILL') {
70
- try {
71
- process.kill(pid, signal);
72
- }
73
- catch {
74
- // taskkill may already have terminated the direct child.
75
- }
76
- }
77
- return;
78
- }
79
- try {
80
- process.kill(-pid, signal);
81
- }
82
- catch {
83
- try {
84
- process.kill(pid, signal);
85
- }
86
- catch {
87
- // The child may already be gone after the timeout fired.
88
- }
89
- }
90
- }
91
- function terminateProcessTree(pid) {
92
- signalProcessTree(pid, 'SIGTERM');
93
- }
94
- function forceTerminateProcessTree(pid) {
95
- signalProcessTree(pid, 'SIGKILL');
96
- }
97
- function terminateProcessTreeNonBlocking(pid) {
98
- signalProcessTreeNonBlocking(pid, 'SIGTERM');
99
- }
100
- function forceTerminateProcessTreeNonBlocking(pid) {
101
- signalProcessTreeNonBlocking(pid, 'SIGKILL');
102
- }
103
- function getKillMethod() {
104
- return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
105
- }
106
- function createBufferedReporter() {
107
- const stdout = [];
108
- const stderr = [];
109
- return {
110
- reporter: {
111
- stdout(message) {
112
- stdout.push(`${message}\n`);
113
- },
114
- stderr(message) {
115
- stderr.push(`${message}\n`);
116
- },
117
- },
118
- stdout() {
119
- return stdout.join('');
120
- },
121
- stderr() {
122
- return stderr.join('');
123
- },
124
- };
125
- }
126
- /**
127
- * mf:anchor cli.run.builtin-inprocess
128
- * purpose: Dispatch selected mustflow built-in commands without spawning a nested CLI process.
129
- * search: builtin intent, in-process command, nested mf run, run receipt
130
- * invariant: Only commands classified by command-classification can use this path.
131
- * risk: config, state
132
- */
133
- async function runKnownBuiltinCommand(args, reporter, lang) {
134
- const [command, ...commandArgs] = args;
135
- if ((command === '--version' || command === '-v' || command === 'version') && commandArgs.length === 0) {
136
- reporter.stdout(getPackageVersion());
137
- return 0;
138
- }
139
- if (!canRunMustflowBuiltinInProcess(command)) {
140
- return undefined;
141
- }
142
- if (command === 'check') {
143
- return (await import('./check.js')).runCheck(commandArgs, reporter, lang);
144
- }
145
- if (command === 'classify') {
146
- return (await import('./classify.js')).runClassify(commandArgs, reporter, lang);
147
- }
148
- if (command === 'context') {
149
- return (await import('./context.js')).runContext(commandArgs, reporter, lang);
150
- }
151
- if (command === 'doctor') {
152
- return (await import('./doctor.js')).runDoctor(commandArgs, reporter, lang);
153
- }
154
- if (command === 'help') {
155
- return (await import('./help.js')).runHelp(commandArgs, reporter, lang);
156
- }
157
- if (command === 'impact') {
158
- return (await import('./impact.js')).runImpact(commandArgs, reporter, lang);
159
- }
160
- if (command === 'line-endings') {
161
- return (await import('./line-endings.js')).runLineEndings(commandArgs, reporter, lang);
162
- }
163
- if (command === 'map') {
164
- return (await import('./map.js')).runMap(commandArgs, reporter, lang);
165
- }
166
- if (command === 'status') {
167
- return (await import('./status.js')).runStatus(commandArgs, reporter, lang);
168
- }
169
- if (command === 'update') {
170
- return (await import('./update.js')).runUpdate(commandArgs, reporter, lang);
171
- }
172
- if (command === 'version-sources') {
173
- return (await import('./version-sources.js')).runVersionSources(commandArgs, reporter, lang);
174
- }
175
- return undefined;
176
- }
177
- async function withWorkingDirectory(cwd, callback) {
178
- const previousCwd = process.cwd();
179
- process.chdir(cwd);
180
- try {
181
- return await callback();
182
- }
183
- finally {
184
- process.chdir(previousCwd);
185
- }
186
- }
187
- async function runBuiltinArgvInProcess(commandArgv, cwd, lang) {
188
- const [command = '', ...builtinArgs] = commandArgv;
189
- if (!isMustflowBinName(command)) {
190
- return undefined;
191
- }
192
- const output = createBufferedReporter();
193
- try {
194
- const status = await withWorkingDirectory(cwd, () => runKnownBuiltinCommand(builtinArgs, output.reporter, lang));
195
- if (status === undefined) {
196
- return undefined;
197
- }
198
- return {
199
- status,
200
- signal: null,
201
- stdout: output.stdout(),
202
- stderr: output.stderr(),
203
- };
204
- }
205
- catch (error) {
206
- const message = error instanceof Error ? error.message : String(error);
207
- return {
208
- status: 1,
209
- signal: null,
210
- stdout: output.stdout(),
211
- stderr: `${output.stderr()}${message}\n`,
212
- };
213
- }
214
- }
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
- function writeStreamChunk(reporter, stream, chunk) {
229
- if (stream === 'stdout') {
230
- if (reporter.writeStdout) {
231
- reporter.writeStdout(chunk);
232
- return;
233
- }
234
- reporter.stdout(chunk.toString());
235
- return;
236
- }
237
- if (reporter.writeStderr) {
238
- reporter.writeStderr(chunk);
239
- return;
240
- }
241
- reporter.stderr(chunk.toString());
242
- }
243
- function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
244
- return new Promise((resolve) => {
245
- const stdout = new BoundedOutputBuffer(stdoutTailBytes);
246
- const stderr = new BoundedOutputBuffer(stderrTailBytes);
247
- let settled = false;
248
- let timedOut = false;
249
- let childError;
250
- let childPid;
251
- let timeout;
252
- const child = spawn(command?.executable ?? '', command?.args ?? [], {
253
- cwd,
254
- env,
255
- shell: command?.shell ?? false,
256
- stdio: ['ignore', 'pipe', 'pipe'],
257
- windowsHide: true,
258
- detached: process.platform !== 'win32',
259
- });
260
- childPid = child.pid;
261
- const finish = (status, signal) => {
262
- if (settled) {
263
- return;
264
- }
265
- settled = true;
266
- if (timeout) {
267
- clearTimeout(timeout);
268
- }
269
- resolve({
270
- status: timedOut ? null : status,
271
- signal: timedOut ? null : signal,
272
- error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
273
- stdout: stdout.toSnapshot(),
274
- stderr: stderr.toSnapshot(),
275
- pid: childPid,
276
- });
277
- };
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;
294
- child.stdout?.destroy();
295
- child.stderr?.destroy();
296
- child.unref();
297
- terminateProcessTreeNonBlocking(childPid);
298
- forceTerminateProcessTreeNonBlocking(childPid);
299
- 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
- };
351
- child.stdout?.on('data', (chunk) => {
352
- stdout.append(chunk);
353
- writeStreamChunk(reporter, 'stdout', chunk);
354
- });
355
- child.stderr?.on('data', (chunk) => {
356
- stderr.append(chunk);
357
- writeStreamChunk(reporter, 'stderr', chunk);
358
- });
359
- child.once('error', (error) => {
360
- childError = error;
361
- });
362
- child.once('close', (status, signal) => {
363
- finish(status, signal);
364
- });
365
- timeout = setTimeout(() => {
366
- timedOut = true;
367
- child.stdout?.destroy();
368
- child.stderr?.destroy();
369
- child.unref();
370
- terminateProcessTreeNonBlocking(childPid);
371
- forceTerminateProcessTreeNonBlocking(childPid);
372
- finish(null, null);
373
- }, timeoutSeconds * 1000);
374
- });
375
- }
376
- function getRunStatus(error, exitCode, successExitCodes) {
377
- const errorWithCode = error;
378
- if (errorWithCode?.code === 'ETIMEDOUT') {
379
- return 'timed_out';
380
- }
381
- if (error) {
382
- return 'start_failed';
383
- }
384
- return exitCode !== null && successExitCodes.includes(exitCode) ? 'passed' : 'failed';
385
- }
13
+ import { runBuiltinArgvInProcess } from './run/builtin-dispatch.js';
14
+ import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from './run/executor.js';
15
+ import { emitOutput } from './run/output.js';
16
+ import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree } from './run/process-tree.js';
17
+ import { assembleRunReceipt } from './run/receipt.js';
386
18
  function getRunPlanDetail(plan, lang, fallbackKey) {
387
19
  return plan.detail ?? t(lang, fallbackKey);
388
20
  }
@@ -540,8 +172,6 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
540
172
  }
541
173
  const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
542
174
  const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
543
- const lifecycleValue = plan.lifecycle ?? 'oneshot';
544
- const runPolicyValue = plan.runPolicy ?? 'agent_allowed';
545
175
  const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName));
546
176
  const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
547
177
  const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
@@ -556,17 +186,11 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
556
186
  }
557
187
  }
558
188
  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);
564
- }
565
- if (!json) {
566
- streamedOutput = true;
567
- return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
189
+ streamedOutput = !json;
190
+ return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
568
191
  }
569
- return runShellCommand(plan.shellCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
192
+ streamedOutput = !json;
193
+ return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
570
194
  });
571
195
  const childDurationMs = performance.now() - childStartedAtMs;
572
196
  const finishedAt = new Date();
@@ -574,44 +198,28 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
574
198
  const exitCode = typeof result.status === 'number' ? result.status : null;
575
199
  const runStatus = getRunStatus(result.error, exitCode, plan.successExitCodes);
576
200
  let killMethod = null;
201
+ let termination = null;
577
202
  if (runStatus === 'timed_out') {
578
- killMethod = getKillMethod();
579
- terminateProcessTree(result.pid);
203
+ termination = result.termination ?? createPendingTimeoutTermination(getKillMethod());
204
+ killMethod = termination.method;
205
+ if (!result.termination && result.pid) {
206
+ terminateProcessTree(result.pid);
207
+ }
580
208
  }
581
- const receipt = profiler.measure('receipt_create', () => createRunReceipt({
582
- intent: intentName,
583
- status: runStatus,
584
- timedOut: runStatus === 'timed_out',
209
+ const receipt = profiler.measure('receipt_create', () => assembleRunReceipt({
210
+ intentName,
211
+ runStatus,
585
212
  startedAt,
586
213
  finishedAt,
587
214
  projectRoot,
588
- cwd: plan.cwd,
589
- lifecycle: lifecycleValue,
590
- runPolicy: runPolicyValue,
591
- mode: plan.mode,
592
- argv: plan.commandArgv,
593
- cmd: plan.shellCommand,
594
- envPolicy: plan.envPolicy,
595
- envAllowlist: plan.envAllowlist,
596
- timeoutSeconds: plan.timeoutSeconds,
597
- maxOutputBytes: plan.maxOutputBytes,
598
- successExitCodes: plan.successExitCodes,
215
+ plan,
216
+ result,
599
217
  exitCode,
600
- signal: result.signal,
601
- error: result.error?.message ?? null,
602
218
  killMethod,
603
- stdout: result.stdout,
604
- stderr: result.stderr,
219
+ termination,
605
220
  writeDrift,
606
221
  executorOverheadMs: Math.max(0, Math.round((performance.now() - executorStartedAtMs - childDurationMs) * 1000) / 1000),
607
222
  phaseTimings: profiler.getReceiptPhases(),
608
- selectionSummary: {
609
- strategy: plan.testTargets.length > 0 ? 'project_test_selection' : 'direct_intent',
610
- changed_file_count: 0,
611
- changed_surface_counts: {},
612
- selected_target_count: Math.max(1, plan.testTargets.length),
613
- fallback_used: false,
614
- },
615
223
  stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
616
224
  stderrTailBytes: runReceiptPolicy.stderrTailBytes,
617
225
  }));