mustflow 2.103.16 → 2.103.21

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 (39) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/commands/run/args.js +83 -0
  3. package/dist/cli/commands/run/execution.js +334 -0
  4. package/dist/cli/commands/run/preview.js +29 -0
  5. package/dist/cli/commands/run/profile.js +6 -0
  6. package/dist/cli/commands/run.js +19 -425
  7. package/dist/cli/commands/script-pack.js +1 -0
  8. package/dist/cli/commands/verify.js +15 -18
  9. package/dist/cli/i18n/en.js +27 -0
  10. package/dist/cli/i18n/es.js +27 -0
  11. package/dist/cli/i18n/fr.js +27 -0
  12. package/dist/cli/i18n/hi.js +27 -0
  13. package/dist/cli/i18n/ko.js +27 -0
  14. package/dist/cli/i18n/zh.js +27 -0
  15. package/dist/cli/lib/command-registry.js +92 -0
  16. package/dist/cli/lib/option-parser.js +26 -0
  17. package/dist/cli/lib/script-pack-registry.js +39 -0
  18. package/dist/cli/script-packs/code-module-boundary.js +210 -0
  19. package/dist/cli/script-packs/repo-env-contract.js +4 -17
  20. package/dist/cli/script-packs/repo-secret-risk-scan.js +4 -17
  21. package/dist/cli/script-packs/repo-security-pattern-scan.js +4 -17
  22. package/dist/core/module-boundary.js +523 -0
  23. package/dist/core/public-json-contracts.js +50 -0
  24. package/dist/core/script-pack-suggestions.js +5 -0
  25. package/package.json +1 -1
  26. package/schemas/README.md +12 -0
  27. package/schemas/check-report.schema.json +52 -0
  28. package/schemas/index-report.schema.json +103 -0
  29. package/schemas/module-boundary-report.schema.json +210 -0
  30. package/schemas/search-report.schema.json +102 -0
  31. package/schemas/status-report.schema.json +50 -0
  32. package/templates/default/i18n.toml +10 -4
  33. package/templates/default/locales/en/.mustflow/skills/INDEX.md +7 -1
  34. package/templates/default/locales/en/.mustflow/skills/database-migration-change/SKILL.md +16 -2
  35. package/templates/default/locales/en/.mustflow/skills/http-api-semantics-review/SKILL.md +286 -0
  36. package/templates/default/locales/en/.mustflow/skills/module-boundary-review/SKILL.md +12 -1
  37. package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +17 -10
  38. package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
  39. package/templates/default/manifest.toml +8 -1
@@ -1,261 +1,11 @@
1
- import { createHash } from 'node:crypto';
2
- import { performance } from 'node:perf_hooks';
3
- import { ACTIVE_RUN_LOCK_ID_ENV, acquireActiveRunLock } from '../../core/active-run-locks.js';
4
- import { createCommandEnv } from '../../core/command-env.js';
5
- import { createCorrelationId } from '../../core/correlation-id.js';
6
- import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
7
- import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
8
- import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
1
+ import { printUsageError } from '../lib/cli-output.js';
9
2
  import { t } from '../lib/i18n.js';
10
- import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
11
- import { resolveMustflowRoot } from '../lib/project-root.js';
12
- import { ALLOW_UNTRUSTED_ROOT_OPTION, assessRunRootTrust } from '../lib/run-root-trust.js';
13
- import { createRunPlan, createRunPreview, renderRunPreviewText, } from '../lib/run-plan.js';
14
- import { writeRunReceipt, } from '../../core/run-receipt.js';
15
- import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
16
- import { RunProfiler } from '../../core/run-profile.js';
17
- import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
18
- import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from './run/executor.js';
19
- import { emitOutput, isOutputLimitExceededError } from './run/output.js';
20
- import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree } from './run/process-tree.js';
21
- import { assembleRunReceipt } from './run/receipt.js';
22
- const DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS = 300;
23
- const ACTIVE_LOCK_WAIT_POLL_MS = 1_000;
24
- const RUN_OPTIONS = [
25
- { name: '--json', kind: 'boolean' },
26
- { name: '--dry-run', kind: 'boolean' },
27
- { name: '--plan-only', kind: 'boolean' },
28
- { name: '--wait', kind: 'boolean' },
29
- { name: ALLOW_UNTRUSTED_ROOT_OPTION, kind: 'boolean' },
30
- { name: '--wait-timeout', kind: 'string' },
31
- ];
32
- function delay(milliseconds) {
33
- return new Promise((resolve) => {
34
- setTimeout(resolve, milliseconds);
35
- });
36
- }
37
- function getRunPlanDetail(plan, lang, fallbackKey) {
38
- return plan.detail ?? t(lang, fallbackKey);
39
- }
40
- function reportRunPlanFailure(plan, reporter, lang) {
41
- let message;
42
- switch (plan.reasonCode) {
43
- case 'status_not_configured':
44
- message = t(lang, 'run.error.statusNotConfigured', { intent: plan.intentName, status: plan.intentStatus ?? 'unknown' });
45
- break;
46
- case 'lifecycle_not_oneshot':
47
- message = t(lang, 'run.error.lifecycleNotOneshot', { intent: plan.intentName, lifecycle: plan.lifecycle ?? 'unknown' });
48
- break;
49
- case 'run_policy_not_agent_allowed':
50
- message = t(lang, 'run.error.runPolicy', { intent: plan.intentName });
51
- break;
52
- case 'stdin_not_closed':
53
- message = t(lang, 'run.error.stdin', { intent: plan.intentName });
54
- break;
55
- case 'agent_shell_requires_allow':
56
- message = t(lang, 'run.error.agentShellRequiresAllow', { intent: plan.intentName });
57
- break;
58
- case 'missing_timeout':
59
- message = t(lang, 'run.error.timeout', { intent: plan.intentName });
60
- break;
61
- case 'missing_command_source':
62
- message = t(lang, 'run.error.commandSource', { intent: plan.intentName });
63
- break;
64
- case 'unsafe_intent_name':
65
- message = t(lang, 'run.error.unsafeIntent', {
66
- intent: plan.intentName,
67
- detail: getRunPlanDetail(plan, lang, 'run.error.unsafeIntentDetail'),
68
- });
69
- break;
70
- case 'blocked_shell_background_pattern':
71
- message = t(lang, 'run.error.blockedShellBackground', {
72
- intent: plan.intentName,
73
- detail: getRunPlanDetail(plan, lang, 'run.error.blockedShellBackgroundDetail'),
74
- });
75
- break;
76
- case 'blocked_long_running_command_pattern':
77
- message = t(lang, 'run.error.blockedLongRunningCommand', {
78
- intent: plan.intentName,
79
- detail: getRunPlanDetail(plan, lang, 'run.error.blockedLongRunningCommandDetail'),
80
- });
81
- break;
82
- case 'network_requires_approval':
83
- case 'destructive_requires_approval':
84
- case 'approval_policy_unreadable':
85
- message = t(lang, 'run.error.approvalRequired', {
86
- intent: plan.intentName,
87
- detail: getRunPlanDetail(plan, lang, 'run.error.approvalRequiredDetail'),
88
- });
89
- break;
90
- case 'cwd_outside_project':
91
- message = t(lang, 'run.error.cwdOutsideProject', {
92
- intent: plan.intentName,
93
- detail: getRunPlanDetail(plan, lang, 'run.error.cwdOutsideProjectDetail'),
94
- });
95
- break;
96
- case 'invalid_test_target':
97
- message = t(lang, 'run.error.invalidTestTarget', {
98
- intent: plan.intentName,
99
- detail: getRunPlanDetail(plan, lang, 'run.error.invalidTestTargetDetail'),
100
- });
101
- break;
102
- case 'max_output_bytes_exceeds_limit':
103
- message = t(lang, 'run.error.maxOutputBytes', {
104
- intent: plan.intentName,
105
- detail: getRunPlanDetail(plan, lang, 'run.error.maxOutputBytesDetail'),
106
- });
107
- break;
108
- case 'intent_not_table':
109
- default:
110
- message = t(lang, 'run.error.unknownIntent', { intent: plan.intentName });
111
- break;
112
- }
113
- if (plan.suggestedIntentSnippet) {
114
- message = `${message}\n\n${t(lang, 'run.label.suggestedIntentSnippet')}:\n${plan.suggestedIntentSnippet}`;
115
- }
116
- reporter.stderr(renderCliError(message, 'mf help commands', lang));
117
- }
118
- function writeLatestProfile(profiler, options, input) {
119
- if (options.writeLatestProfile === false) {
120
- return;
121
- }
122
- profiler.writeLatest(input);
123
- }
124
- function createPlanCommandHash(plan) {
125
- const payload = {
126
- mode: plan.mode,
127
- cwd: plan.relativeCwd,
128
- argv: plan.commandArgv ?? null,
129
- cmd: plan.shellCommand ?? null,
130
- };
131
- return `sha256:${createHash('sha256').update(JSON.stringify(payload)).digest('hex')}`;
132
- }
133
- function renderActiveLockConflictMessage(intentName, conflicts, lang) {
134
- const [first] = conflicts;
135
- const detail = first
136
- ? t(lang, 'run.error.activeLockConflictDetail', {
137
- lock: first.lock,
138
- intent: first.conflictsWithIntent,
139
- pid: first.conflictsWithPid,
140
- })
141
- : t(lang, 'run.error.activeLockConflictUnknown');
142
- return t(lang, 'run.error.activeLockConflict', { intent: intentName, detail });
143
- }
144
- function parseRunArguments(args) {
145
- const parsed = parseCliOptions(args, RUN_OPTIONS, { allowPositionals: true });
146
- let waitTimeoutSeconds = DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS;
147
- if (parsed.error) {
148
- const [intentName, ...extra] = parsed.positionals;
149
- return {
150
- json: hasParsedCliOption(parsed, '--json'),
151
- dryRun: hasParsedCliOption(parsed, '--dry-run'),
152
- planOnly: hasParsedCliOption(parsed, '--plan-only'),
153
- allowUntrustedRoot: hasParsedCliOption(parsed, ALLOW_UNTRUSTED_ROOT_OPTION),
154
- wait: hasParsedCliOption(parsed, '--wait'),
155
- waitTimeoutSeconds,
156
- intentName: intentName ?? null,
157
- extra,
158
- error: parsed.error,
159
- };
160
- }
161
- const waitTimeoutValue = getParsedCliStringOption(parsed, '--wait-timeout');
162
- let error = null;
163
- if (waitTimeoutValue !== null) {
164
- const parsedWaitTimeout = Number(waitTimeoutValue);
165
- if (!Number.isInteger(parsedWaitTimeout) || parsedWaitTimeout <= 0) {
166
- error = 'invalid_wait_timeout';
167
- }
168
- else {
169
- waitTimeoutSeconds = parsedWaitTimeout;
170
- }
171
- }
172
- const [intentName, ...extra] = parsed.positionals;
173
- return {
174
- json: hasParsedCliOption(parsed, '--json'),
175
- dryRun: hasParsedCliOption(parsed, '--dry-run'),
176
- planOnly: hasParsedCliOption(parsed, '--plan-only'),
177
- allowUntrustedRoot: hasParsedCliOption(parsed, ALLOW_UNTRUSTED_ROOT_OPTION),
178
- wait: hasParsedCliOption(parsed, '--wait'),
179
- waitTimeoutSeconds,
180
- intentName: intentName ?? null,
181
- extra,
182
- error,
183
- };
184
- }
185
- async function acquireActiveRunLockWithOptionalWait(input) {
186
- const startedAt = Date.now();
187
- let reportedWait = false;
188
- while (true) {
189
- const result = acquireActiveRunLock(input.projectRoot, input.contract, input.intentName, { commandHash: input.commandHash });
190
- if (result.ok || !input.enabled || result.conflicts.length === 0) {
191
- return result;
192
- }
193
- if (!input.json && !reportedWait) {
194
- const [first] = result.conflicts;
195
- input.reporter.stderr(t(input.lang, 'run.progress.waitingForActiveLock', {
196
- intent: input.intentName,
197
- activeIntent: first?.conflictsWithIntent ?? 'unknown',
198
- seconds: input.waitTimeoutSeconds,
199
- }));
200
- reportedWait = true;
201
- }
202
- if (Date.now() - startedAt >= input.waitTimeoutSeconds * 1000) {
203
- return result;
204
- }
205
- await delay(Math.min(ACTIVE_LOCK_WAIT_POLL_MS, Math.max(1, input.waitTimeoutSeconds * 1000 - (Date.now() - startedAt))));
206
- }
207
- }
208
- function createRunProgressReporter(input) {
209
- if (!input.enabled) {
210
- return () => undefined;
211
- }
212
- input.reporter.stderr(t(input.lang, 'run.progress.started', { intent: input.intentName, seconds: input.timeoutSeconds }));
213
- const timers = [];
214
- for (const ratio of [0.5, 0.8]) {
215
- const delayMs = Math.max(1, Math.floor(input.timeoutSeconds * 1000 * ratio));
216
- const elapsedSeconds = Math.max(1, Math.round(input.timeoutSeconds * ratio));
217
- const timer = setTimeout(() => {
218
- input.reporter.stderr(t(input.lang, 'run.progress.timeoutWarning', {
219
- intent: input.intentName,
220
- seconds: elapsedSeconds,
221
- percent: Math.round(ratio * 100),
222
- }));
223
- }, delayMs);
224
- timer.unref?.();
225
- timers.push(timer);
226
- }
227
- return () => {
228
- for (const timer of timers) {
229
- clearTimeout(timer);
230
- }
231
- };
232
- }
233
- export function getRunHelp(lang = 'en') {
234
- return renderHelp({
235
- usage: 'mf run <intent> [options]',
236
- summary: t(lang, 'run.help.summary'),
237
- options: [
238
- { label: '--dry-run', description: t(lang, 'run.help.option.dryRun') },
239
- { label: '--plan-only', description: t(lang, 'run.help.option.planOnly') },
240
- { label: '--json', description: t(lang, 'run.help.option.json') },
241
- { label: '--wait', description: t(lang, 'run.help.option.wait') },
242
- { label: '--wait-timeout <seconds>', description: t(lang, 'run.help.option.waitTimeout') },
243
- { label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
244
- { label: '-h, --help', description: t(lang, 'cli.option.help') },
245
- ],
246
- examples: ['mf run test', 'mf run lint --json', 'mf run mustflow_check --dry-run --json'],
247
- exitCodes: [
248
- {
249
- label: '0',
250
- description: t(lang, 'run.help.exit.ok'),
251
- },
252
- {
253
- label: '1',
254
- description: t(lang, 'run.help.exit.fail'),
255
- },
256
- ],
257
- }, lang);
258
- }
3
+ import { formatCliOptionParseError } from '../lib/option-parser.js';
4
+ import { getRunHelp, hasRunHelpOption, parseRunArguments } from './run/args.js';
5
+ import { executeRunCommand } from './run/execution.js';
6
+ import { executeRunPreviewCommand, getRunPreviewMode } from './run/preview.js';
7
+ export { getRunHelp } from './run/args.js';
8
+ export { executeRunCommand } from './run/execution.js';
259
9
  /**
260
10
  * mf:anchor cli.run.intent-contract
261
11
  * purpose: Enforce command intent eligibility before executing a configured oneshot command.
@@ -264,9 +14,7 @@ export function getRunHelp(lang = 'en') {
264
14
  * risk: config, security, state
265
15
  */
266
16
  export async function runRun(args, reporter, lang = 'en', options = {}) {
267
- const executorStartedAtMs = performance.now();
268
- const profiler = new RunProfiler();
269
- if (hasCliOptionToken(args, '--help', ['-h'])) {
17
+ if (hasRunHelpOption(args)) {
270
18
  reporter.stdout(getRunHelp(lang));
271
19
  return 0;
272
20
  }
@@ -287,7 +35,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
287
35
  const dryRun = parsedArgs.dryRun;
288
36
  const planOnly = parsedArgs.planOnly;
289
37
  const allowUntrustedRoot = parsedArgs.allowUntrustedRoot;
290
- const previewMode = dryRun ? 'dry-run' : planOnly ? 'plan-only' : null;
38
+ const previewMode = getRunPreviewMode({ dryRun, planOnly });
291
39
  if (dryRun && planOnly) {
292
40
  printUsageError(reporter, t(lang, 'run.error.conflictingPreviewModes'), 'mf run --help', getRunHelp(lang), lang);
293
41
  return 1;
@@ -306,169 +54,15 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
306
54
  printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: extra[0] }), 'mf run --help', getRunHelp(lang), lang);
307
55
  return 1;
308
56
  }
309
- const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
310
- const rootTrust = profiler.measure('root_trust', () => assessRunRootTrust(projectRoot));
311
- if (!previewMode && !allowUntrustedRoot && !rootTrust.trusted) {
312
- const message = rootTrust.reason === 'manifest_lock_invalid'
313
- ? t(lang, 'run.error.untrustedRootInvalid', { detail: rootTrust.detail ?? rootTrust.manifestLockPath })
314
- : t(lang, 'run.error.untrustedRootMissing', { path: rootTrust.detail ?? rootTrust.manifestLockPath });
315
- reporter.stderr(renderCliError(message, 'mf run --help', lang));
316
- return 1;
317
- }
318
- const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
319
- const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, intentName, { testTargets: options.testTargets }));
320
- if (previewMode) {
321
- profiler.measure('preview_render', () => {
322
- if (json) {
323
- reporter.stdout(JSON.stringify(createRunPreview(plan, previewMode), null, 2));
324
- }
325
- else {
326
- reporter.stdout(renderRunPreviewText(plan, previewMode, lang));
327
- }
328
- });
329
- writeLatestProfile(profiler, options, {
330
- projectRoot,
331
- intent: intentName,
332
- status: plan.ok ? 'previewed' : 'blocked',
333
- previewMode,
334
- });
335
- return plan.ok ? 0 : 1;
336
- }
337
- if (!plan.ok) {
338
- if (json) {
339
- reporter.stdout(JSON.stringify(createRunPreview(plan, 'plan-only'), null, 2));
340
- }
341
- reportRunPlanFailure(plan, reporter, lang);
342
- writeLatestProfile(profiler, options, {
343
- projectRoot,
344
- intent: intentName,
345
- status: 'blocked',
346
- previewMode: null,
347
- });
348
- return 1;
349
- }
350
- const activeRunLock = await profiler.measureAsync('active_lock_acquire', () => acquireActiveRunLockWithOptionalWait({
351
- enabled: parsedArgs.wait,
352
- waitTimeoutSeconds: parsedArgs.waitTimeoutSeconds,
353
- projectRoot,
354
- contract,
355
- intentName,
356
- commandHash: createPlanCommandHash(plan),
357
- json,
358
- reporter,
359
- lang,
360
- }));
361
- if (!activeRunLock.ok) {
362
- reporter.stderr(renderCliError(renderActiveLockConflictMessage(intentName, activeRunLock.conflicts, lang), 'mf run --dry-run --json', lang));
363
- writeLatestProfile(profiler, options, {
364
- projectRoot,
365
- intent: intentName,
366
- status: 'blocked',
367
- previewMode: null,
368
- });
369
- return 1;
370
- }
371
- try {
372
- const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
373
- const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
374
- env[ACTIVE_RUN_LOCK_ID_ENV] = activeRunLock.handle.record.run_id;
375
- const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName, {
376
- additionalDeclaredPaths: options.additionalDeclaredWritePaths,
377
- env,
378
- }));
379
- const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
380
- const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
381
- let streamedOutput = false;
382
- const childStartedAtMs = performance.now();
383
- const startedAt = new Date();
384
- const stopRunProgress = createRunProgressReporter({
385
- enabled: !json && Boolean(reporter.writeStderr),
57
+ if (!previewMode) {
58
+ const result = await executeRunCommand({
386
59
  intentName,
387
- timeoutSeconds: plan.timeoutSeconds,
388
- reporter,
389
- lang,
390
- });
391
- const result = await profiler.measureAsync('child_command', async () => {
392
- try {
393
- if (plan.commandArgv) {
394
- streamedOutput = !json;
395
- return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
396
- }
397
- streamedOutput = !json;
398
- return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
399
- }
400
- finally {
401
- stopRunProgress();
402
- }
403
- });
404
- const childDurationMs = performance.now() - childStartedAtMs;
405
- const finishedAt = new Date();
406
- const writeDrift = profiler.measure('write_drift_after', () => finishRunWriteTracking(writeTracker));
407
- const exitCode = typeof result.status === 'number' ? result.status : null;
408
- const runStatus = getRunStatus(result.error, exitCode, plan.successExitCodes);
409
- let killMethod = null;
410
- let termination = null;
411
- if (runStatus === 'timed_out') {
412
- termination = result.termination ?? createPendingTimeoutTermination(getKillMethod());
413
- killMethod = termination.method;
414
- if (!result.termination && result.pid) {
415
- terminateProcessTree(result.pid);
416
- }
417
- }
418
- const receipt = profiler.measure('receipt_create', () => assembleRunReceipt({
419
- correlationId: options.correlationId ?? createCorrelationId('run'),
420
- intentName,
421
- runStatus,
422
- startedAt,
423
- finishedAt,
424
- projectRoot,
425
- plan,
426
- result,
427
- exitCode,
428
- killMethod,
429
- termination,
430
- writeDrift,
431
- executorOverheadMs: Math.max(0, Math.round((performance.now() - executorStartedAtMs - childDurationMs) * 1000) / 1000),
432
- phaseTimings: profiler.getReceiptPhases(),
433
- stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
434
- stderrTailBytes: runReceiptPolicy.stderrTailBytes,
435
- }));
436
- if (options.writeLatestReceipt !== false) {
437
- profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));
438
- }
439
- if (options.recordPerformanceHistory !== false) {
440
- profiler.measure('performance_history_write', () => recordRunPerformanceHistory(projectRoot, receipt));
441
- }
442
- writeLatestProfile(profiler, options, {
443
- projectRoot,
444
- intent: intentName,
445
- status: runStatus,
446
- previewMode: null,
447
- });
448
- if (json) {
449
- reporter.stdout(JSON.stringify(receipt, null, 2));
450
- return runStatus === 'passed' ? 0 : 1;
451
- }
452
- if (!streamedOutput) {
453
- emitOutput(reporter, result.stdout, 'stdout');
454
- emitOutput(reporter, result.stderr, 'stderr');
455
- }
456
- if (result.error) {
457
- const errorWithCode = result.error;
458
- if (errorWithCode.code === 'ETIMEDOUT') {
459
- reporter.stderr(t(lang, 'run.error.timedOut', { intent: intentName, seconds: plan.timeoutSeconds }));
460
- return 1;
461
- }
462
- if (isOutputLimitExceededError(result.error)) {
463
- reporter.stderr(t(lang, 'run.error.outputLimitExceeded', { intent: intentName, message: result.error.message }));
464
- return 1;
465
- }
466
- reporter.stderr(t(lang, 'run.error.startFailed', { intent: intentName, message: result.error.message }));
467
- return 1;
468
- }
469
- return runStatus === 'passed' ? 0 : 1;
470
- }
471
- finally {
472
- activeRunLock.handle.release();
473
- }
60
+ outputMode: json ? 'json' : 'text',
61
+ allowUntrustedRoot,
62
+ wait: parsedArgs.wait,
63
+ waitTimeoutSeconds: parsedArgs.waitTimeoutSeconds,
64
+ }, reporter, lang, options);
65
+ return result.exitCode;
66
+ }
67
+ return executeRunPreviewCommand({ intentName, json, previewMode }, reporter, lang, options);
474
68
  }
@@ -64,6 +64,7 @@ export function getScriptPackHelp(lang = 'en') {
64
64
  'mf script-pack run code/outline scan src --json',
65
65
  'mf script-pack run code/dependency-graph scan src/cli/index.ts --json',
66
66
  'mf script-pack run code/import-cycle check src --json',
67
+ 'mf script-pack run code/module-boundary check src --json',
67
68
  'mf script-pack run code/change-impact analyze --base HEAD --json',
68
69
  'mf script-pack run code/symbol-read read src/cli/index.ts --start-line 25 --json',
69
70
  'mf script-pack run code/route-outline scan src/cli/index.ts --json',
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { runRun } from './run.js';
2
+ import { executeRunCommand } from './run/execution.js';
3
3
  import { createChangeVerificationReport, } from '../../core/change-verification.js';
4
4
  import { createCorrelationId } from '../../core/correlation-id.js';
5
5
  import { readUtf8FileInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
@@ -51,6 +51,9 @@ function createBufferedOutput() {
51
51
  },
52
52
  };
53
53
  }
54
+ function toVerificationReceipt(receipt) {
55
+ return receipt ? { ...receipt } : null;
56
+ }
54
57
  export function getVerifyHelp(lang = 'en') {
55
58
  return renderHelp({
56
59
  usage: 'mf verify --reason <event> [options] | mf verify --from-classification <path> [options] | mf verify --changed [options]',
@@ -152,7 +155,13 @@ function testTargetsByScheduledIntent(report) {
152
155
  }
153
156
  async function runVerificationIntent(intent, lang, verificationPlanId, correlationId, testTargets = [], additionalDeclaredWritePaths = []) {
154
157
  const output = createBufferedOutput();
155
- const exitCode = await runRun([intent, '--json'], output.reporter, lang, {
158
+ const runResult = await executeRunCommand({
159
+ intentName: intent,
160
+ outputMode: 'silent',
161
+ allowUntrustedRoot: false,
162
+ wait: false,
163
+ waitTimeoutSeconds: 1,
164
+ }, output.reporter, lang, {
156
165
  correlationId,
157
166
  writeLatestReceipt: false,
158
167
  writeLatestProfile: false,
@@ -160,23 +169,11 @@ async function runVerificationIntent(intent, lang, verificationPlanId, correlati
160
169
  testTargets,
161
170
  additionalDeclaredWritePaths,
162
171
  });
163
- const rawStdout = output.stdout().trim();
164
- let receipt = null;
172
+ const exitCode = runResult.exitCode;
173
+ const receipt = toVerificationReceipt(runResult.receipt);
165
174
  let status = exitCode === 0 ? 'passed' : 'failed';
166
- try {
167
- const parsed = JSON.parse(rawStdout);
168
- receipt = parsed;
169
- const receiptStatus = parsed.status;
170
- if (receiptStatus === 'passed' ||
171
- receiptStatus === 'failed' ||
172
- receiptStatus === 'timed_out' ||
173
- receiptStatus === 'start_failed' ||
174
- receiptStatus === 'output_limit_exceeded') {
175
- status = receiptStatus;
176
- }
177
- }
178
- catch {
179
- status = 'failed';
175
+ if (runResult.receipt) {
176
+ status = runResult.receipt.status;
180
177
  }
181
178
  return {
182
179
  intent,
@@ -777,6 +777,7 @@ Read these files before working:
777
777
  "scriptPack.script.codeOutline.summary": "Scan TypeScript and JavaScript files for symbol headers and line ranges",
778
778
  "scriptPack.script.codeDependencyGraph.summary": "Trace relative TypeScript and JavaScript dependency graph edges",
779
779
  "scriptPack.script.codeImportCycle.summary": "Detect relative TypeScript and JavaScript import cycles with line evidence",
780
+ "scriptPack.script.codeModuleBoundary.summary": "Check configured module boundary import rules and shared budgets",
780
781
  "scriptPack.script.codeChangeImpact.summary": "Analyze changed files for impact, script-pack, and verification hints",
781
782
  "scriptPack.script.codeSymbolRead.summary": "Read a bounded source snippet by source anchor, symbol line, or explicit line range",
782
783
  "scriptPack.script.codeRouteOutline.summary": "Scan Hono, Elysia, Axum, and NestJS route methods, paths, handlers, and lifecycle chains",
@@ -978,6 +979,32 @@ Read these files before working:
978
979
  "importCycle.error.unknownAction": "Unknown import-cycle action: {action}",
979
980
  "importCycle.error.missingPath": "Provide at least one path to check",
980
981
  "importCycle.error.invalidPositiveInteger": "{option} must be a positive safe integer: {value}",
982
+ "moduleBoundary.help.summary": "Check configured module boundary rules against TypeScript and JavaScript import graph evidence.",
983
+ "moduleBoundary.help.option.config": "Module boundary TOML config path. Default: .mustflow/config/module-boundaries.toml",
984
+ "moduleBoundary.help.option.maxDepth": "Maximum dependency depth from target files. Default: 20",
985
+ "moduleBoundary.help.option.maxFiles": "Maximum number of source files to scan. Default: 1000",
986
+ "moduleBoundary.help.option.maxFileBytes": "Maximum bytes to read from each source file. Default: 262144",
987
+ "moduleBoundary.help.option.maxNodes": "Maximum number of graph nodes to inspect. Default: 300",
988
+ "moduleBoundary.help.option.maxEdges": "Maximum number of graph edges to inspect. Default: 800",
989
+ "moduleBoundary.help.option.maxCycles": "Maximum number of import cycles to report. Default: 50",
990
+ "moduleBoundary.help.option.maxSharedFiles": "Maximum number of shared-budget source files to count. Default: 1000",
991
+ "moduleBoundary.help.exit.ok": "No blocking module boundary findings were detected",
992
+ "moduleBoundary.help.exit.fail": "A module boundary rule, cycle, invalid input, unreadable path, or scan limit failed",
993
+ "moduleBoundary.title": "mustflow module boundary",
994
+ "moduleBoundary.label.config": "Config",
995
+ "moduleBoundary.label.targets": "Targets",
996
+ "moduleBoundary.label.edges": "Edges",
997
+ "moduleBoundary.label.rules": "Rules",
998
+ "moduleBoundary.label.findings": "Findings",
999
+ "moduleBoundary.label.truncated": "Truncated",
1000
+ "moduleBoundary.label.findingList": "Module boundary findings",
1001
+ "moduleBoundary.label.sharedMetrics": "Shared budget metrics",
1002
+ "moduleBoundary.label.issues": "Issues",
1003
+ "moduleBoundary.clean": "No module boundary findings were found.",
1004
+ "moduleBoundary.error.missingAction": "Specify a module-boundary action: check",
1005
+ "moduleBoundary.error.unknownAction": "Unknown module-boundary action: {action}",
1006
+ "moduleBoundary.error.missingPath": "Provide at least one path to check",
1007
+ "moduleBoundary.error.invalidPositiveInteger": "{option} must be a positive safe integer: {value}",
981
1008
  "changeImpact.help.summary": "Analyze git changes and return bounded file-impact, script-pack, and verification hints.",
982
1009
  "changeImpact.help.option.base": "Git base ref to compare from. Default: HEAD",
983
1010
  "changeImpact.help.option.head": "Git head ref to compare to. Omit to compare the base with the working tree.",
@@ -777,6 +777,7 @@ Lee estos archivos antes de trabajar:
777
777
  "scriptPack.script.codeOutline.summary": "Scan TypeScript and JavaScript files for symbol headers and line ranges",
778
778
  "scriptPack.script.codeDependencyGraph.summary": "Trace relative TypeScript and JavaScript dependency graph edges",
779
779
  "scriptPack.script.codeImportCycle.summary": "Detect relative TypeScript and JavaScript import cycles with line evidence",
780
+ "scriptPack.script.codeModuleBoundary.summary": "Check configured module boundary import rules and shared budgets",
780
781
  "scriptPack.script.codeChangeImpact.summary": "Analyze changed files for impact, script-pack, and verification hints",
781
782
  "scriptPack.script.codeSymbolRead.summary": "Read a bounded source snippet by source anchor, symbol line, or explicit line range",
782
783
  "scriptPack.script.codeRouteOutline.summary": "Scan Hono, Elysia, Axum, and NestJS route methods, paths, handlers, and lifecycle chains",
@@ -978,6 +979,32 @@ Lee estos archivos antes de trabajar:
978
979
  "importCycle.error.unknownAction": "Unknown import-cycle action: {action}",
979
980
  "importCycle.error.missingPath": "Provide at least one path to check",
980
981
  "importCycle.error.invalidPositiveInteger": "{option} must be a positive safe integer: {value}",
982
+ "moduleBoundary.help.summary": "Check configured module boundary rules against TypeScript and JavaScript import graph evidence.",
983
+ "moduleBoundary.help.option.config": "Module boundary TOML config path. Default: .mustflow/config/module-boundaries.toml",
984
+ "moduleBoundary.help.option.maxDepth": "Maximum dependency depth from target files. Default: 20",
985
+ "moduleBoundary.help.option.maxFiles": "Maximum number of source files to scan. Default: 1000",
986
+ "moduleBoundary.help.option.maxFileBytes": "Maximum bytes to read from each source file. Default: 262144",
987
+ "moduleBoundary.help.option.maxNodes": "Maximum number of graph nodes to inspect. Default: 300",
988
+ "moduleBoundary.help.option.maxEdges": "Maximum number of graph edges to inspect. Default: 800",
989
+ "moduleBoundary.help.option.maxCycles": "Maximum number of import cycles to report. Default: 50",
990
+ "moduleBoundary.help.option.maxSharedFiles": "Maximum number of shared-budget source files to count. Default: 1000",
991
+ "moduleBoundary.help.exit.ok": "No blocking module boundary findings were detected",
992
+ "moduleBoundary.help.exit.fail": "A module boundary rule, cycle, invalid input, unreadable path, or scan limit failed",
993
+ "moduleBoundary.title": "mustflow module boundary",
994
+ "moduleBoundary.label.config": "Config",
995
+ "moduleBoundary.label.targets": "Targets",
996
+ "moduleBoundary.label.edges": "Edges",
997
+ "moduleBoundary.label.rules": "Rules",
998
+ "moduleBoundary.label.findings": "Findings",
999
+ "moduleBoundary.label.truncated": "Truncated",
1000
+ "moduleBoundary.label.findingList": "Module boundary findings",
1001
+ "moduleBoundary.label.sharedMetrics": "Shared budget metrics",
1002
+ "moduleBoundary.label.issues": "Issues",
1003
+ "moduleBoundary.clean": "No module boundary findings were found.",
1004
+ "moduleBoundary.error.missingAction": "Specify a module-boundary action: check",
1005
+ "moduleBoundary.error.unknownAction": "Unknown module-boundary action: {action}",
1006
+ "moduleBoundary.error.missingPath": "Provide at least one path to check",
1007
+ "moduleBoundary.error.invalidPositiveInteger": "{option} must be a positive safe integer: {value}",
981
1008
  "changeImpact.help.summary": "Analyze git changes and return bounded file-impact, script-pack, and verification hints.",
982
1009
  "changeImpact.help.option.base": "Git base ref to compare from. Default: HEAD",
983
1010
  "changeImpact.help.option.head": "Git head ref to compare to. Omit to compare the base with the working tree.",