mustflow 2.18.2 → 2.18.7

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 (34) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/commands/run/builtin-dispatch.js +92 -0
  3. package/dist/cli/commands/run/executor.js +149 -0
  4. package/dist/cli/commands/run/output.js +59 -0
  5. package/dist/cli/commands/run/process-tree.js +91 -0
  6. package/dist/cli/commands/run/receipt.js +42 -0
  7. package/dist/cli/commands/run.js +17 -382
  8. package/dist/cli/commands/verify/args.js +262 -0
  9. package/dist/cli/commands/verify.js +1 -262
  10. package/dist/cli/i18n/en.js +1 -0
  11. package/dist/cli/i18n/es.js +1 -0
  12. package/dist/cli/i18n/fr.js +1 -0
  13. package/dist/cli/i18n/hi.js +1 -0
  14. package/dist/cli/i18n/ko.js +1 -0
  15. package/dist/cli/i18n/zh.js +1 -0
  16. package/dist/cli/index.js +6 -72
  17. package/dist/cli/lib/command-registry.js +27 -0
  18. package/dist/cli/lib/dashboard-export.js +2 -1
  19. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +3 -2
  20. package/dist/cli/lib/dashboard-html/template.js +5 -4
  21. package/dist/cli/lib/html-json.js +11 -0
  22. package/dist/cli/lib/local-index/index.js +166 -14
  23. package/dist/cli/lib/run-plan.js +6 -0
  24. package/dist/core/check-issues.js +1 -0
  25. package/dist/core/command-contract-rules.js +0 -3
  26. package/dist/core/command-contract-validation.js +42 -4
  27. package/dist/core/command-intent-eligibility.js +4 -4
  28. package/dist/core/contract-lint.js +3 -3
  29. package/package.json +1 -1
  30. package/templates/default/i18n.toml +7 -1
  31. package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
  32. package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
  33. package/templates/default/locales/en/.mustflow/skills/source-anchor-authoring/SKILL.md +147 -0
  34. package/templates/default/manifest.toml +8 -1
@@ -1,365 +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, createRunReceiptRelativePath, 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
- const OUTPUT_LIMIT_ERROR_CODE = 'ENOBUFS';
18
- const OUTPUT_LIMIT_ERROR_MESSAGE = /\bmaxBuffer\b.*\bexceeded\b/i;
19
- function emitOutput(reporter, output, stream) {
20
- if (!output) {
21
- return;
22
- }
23
- const text = (typeof output === 'object' && 'tail' in output ? output.tail : output.toString()).trimEnd();
24
- if (text.length === 0) {
25
- return;
26
- }
27
- reporter[stream](text);
28
- }
29
- function signalProcessTree(pid, signal) {
30
- if (!pid || pid <= 0) {
31
- return;
32
- }
33
- if (process.platform === 'win32') {
34
- spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
35
- stdio: 'ignore',
36
- windowsHide: true,
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
- }
46
- return;
47
- }
48
- try {
49
- process.kill(-pid, signal);
50
- }
51
- catch {
52
- try {
53
- process.kill(pid, signal);
54
- }
55
- catch {
56
- // The child may already be gone after Node's spawn timeout handling.
57
- }
58
- }
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
- }
105
- function getKillMethod() {
106
- return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
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
- }
119
- function createBufferedReporter() {
120
- const stdout = [];
121
- const stderr = [];
122
- return {
123
- reporter: {
124
- stdout(message) {
125
- stdout.push(`${message}\n`);
126
- },
127
- stderr(message) {
128
- stderr.push(`${message}\n`);
129
- },
130
- },
131
- stdout() {
132
- return stdout.join('');
133
- },
134
- stderr() {
135
- return stderr.join('');
136
- },
137
- };
138
- }
139
- /**
140
- * mf:anchor cli.run.builtin-inprocess
141
- * purpose: Dispatch selected mustflow built-in commands without spawning a nested CLI process.
142
- * search: builtin intent, in-process command, nested mf run, run receipt
143
- * invariant: Only commands classified by command-classification can use this path.
144
- * risk: config, state
145
- */
146
- async function runKnownBuiltinCommand(args, reporter, lang) {
147
- const [command, ...commandArgs] = args;
148
- if ((command === '--version' || command === '-v' || command === 'version') && commandArgs.length === 0) {
149
- reporter.stdout(getPackageVersion());
150
- return 0;
151
- }
152
- if (!canRunMustflowBuiltinInProcess(command)) {
153
- return undefined;
154
- }
155
- if (command === 'check') {
156
- return (await import('./check.js')).runCheck(commandArgs, reporter, lang);
157
- }
158
- if (command === 'classify') {
159
- return (await import('./classify.js')).runClassify(commandArgs, reporter, lang);
160
- }
161
- if (command === 'context') {
162
- return (await import('./context.js')).runContext(commandArgs, reporter, lang);
163
- }
164
- if (command === 'doctor') {
165
- return (await import('./doctor.js')).runDoctor(commandArgs, reporter, lang);
166
- }
167
- if (command === 'help') {
168
- return (await import('./help.js')).runHelp(commandArgs, reporter, lang);
169
- }
170
- if (command === 'impact') {
171
- return (await import('./impact.js')).runImpact(commandArgs, reporter, lang);
172
- }
173
- if (command === 'line-endings') {
174
- return (await import('./line-endings.js')).runLineEndings(commandArgs, reporter, lang);
175
- }
176
- if (command === 'map') {
177
- return (await import('./map.js')).runMap(commandArgs, reporter, lang);
178
- }
179
- if (command === 'status') {
180
- return (await import('./status.js')).runStatus(commandArgs, reporter, lang);
181
- }
182
- if (command === 'update') {
183
- return (await import('./update.js')).runUpdate(commandArgs, reporter, lang);
184
- }
185
- if (command === 'version-sources') {
186
- return (await import('./version-sources.js')).runVersionSources(commandArgs, reporter, lang);
187
- }
188
- return undefined;
189
- }
190
- async function withWorkingDirectory(cwd, callback) {
191
- const previousCwd = process.cwd();
192
- process.chdir(cwd);
193
- try {
194
- return await callback();
195
- }
196
- finally {
197
- process.chdir(previousCwd);
198
- }
199
- }
200
- async function runBuiltinArgvInProcess(commandArgv, cwd, lang) {
201
- const [command = '', ...builtinArgs] = commandArgv;
202
- if (!isMustflowBinName(command)) {
203
- return undefined;
204
- }
205
- const output = createBufferedReporter();
206
- try {
207
- const status = await withWorkingDirectory(cwd, () => runKnownBuiltinCommand(builtinArgs, output.reporter, lang));
208
- if (status === undefined) {
209
- return undefined;
210
- }
211
- return {
212
- status,
213
- signal: null,
214
- stdout: output.stdout(),
215
- stderr: output.stderr(),
216
- };
217
- }
218
- catch (error) {
219
- const message = error instanceof Error ? error.message : String(error);
220
- return {
221
- status: 1,
222
- signal: null,
223
- stdout: output.stdout(),
224
- stderr: `${output.stderr()}${message}\n`,
225
- };
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 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) {
249
- return new Promise((resolve) => {
250
- const stdout = new BoundedOutputBuffer(stdoutTailBytes);
251
- const stderr = new BoundedOutputBuffer(stderrTailBytes);
252
- let settled = false;
253
- let timedOut = false;
254
- let childError;
255
- let childPid;
256
- let stdoutBytes = 0;
257
- let stderrBytes = 0;
258
- let timeout;
259
- let termination = null;
260
- const child = spawn(command.executable, command.args ?? [], {
261
- cwd,
262
- env,
263
- shell: command.shell,
264
- stdio: ['ignore', 'pipe', 'pipe'],
265
- windowsHide: true,
266
- detached: process.platform !== 'win32',
267
- });
268
- childPid = child.pid;
269
- const finish = (status, signal) => {
270
- if (settled) {
271
- return;
272
- }
273
- settled = true;
274
- if (timeout) {
275
- clearTimeout(timeout);
276
- }
277
- resolve({
278
- status: timedOut ? null : status,
279
- signal: timedOut ? null : signal,
280
- error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
281
- stdout: stdout.toSnapshot(),
282
- stderr: stderr.toSnapshot(),
283
- pid: childPid,
284
- termination,
285
- });
286
- };
287
- const stopForOutputLimit = (stream) => {
288
- if (settled || childError) {
289
- return;
290
- }
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);
298
- };
299
- child.stdout?.on('data', (chunk) => {
300
- stdout.append(chunk);
301
- stdoutBytes += chunk.byteLength;
302
- if (streamOutput) {
303
- writeStreamChunk(reporter, 'stdout', chunk);
304
- }
305
- if (enforceOutputLimit && stdoutBytes > maxOutputBytes) {
306
- stopForOutputLimit('stdout');
307
- }
308
- });
309
- child.stderr?.on('data', (chunk) => {
310
- stderr.append(chunk);
311
- stderrBytes += chunk.byteLength;
312
- if (streamOutput) {
313
- writeStreamChunk(reporter, 'stderr', chunk);
314
- }
315
- if (enforceOutputLimit && stderrBytes > maxOutputBytes) {
316
- stopForOutputLimit('stderr');
317
- }
318
- });
319
- child.once('error', (error) => {
320
- childError = error;
321
- });
322
- child.once('close', (status, signal) => {
323
- finish(status, signal);
324
- });
325
- timeout = setTimeout(() => {
326
- timedOut = true;
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);
334
- }, timeoutSeconds * 1000);
335
- });
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
- }
343
- function getRunStatus(error, exitCode, successExitCodes) {
344
- const errorWithCode = error;
345
- if (errorWithCode?.code === 'ETIMEDOUT') {
346
- return 'timed_out';
347
- }
348
- if (isOutputLimitExceededError(error)) {
349
- return 'output_limit_exceeded';
350
- }
351
- if (error) {
352
- return 'start_failed';
353
- }
354
- return exitCode !== null && successExitCodes.includes(exitCode) ? 'passed' : 'failed';
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
- }
13
+ import { runBuiltinArgvInProcess } from './run/builtin-dispatch.js';
14
+ import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from './run/executor.js';
15
+ import { emitOutput, isOutputLimitExceededError } from './run/output.js';
16
+ import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree } from './run/process-tree.js';
17
+ import { assembleRunReceipt } from './run/receipt.js';
363
18
  function getRunPlanDetail(plan, lang, fallbackKey) {
364
19
  return plan.detail ?? t(lang, fallbackKey);
365
20
  }
@@ -517,8 +172,6 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
517
172
  }
518
173
  const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
519
174
  const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
520
- const lifecycleValue = plan.lifecycle ?? 'oneshot';
521
- const runPolicyValue = plan.runPolicy ?? 'agent_allowed';
522
175
  const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName));
523
176
  const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
524
177
  const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
@@ -534,10 +187,10 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
534
187
  }
535
188
  if (plan.commandArgv) {
536
189
  streamedOutput = !json;
537
- return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
190
+ return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
538
191
  }
539
192
  streamedOutput = !json;
540
- return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
193
+ return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
541
194
  });
542
195
  const childDurationMs = performance.now() - childStartedAtMs;
543
196
  const finishedAt = new Date();
@@ -553,44 +206,22 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
553
206
  terminateProcessTree(result.pid);
554
207
  }
555
208
  }
556
- const receipt = profiler.measure('receipt_create', () => createRunReceipt({
557
- intent: intentName,
558
- status: runStatus,
559
- timedOut: runStatus === 'timed_out',
209
+ const receipt = profiler.measure('receipt_create', () => assembleRunReceipt({
210
+ intentName,
211
+ runStatus,
560
212
  startedAt,
561
213
  finishedAt,
562
214
  projectRoot,
563
- cwd: plan.cwd,
564
- lifecycle: lifecycleValue,
565
- runPolicy: runPolicyValue,
566
- mode: plan.mode,
567
- argv: plan.commandArgv,
568
- cmd: plan.shellCommand,
569
- envPolicy: plan.envPolicy,
570
- envAllowlist: plan.envAllowlist,
571
- timeoutSeconds: plan.timeoutSeconds,
572
- maxOutputBytes: plan.maxOutputBytes,
573
- successExitCodes: plan.successExitCodes,
215
+ plan,
216
+ result,
574
217
  exitCode,
575
- signal: result.signal,
576
- error: result.error?.message ?? null,
577
218
  killMethod,
578
219
  termination,
579
- stdout: result.stdout,
580
- stderr: result.stderr,
581
220
  writeDrift,
582
221
  executorOverheadMs: Math.max(0, Math.round((performance.now() - executorStartedAtMs - childDurationMs) * 1000) / 1000),
583
222
  phaseTimings: profiler.getReceiptPhases(),
584
- selectionSummary: {
585
- strategy: plan.testTargets.length > 0 ? 'project_test_selection' : 'direct_intent',
586
- changed_file_count: 0,
587
- changed_surface_counts: {},
588
- selected_target_count: Math.max(1, plan.testTargets.length),
589
- fallback_used: false,
590
- },
591
223
  stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
592
224
  stderrTailBytes: runReceiptPolicy.stderrTailBytes,
593
- receiptPath: createRunReceiptRelativePath(),
594
225
  }));
595
226
  if (options.writeLatestReceipt !== false) {
596
227
  profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));
@@ -616,6 +247,10 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
616
247
  reporter.stderr(t(lang, 'run.error.timedOut', { intent: intentName, seconds: plan.timeoutSeconds }));
617
248
  return 1;
618
249
  }
250
+ if (isOutputLimitExceededError(result.error)) {
251
+ reporter.stderr(t(lang, 'run.error.outputLimitExceeded', { intent: intentName, message: result.error.message }));
252
+ return 1;
253
+ }
619
254
  reporter.stderr(t(lang, 'run.error.startFailed', { intent: intentName, message: result.error.message }));
620
255
  return 1;
621
256
  }