mustflow 2.103.15 → 2.103.20

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 (33) 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/script-pack-registry.js +39 -0
  17. package/dist/cli/script-packs/code-module-boundary.js +210 -0
  18. package/dist/core/module-boundary.js +523 -0
  19. package/dist/core/public-json-contracts.js +50 -0
  20. package/dist/core/script-pack-suggestions.js +5 -0
  21. package/package.json +1 -1
  22. package/schemas/README.md +12 -0
  23. package/schemas/check-report.schema.json +52 -0
  24. package/schemas/index-report.schema.json +103 -0
  25. package/schemas/module-boundary-report.schema.json +210 -0
  26. package/schemas/search-report.schema.json +102 -0
  27. package/schemas/status-report.schema.json +50 -0
  28. package/templates/default/i18n.toml +4 -4
  29. package/templates/default/locales/en/.mustflow/skills/database-migration-change/SKILL.md +16 -2
  30. package/templates/default/locales/en/.mustflow/skills/module-boundary-review/SKILL.md +12 -1
  31. package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +8 -6
  32. package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +17 -10
  33. package/templates/default/manifest.toml +1 -1
package/README.md CHANGED
@@ -129,6 +129,7 @@ mustflow installs and validates an agent workflow for user projects.
129
129
  config/schema churn, and broad structural changes before treating added complexity as free.
130
130
  - Lists, suggests, and runs bundled read-only utility scripts through `mf script-pack`, including
131
131
  `code/outline` for source symbol maps, `code/dependency-graph` for bounded relative import graphs,
132
+ `code/module-boundary` for configured import-boundary guardrails,
132
133
  `code/change-impact` for git-diff impact and verification hints, `code/symbol-read` for focused source snippets,
133
134
  `code/route-outline` for Hono, Elysia, Axum, and NestJS route maps, `docs/reference-drift` for stale
134
135
  documentation references, `repo/config-chain` for nearby config inheritance,
@@ -296,6 +297,7 @@ mf run mustflow_update_apply
296
297
  | `mf script-pack suggest --path <path> --phase before_change` | Rank helpers for an explicit path and workflow phase before deciding which script to run. |
297
298
  | `mf script-pack run code/outline scan <path...> --json` | Scan supported source files for symbol headers, line ranges, source anchors, return metadata, and content hashes. |
298
299
  | `mf script-pack run code/dependency-graph scan <path...> --json` | Trace bounded relative import, export, require, and dynamic import edges for TypeScript and JavaScript source files. |
300
+ | `mf script-pack run code/module-boundary check <path...> --json` | Check configured module-boundary import rules, import cycles, public entrypoints, feature imports, and shared budgets. |
299
301
  | `mf script-pack run code/change-impact analyze --base HEAD --json` | Analyze changed files and return bounded impact candidates, script-pack hints, and verification intent hints. |
300
302
  | `mf script-pack run code/symbol-read read <path> --start-line <line> --json` | Read the focused symbol range or bounded source snippet after `code/outline` identifies the relevant location. |
301
303
  | `mf script-pack run code/symbol-read read --anchor <id> --json` | Read the conservative target symbol for a structured `mf:anchor` source marker. |
@@ -0,0 +1,83 @@
1
+ import { renderHelp } from '../../lib/cli-output.js';
2
+ import { t } from '../../lib/i18n.js';
3
+ import { getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../../lib/option-parser.js';
4
+ import { ALLOW_UNTRUSTED_ROOT_OPTION } from '../../lib/run-root-trust.js';
5
+ const DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS = 300;
6
+ const RUN_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--dry-run', kind: 'boolean' },
9
+ { name: '--plan-only', kind: 'boolean' },
10
+ { name: '--wait', kind: 'boolean' },
11
+ { name: ALLOW_UNTRUSTED_ROOT_OPTION, kind: 'boolean' },
12
+ { name: '--wait-timeout', kind: 'string' },
13
+ ];
14
+ export function hasRunHelpOption(args) {
15
+ return hasCliOptionToken(args, '--help', ['-h']);
16
+ }
17
+ export function parseRunArguments(args) {
18
+ const parsed = parseCliOptions(args, RUN_OPTIONS, { allowPositionals: true });
19
+ let waitTimeoutSeconds = DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS;
20
+ if (parsed.error) {
21
+ const [intentName, ...extra] = parsed.positionals;
22
+ return {
23
+ json: hasParsedCliOption(parsed, '--json'),
24
+ dryRun: hasParsedCliOption(parsed, '--dry-run'),
25
+ planOnly: hasParsedCliOption(parsed, '--plan-only'),
26
+ allowUntrustedRoot: hasParsedCliOption(parsed, ALLOW_UNTRUSTED_ROOT_OPTION),
27
+ wait: hasParsedCliOption(parsed, '--wait'),
28
+ waitTimeoutSeconds,
29
+ intentName: intentName ?? null,
30
+ extra,
31
+ error: parsed.error,
32
+ };
33
+ }
34
+ const waitTimeoutValue = getParsedCliStringOption(parsed, '--wait-timeout');
35
+ let error = null;
36
+ if (waitTimeoutValue !== null) {
37
+ const parsedWaitTimeout = Number(waitTimeoutValue);
38
+ if (!Number.isInteger(parsedWaitTimeout) || parsedWaitTimeout <= 0) {
39
+ error = 'invalid_wait_timeout';
40
+ }
41
+ else {
42
+ waitTimeoutSeconds = parsedWaitTimeout;
43
+ }
44
+ }
45
+ const [intentName, ...extra] = parsed.positionals;
46
+ return {
47
+ json: hasParsedCliOption(parsed, '--json'),
48
+ dryRun: hasParsedCliOption(parsed, '--dry-run'),
49
+ planOnly: hasParsedCliOption(parsed, '--plan-only'),
50
+ allowUntrustedRoot: hasParsedCliOption(parsed, ALLOW_UNTRUSTED_ROOT_OPTION),
51
+ wait: hasParsedCliOption(parsed, '--wait'),
52
+ waitTimeoutSeconds,
53
+ intentName: intentName ?? null,
54
+ extra,
55
+ error,
56
+ };
57
+ }
58
+ export function getRunHelp(lang = 'en') {
59
+ return renderHelp({
60
+ usage: 'mf run <intent> [options]',
61
+ summary: t(lang, 'run.help.summary'),
62
+ options: [
63
+ { label: '--dry-run', description: t(lang, 'run.help.option.dryRun') },
64
+ { label: '--plan-only', description: t(lang, 'run.help.option.planOnly') },
65
+ { label: '--json', description: t(lang, 'run.help.option.json') },
66
+ { label: '--wait', description: t(lang, 'run.help.option.wait') },
67
+ { label: '--wait-timeout <seconds>', description: t(lang, 'run.help.option.waitTimeout') },
68
+ { label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
69
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
70
+ ],
71
+ examples: ['mf run test', 'mf run lint --json', 'mf run mustflow_check --dry-run --json'],
72
+ exitCodes: [
73
+ {
74
+ label: '0',
75
+ description: t(lang, 'run.help.exit.ok'),
76
+ },
77
+ {
78
+ label: '1',
79
+ description: t(lang, 'run.help.exit.fail'),
80
+ },
81
+ ],
82
+ }, lang);
83
+ }
@@ -0,0 +1,334 @@
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 { readCommandContract, readMustflowConfigIfExists } from '../../../core/config-loading.js';
6
+ import { createCorrelationId } from '../../../core/correlation-id.js';
7
+ import { recordRunPerformanceHistory } from '../../../core/run-performance-history.js';
8
+ import { RunProfiler } from '../../../core/run-profile.js';
9
+ import { writeRunReceipt, } from '../../../core/run-receipt.js';
10
+ import { finishRunWriteTracking, startRunWriteTracking } from '../../../core/run-write-drift.js';
11
+ import { resolveRunReceiptRetentionPolicy } from '../../../core/retention-policy.js';
12
+ import { renderCliError } from '../../lib/cli-output.js';
13
+ import { t } from '../../lib/i18n.js';
14
+ import { resolveMustflowRoot } from '../../lib/project-root.js';
15
+ import { assessRunRootTrust } from '../../lib/run-root-trust.js';
16
+ import { createRunPlan, createRunPreview, } from '../../lib/run-plan.js';
17
+ import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from './executor.js';
18
+ import { emitOutput, isOutputLimitExceededError } from './output.js';
19
+ import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree } from './process-tree.js';
20
+ import { writeLatestRunProfile } from './profile.js';
21
+ import { assembleRunReceipt } from './receipt.js';
22
+ const ACTIVE_LOCK_WAIT_POLL_MS = 1_000;
23
+ function delay(milliseconds) {
24
+ return new Promise((resolve) => {
25
+ setTimeout(resolve, milliseconds);
26
+ });
27
+ }
28
+ function getRunPlanDetail(plan, lang, fallbackKey) {
29
+ return plan.detail ?? t(lang, fallbackKey);
30
+ }
31
+ function reportRunPlanFailure(plan, reporter, lang) {
32
+ let message;
33
+ switch (plan.reasonCode) {
34
+ case 'status_not_configured':
35
+ message = t(lang, 'run.error.statusNotConfigured', { intent: plan.intentName, status: plan.intentStatus ?? 'unknown' });
36
+ break;
37
+ case 'lifecycle_not_oneshot':
38
+ message = t(lang, 'run.error.lifecycleNotOneshot', { intent: plan.intentName, lifecycle: plan.lifecycle ?? 'unknown' });
39
+ break;
40
+ case 'run_policy_not_agent_allowed':
41
+ message = t(lang, 'run.error.runPolicy', { intent: plan.intentName });
42
+ break;
43
+ case 'stdin_not_closed':
44
+ message = t(lang, 'run.error.stdin', { intent: plan.intentName });
45
+ break;
46
+ case 'agent_shell_requires_allow':
47
+ message = t(lang, 'run.error.agentShellRequiresAllow', { intent: plan.intentName });
48
+ break;
49
+ case 'missing_timeout':
50
+ message = t(lang, 'run.error.timeout', { intent: plan.intentName });
51
+ break;
52
+ case 'missing_command_source':
53
+ message = t(lang, 'run.error.commandSource', { intent: plan.intentName });
54
+ break;
55
+ case 'unsafe_intent_name':
56
+ message = t(lang, 'run.error.unsafeIntent', {
57
+ intent: plan.intentName,
58
+ detail: getRunPlanDetail(plan, lang, 'run.error.unsafeIntentDetail'),
59
+ });
60
+ break;
61
+ case 'blocked_shell_background_pattern':
62
+ message = t(lang, 'run.error.blockedShellBackground', {
63
+ intent: plan.intentName,
64
+ detail: getRunPlanDetail(plan, lang, 'run.error.blockedShellBackgroundDetail'),
65
+ });
66
+ break;
67
+ case 'blocked_long_running_command_pattern':
68
+ message = t(lang, 'run.error.blockedLongRunningCommand', {
69
+ intent: plan.intentName,
70
+ detail: getRunPlanDetail(plan, lang, 'run.error.blockedLongRunningCommandDetail'),
71
+ });
72
+ break;
73
+ case 'network_requires_approval':
74
+ case 'destructive_requires_approval':
75
+ case 'approval_policy_unreadable':
76
+ message = t(lang, 'run.error.approvalRequired', {
77
+ intent: plan.intentName,
78
+ detail: getRunPlanDetail(plan, lang, 'run.error.approvalRequiredDetail'),
79
+ });
80
+ break;
81
+ case 'cwd_outside_project':
82
+ message = t(lang, 'run.error.cwdOutsideProject', {
83
+ intent: plan.intentName,
84
+ detail: getRunPlanDetail(plan, lang, 'run.error.cwdOutsideProjectDetail'),
85
+ });
86
+ break;
87
+ case 'invalid_test_target':
88
+ message = t(lang, 'run.error.invalidTestTarget', {
89
+ intent: plan.intentName,
90
+ detail: getRunPlanDetail(plan, lang, 'run.error.invalidTestTargetDetail'),
91
+ });
92
+ break;
93
+ case 'max_output_bytes_exceeds_limit':
94
+ message = t(lang, 'run.error.maxOutputBytes', {
95
+ intent: plan.intentName,
96
+ detail: getRunPlanDetail(plan, lang, 'run.error.maxOutputBytesDetail'),
97
+ });
98
+ break;
99
+ case 'intent_not_table':
100
+ default:
101
+ message = t(lang, 'run.error.unknownIntent', { intent: plan.intentName });
102
+ break;
103
+ }
104
+ if (plan.suggestedIntentSnippet) {
105
+ message = `${message}\n\n${t(lang, 'run.label.suggestedIntentSnippet')}:\n${plan.suggestedIntentSnippet}`;
106
+ }
107
+ reporter.stderr(renderCliError(message, 'mf help commands', lang));
108
+ }
109
+ function createPlanCommandHash(plan) {
110
+ const payload = {
111
+ mode: plan.mode,
112
+ cwd: plan.relativeCwd,
113
+ argv: plan.commandArgv ?? null,
114
+ cmd: plan.shellCommand ?? null,
115
+ };
116
+ return `sha256:${createHash('sha256').update(JSON.stringify(payload)).digest('hex')}`;
117
+ }
118
+ function renderActiveLockConflictMessage(intentName, conflicts, lang) {
119
+ const [first] = conflicts;
120
+ const detail = first
121
+ ? t(lang, 'run.error.activeLockConflictDetail', {
122
+ lock: first.lock,
123
+ intent: first.conflictsWithIntent,
124
+ pid: first.conflictsWithPid,
125
+ })
126
+ : t(lang, 'run.error.activeLockConflictUnknown');
127
+ return t(lang, 'run.error.activeLockConflict', { intent: intentName, detail });
128
+ }
129
+ async function acquireActiveRunLockWithOptionalWait(input) {
130
+ const startedAt = Date.now();
131
+ let reportedWait = false;
132
+ while (true) {
133
+ const result = acquireActiveRunLock(input.projectRoot, input.contract, input.intentName, { commandHash: input.commandHash });
134
+ if (result.ok || !input.enabled || result.conflicts.length === 0) {
135
+ return result;
136
+ }
137
+ if (!input.json && !reportedWait) {
138
+ const [first] = result.conflicts;
139
+ input.reporter.stderr(t(input.lang, 'run.progress.waitingForActiveLock', {
140
+ intent: input.intentName,
141
+ activeIntent: first?.conflictsWithIntent ?? 'unknown',
142
+ seconds: input.waitTimeoutSeconds,
143
+ }));
144
+ reportedWait = true;
145
+ }
146
+ if (Date.now() - startedAt >= input.waitTimeoutSeconds * 1000) {
147
+ return result;
148
+ }
149
+ await delay(Math.min(ACTIVE_LOCK_WAIT_POLL_MS, Math.max(1, input.waitTimeoutSeconds * 1000 - (Date.now() - startedAt))));
150
+ }
151
+ }
152
+ function createRunProgressReporter(input) {
153
+ if (!input.enabled) {
154
+ return () => undefined;
155
+ }
156
+ input.reporter.stderr(t(input.lang, 'run.progress.started', { intent: input.intentName, seconds: input.timeoutSeconds }));
157
+ const timers = [];
158
+ for (const ratio of [0.5, 0.8]) {
159
+ const delayMs = Math.max(1, Math.floor(input.timeoutSeconds * 1000 * ratio));
160
+ const elapsedSeconds = Math.max(1, Math.round(input.timeoutSeconds * ratio));
161
+ const timer = setTimeout(() => {
162
+ input.reporter.stderr(t(input.lang, 'run.progress.timeoutWarning', {
163
+ intent: input.intentName,
164
+ seconds: elapsedSeconds,
165
+ percent: Math.round(ratio * 100),
166
+ }));
167
+ }, delayMs);
168
+ timer.unref?.();
169
+ timers.push(timer);
170
+ }
171
+ return () => {
172
+ for (const timer of timers) {
173
+ clearTimeout(timer);
174
+ }
175
+ };
176
+ }
177
+ export async function executeRunCommand(request, reporter, lang = 'en', options = {}) {
178
+ const executorStartedAtMs = performance.now();
179
+ const profiler = new RunProfiler();
180
+ const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
181
+ const rootTrust = profiler.measure('root_trust', () => assessRunRootTrust(projectRoot));
182
+ const jsonLikeOutput = request.outputMode !== 'text';
183
+ if (!request.allowUntrustedRoot && !rootTrust.trusted) {
184
+ const message = rootTrust.reason === 'manifest_lock_invalid'
185
+ ? t(lang, 'run.error.untrustedRootInvalid', { detail: rootTrust.detail ?? rootTrust.manifestLockPath })
186
+ : t(lang, 'run.error.untrustedRootMissing', { path: rootTrust.detail ?? rootTrust.manifestLockPath });
187
+ reporter.stderr(renderCliError(message, 'mf run --help', lang));
188
+ return { exitCode: 1, receipt: null };
189
+ }
190
+ const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
191
+ const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, request.intentName, { testTargets: options.testTargets }));
192
+ if (!plan.ok) {
193
+ if (request.outputMode === 'json') {
194
+ reporter.stdout(JSON.stringify(createRunPreview(plan, 'plan-only'), null, 2));
195
+ }
196
+ reportRunPlanFailure(plan, reporter, lang);
197
+ writeLatestRunProfile(profiler, options, {
198
+ projectRoot,
199
+ intent: request.intentName,
200
+ status: 'blocked',
201
+ previewMode: null,
202
+ });
203
+ return { exitCode: 1, receipt: null };
204
+ }
205
+ const activeRunLock = await profiler.measureAsync('active_lock_acquire', () => acquireActiveRunLockWithOptionalWait({
206
+ enabled: request.wait,
207
+ waitTimeoutSeconds: request.waitTimeoutSeconds,
208
+ projectRoot,
209
+ contract,
210
+ intentName: request.intentName,
211
+ commandHash: createPlanCommandHash(plan),
212
+ json: jsonLikeOutput,
213
+ reporter,
214
+ lang,
215
+ }));
216
+ if (!activeRunLock.ok) {
217
+ reporter.stderr(renderCliError(renderActiveLockConflictMessage(request.intentName, activeRunLock.conflicts, lang), 'mf run --dry-run --json', lang));
218
+ writeLatestRunProfile(profiler, options, {
219
+ projectRoot,
220
+ intent: request.intentName,
221
+ status: 'blocked',
222
+ previewMode: null,
223
+ });
224
+ return { exitCode: 1, receipt: null };
225
+ }
226
+ try {
227
+ const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
228
+ const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
229
+ env[ACTIVE_RUN_LOCK_ID_ENV] = activeRunLock.handle.record.run_id;
230
+ const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, request.intentName, {
231
+ additionalDeclaredPaths: options.additionalDeclaredWritePaths,
232
+ env,
233
+ }));
234
+ const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
235
+ const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
236
+ let streamedOutput = false;
237
+ const childStartedAtMs = performance.now();
238
+ const startedAt = new Date();
239
+ const streamChildOutput = request.outputMode === 'text';
240
+ const stopRunProgress = createRunProgressReporter({
241
+ enabled: streamChildOutput && Boolean(reporter.writeStderr),
242
+ intentName: request.intentName,
243
+ timeoutSeconds: plan.timeoutSeconds,
244
+ reporter,
245
+ lang,
246
+ });
247
+ const result = await profiler.measureAsync('child_command', async () => {
248
+ try {
249
+ if (plan.commandArgv) {
250
+ streamedOutput = streamChildOutput;
251
+ return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamChildOutput, true);
252
+ }
253
+ streamedOutput = streamChildOutput;
254
+ return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamChildOutput, true);
255
+ }
256
+ finally {
257
+ stopRunProgress();
258
+ }
259
+ });
260
+ const childDurationMs = performance.now() - childStartedAtMs;
261
+ const finishedAt = new Date();
262
+ const writeDrift = profiler.measure('write_drift_after', () => finishRunWriteTracking(writeTracker));
263
+ const exitCode = typeof result.status === 'number' ? result.status : null;
264
+ const runStatus = getRunStatus(result.error, exitCode, plan.successExitCodes);
265
+ let killMethod = null;
266
+ let termination = null;
267
+ if (runStatus === 'timed_out') {
268
+ termination = result.termination ?? createPendingTimeoutTermination(getKillMethod());
269
+ killMethod = termination.method;
270
+ if (!result.termination && result.pid) {
271
+ terminateProcessTree(result.pid);
272
+ }
273
+ }
274
+ const receipt = profiler.measure('receipt_create', () => assembleRunReceipt({
275
+ correlationId: options.correlationId ?? createCorrelationId('run'),
276
+ intentName: request.intentName,
277
+ runStatus,
278
+ startedAt,
279
+ finishedAt,
280
+ projectRoot,
281
+ plan,
282
+ result,
283
+ exitCode,
284
+ killMethod,
285
+ termination,
286
+ writeDrift,
287
+ executorOverheadMs: Math.max(0, Math.round((performance.now() - executorStartedAtMs - childDurationMs) * 1000) / 1000),
288
+ phaseTimings: profiler.getReceiptPhases(),
289
+ stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
290
+ stderrTailBytes: runReceiptPolicy.stderrTailBytes,
291
+ }));
292
+ if (options.writeLatestReceipt !== false) {
293
+ profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));
294
+ }
295
+ if (options.recordPerformanceHistory !== false) {
296
+ profiler.measure('performance_history_write', () => recordRunPerformanceHistory(projectRoot, receipt));
297
+ }
298
+ writeLatestRunProfile(profiler, options, {
299
+ projectRoot,
300
+ intent: request.intentName,
301
+ status: runStatus,
302
+ previewMode: null,
303
+ });
304
+ const commandExitCode = runStatus === 'passed' ? 0 : 1;
305
+ if (request.outputMode === 'json') {
306
+ reporter.stdout(JSON.stringify(receipt, null, 2));
307
+ return { exitCode: commandExitCode, receipt };
308
+ }
309
+ if (request.outputMode === 'silent') {
310
+ return { exitCode: commandExitCode, receipt };
311
+ }
312
+ if (!streamedOutput) {
313
+ emitOutput(reporter, result.stdout, 'stdout');
314
+ emitOutput(reporter, result.stderr, 'stderr');
315
+ }
316
+ if (result.error) {
317
+ const errorWithCode = result.error;
318
+ if (errorWithCode.code === 'ETIMEDOUT') {
319
+ reporter.stderr(t(lang, 'run.error.timedOut', { intent: request.intentName, seconds: plan.timeoutSeconds }));
320
+ return { exitCode: 1, receipt };
321
+ }
322
+ if (isOutputLimitExceededError(result.error)) {
323
+ reporter.stderr(t(lang, 'run.error.outputLimitExceeded', { intent: request.intentName, message: result.error.message }));
324
+ return { exitCode: 1, receipt };
325
+ }
326
+ reporter.stderr(t(lang, 'run.error.startFailed', { intent: request.intentName, message: result.error.message }));
327
+ return { exitCode: 1, receipt };
328
+ }
329
+ return { exitCode: commandExitCode, receipt };
330
+ }
331
+ finally {
332
+ activeRunLock.handle.release();
333
+ }
334
+ }
@@ -0,0 +1,29 @@
1
+ import { readCommandContract } from '../../../core/config-loading.js';
2
+ import { RunProfiler } from '../../../core/run-profile.js';
3
+ import { resolveMustflowRoot } from '../../lib/project-root.js';
4
+ import { createRunPlan, createRunPreview, renderRunPreviewText, } from '../../lib/run-plan.js';
5
+ import { writeLatestRunProfile } from './profile.js';
6
+ export function getRunPreviewMode(input) {
7
+ return input.dryRun ? 'dry-run' : input.planOnly ? 'plan-only' : null;
8
+ }
9
+ export function executeRunPreviewCommand(input, reporter, lang, options) {
10
+ const profiler = new RunProfiler();
11
+ const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
12
+ const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
13
+ const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, input.intentName, { testTargets: options.testTargets }));
14
+ profiler.measure('preview_render', () => {
15
+ if (input.json) {
16
+ reporter.stdout(JSON.stringify(createRunPreview(plan, input.previewMode), null, 2));
17
+ }
18
+ else {
19
+ reporter.stdout(renderRunPreviewText(plan, input.previewMode, lang));
20
+ }
21
+ });
22
+ writeLatestRunProfile(profiler, options, {
23
+ projectRoot,
24
+ intent: input.intentName,
25
+ status: plan.ok ? 'previewed' : 'blocked',
26
+ previewMode: input.previewMode,
27
+ });
28
+ return plan.ok ? 0 : 1;
29
+ }
@@ -0,0 +1,6 @@
1
+ export function writeLatestRunProfile(profiler, options, input) {
2
+ if (options.writeLatestProfile === false) {
3
+ return;
4
+ }
5
+ profiler.writeLatest(input);
6
+ }