mustflow 1.30.0 → 1.31.0
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.
- package/README.md +12 -2
- package/dist/cli/commands/run.js +221 -48
- package/dist/cli/commands/upgrade.js +65 -0
- package/dist/cli/commands/verify.js +79 -7
- package/dist/cli/i18n/en.js +12 -0
- package/dist/cli/i18n/es.js +12 -0
- package/dist/cli/i18n/fr.js +12 -0
- package/dist/cli/i18n/hi.js +12 -0
- package/dist/cli/i18n/ko.js +12 -0
- package/dist/cli/i18n/zh.js +12 -0
- package/dist/cli/index.js +27 -46
- package/dist/cli/lib/command-registry.js +5 -0
- package/dist/cli/lib/dashboard-html.js +1 -1
- package/dist/cli/lib/local-index.js +11 -8
- package/dist/cli/lib/reporter.js +6 -0
- package/dist/cli/lib/run-plan.js +20 -3
- package/dist/cli/lib/validation.js +110 -1
- package/dist/core/bounded-output.js +38 -0
- package/dist/core/change-classification.js +6 -2
- package/dist/core/change-verification.js +240 -6
- package/dist/core/check-issues.js +6 -0
- package/dist/core/command-contract-validation.js +20 -0
- package/dist/core/command-effects.js +13 -0
- package/dist/core/contract-lint.js +95 -1
- package/dist/core/dashboard-verification.js +8 -0
- package/dist/core/public-json-contracts.js +7 -0
- package/dist/core/run-performance-history.js +307 -0
- package/dist/core/run-profile.js +87 -0
- package/dist/core/run-receipt.js +171 -4
- package/dist/core/run-write-drift.js +18 -2
- package/dist/core/skill-route-alignment.js +90 -0
- package/dist/core/test-selection.js +224 -0
- package/dist/core/verification-decision-graph.js +67 -0
- package/dist/core/verification-scheduler.js +96 -2
- package/package.json +1 -1
- package/schemas/README.md +6 -2
- package/schemas/change-verification-report.schema.json +153 -3
- package/schemas/commands.schema.json +47 -1
- package/schemas/contract-lint-report.schema.json +51 -0
- package/schemas/dashboard-export.schema.json +273 -0
- package/schemas/explain-report.schema.json +2 -0
- package/schemas/run-receipt.schema.json +109 -0
- package/templates/default/common/.mustflow/config/commands.toml +1 -1
- package/templates/default/manifest.toml +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Languages: [English](README.md) · [한국어](docs/i18n/ko/README.md) · [中文](docs/i18n/zh/README.md) · [Español](docs/i18n/es/README.md) · [Français](docs/i18n/fr/README.md) · [हिन्दी](docs/i18n/hi/README.md)
|
|
4
4
|
|
|
5
|
-
mustflow is a
|
|
5
|
+
mustflow is a repository-local work contract and verification CLI for LLM coding agents. It keeps agents inside explicit read, command, and verification boundaries without replacing the host agent's sandbox, approval, checkpoint, model, or tool policies.
|
|
6
6
|
|
|
7
|
-
The core concept is straightforward: place `AGENTS.md` at the project root and keep detailed workflows under `.mustflow/`. Agents start from `AGENTS.md
|
|
7
|
+
The core concept is straightforward: place `AGENTS.md` at the project root and keep detailed workflows under `.mustflow/`. Agents start from `AGENTS.md`, then follow the repository command contract, skills, project context, and verification rules in sequence.
|
|
8
8
|
|
|
9
9
|
- Documentation site: <https://0disoft.github.io/mustflow/>
|
|
10
10
|
- Human-readable project examples: [`examples/`](examples/)
|
|
@@ -268,6 +268,14 @@ npx mf update --dry-run
|
|
|
268
268
|
npx mf update --apply
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
+
After updating the mustflow package, `mf upgrade` combines the package freshness check with the safe project-file update step. It does not install packages by itself; update npm, pnpm, or Bun first.
|
|
272
|
+
|
|
273
|
+
```sh
|
|
274
|
+
bun update -g mustflow
|
|
275
|
+
mf upgrade --dry-run
|
|
276
|
+
mf upgrade
|
|
277
|
+
```
|
|
278
|
+
|
|
271
279
|
Agents should prefer the configured update intents so the repository receives a run receipt.
|
|
272
280
|
|
|
273
281
|
```sh
|
|
@@ -304,6 +312,8 @@ mf run mustflow_update_apply
|
|
|
304
312
|
| `mf status` | Inspect installed state and changed or missing files. |
|
|
305
313
|
| `mf update --dry-run` | Calculate a template update plan without writing files. |
|
|
306
314
|
| `mf update --apply` | Apply template updates when nothing is blocked. |
|
|
315
|
+
| `mf upgrade` | Check package freshness, then apply safe bundled template updates when the package is current. |
|
|
316
|
+
| `mf upgrade --dry-run` | Check package freshness and print the safe project update plan without writing files. |
|
|
307
317
|
| `mf help <topic>` | Show installed mustflow help. |
|
|
308
318
|
| `mf dashboard` | Start a local inspection dashboard for status, verification recommendations, release/version-source status, template update readiness, latest run receipt, skill routes, safe preferences, and documentation review. Use `--export-json <path>` or `--export <path>` for a bounded static report. It does not execute commands or apply fixes. |
|
|
309
319
|
| `mf version` | Print the installed mustflow package version. |
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import { runClassify } from './classify.js';
|
|
4
|
-
import { runContext } from './context.js';
|
|
5
|
-
import { runDoctor } from './doctor.js';
|
|
6
|
-
import { runHelp } from './help.js';
|
|
7
|
-
import { runImpact } from './impact.js';
|
|
8
|
-
import { runLineEndings } from './line-endings.js';
|
|
9
|
-
import { runMap } from './map.js';
|
|
10
|
-
import { runStatus } from './status.js';
|
|
11
|
-
import { runUpdate } from './update.js';
|
|
12
|
-
import { runVersionSources } from './version-sources.js';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
13
3
|
import { canRunMustflowBuiltinInProcess, isMustflowBinName } from '../../core/command-classification.js';
|
|
14
4
|
import { createCommandEnv } from '../../core/command-env.js';
|
|
5
|
+
import { BoundedOutputBuffer } from '../../core/bounded-output.js';
|
|
15
6
|
import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
|
|
16
7
|
import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
|
|
17
8
|
import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
|
|
@@ -20,12 +11,14 @@ import { getPackageVersion } from '../lib/package-info.js';
|
|
|
20
11
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
21
12
|
import { createRunPlan, createRunPreview, isMustflowBuiltinIntent, renderRunPreviewText, } from '../lib/run-plan.js';
|
|
22
13
|
import { createRunReceipt, writeRunReceipt } from '../../core/run-receipt.js';
|
|
14
|
+
import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
|
|
15
|
+
import { RunProfiler } from '../../core/run-profile.js';
|
|
23
16
|
import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
|
|
24
17
|
function emitOutput(reporter, output, stream) {
|
|
25
18
|
if (!output) {
|
|
26
19
|
return;
|
|
27
20
|
}
|
|
28
|
-
const text = output.toString().trimEnd();
|
|
21
|
+
const text = (typeof output === 'object' && 'tail' in output ? output.tail : output.toString()).trimEnd();
|
|
29
22
|
if (text.length === 0) {
|
|
30
23
|
return;
|
|
31
24
|
}
|
|
@@ -94,37 +87,37 @@ async function runKnownBuiltinCommand(args, reporter, lang) {
|
|
|
94
87
|
return undefined;
|
|
95
88
|
}
|
|
96
89
|
if (command === 'check') {
|
|
97
|
-
return runCheck(commandArgs, reporter, lang);
|
|
90
|
+
return (await import('./check.js')).runCheck(commandArgs, reporter, lang);
|
|
98
91
|
}
|
|
99
92
|
if (command === 'classify') {
|
|
100
|
-
return runClassify(commandArgs, reporter, lang);
|
|
93
|
+
return (await import('./classify.js')).runClassify(commandArgs, reporter, lang);
|
|
101
94
|
}
|
|
102
95
|
if (command === 'context') {
|
|
103
|
-
return runContext(commandArgs, reporter, lang);
|
|
96
|
+
return (await import('./context.js')).runContext(commandArgs, reporter, lang);
|
|
104
97
|
}
|
|
105
98
|
if (command === 'doctor') {
|
|
106
|
-
return runDoctor(commandArgs, reporter, lang);
|
|
99
|
+
return (await import('./doctor.js')).runDoctor(commandArgs, reporter, lang);
|
|
107
100
|
}
|
|
108
101
|
if (command === 'help') {
|
|
109
|
-
return runHelp(commandArgs, reporter, lang);
|
|
102
|
+
return (await import('./help.js')).runHelp(commandArgs, reporter, lang);
|
|
110
103
|
}
|
|
111
104
|
if (command === 'impact') {
|
|
112
|
-
return runImpact(commandArgs, reporter, lang);
|
|
105
|
+
return (await import('./impact.js')).runImpact(commandArgs, reporter, lang);
|
|
113
106
|
}
|
|
114
107
|
if (command === 'line-endings') {
|
|
115
|
-
return runLineEndings(commandArgs, reporter, lang);
|
|
108
|
+
return (await import('./line-endings.js')).runLineEndings(commandArgs, reporter, lang);
|
|
116
109
|
}
|
|
117
110
|
if (command === 'map') {
|
|
118
|
-
return runMap(commandArgs, reporter, lang);
|
|
111
|
+
return (await import('./map.js')).runMap(commandArgs, reporter, lang);
|
|
119
112
|
}
|
|
120
113
|
if (command === 'status') {
|
|
121
|
-
return runStatus(commandArgs, reporter, lang);
|
|
114
|
+
return (await import('./status.js')).runStatus(commandArgs, reporter, lang);
|
|
122
115
|
}
|
|
123
116
|
if (command === 'update') {
|
|
124
|
-
return runUpdate(commandArgs, reporter, lang);
|
|
117
|
+
return (await import('./update.js')).runUpdate(commandArgs, reporter, lang);
|
|
125
118
|
}
|
|
126
119
|
if (command === 'version-sources') {
|
|
127
|
-
return runVersionSources(commandArgs, reporter, lang);
|
|
120
|
+
return (await import('./version-sources.js')).runVersionSources(commandArgs, reporter, lang);
|
|
128
121
|
}
|
|
129
122
|
return undefined;
|
|
130
123
|
}
|
|
@@ -179,6 +172,76 @@ function runArgvCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
|
|
|
179
172
|
windowsHide: true,
|
|
180
173
|
});
|
|
181
174
|
}
|
|
175
|
+
function writeStreamChunk(reporter, stream, chunk) {
|
|
176
|
+
if (stream === 'stdout') {
|
|
177
|
+
if (reporter.writeStdout) {
|
|
178
|
+
reporter.writeStdout(chunk);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
reporter.stdout(chunk.toString().trimEnd());
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (reporter.writeStderr) {
|
|
185
|
+
reporter.writeStderr(chunk);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
reporter.stderr(chunk.toString().trimEnd());
|
|
189
|
+
}
|
|
190
|
+
function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
const stdout = new BoundedOutputBuffer(stdoutTailBytes);
|
|
193
|
+
const stderr = new BoundedOutputBuffer(stderrTailBytes);
|
|
194
|
+
let settled = false;
|
|
195
|
+
let timedOut = false;
|
|
196
|
+
let childError;
|
|
197
|
+
let childPid;
|
|
198
|
+
let timeout;
|
|
199
|
+
const child = spawn(command?.executable ?? '', command?.args ?? [], {
|
|
200
|
+
cwd,
|
|
201
|
+
env,
|
|
202
|
+
shell: command?.shell ?? false,
|
|
203
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
204
|
+
windowsHide: true,
|
|
205
|
+
detached: process.platform !== 'win32',
|
|
206
|
+
});
|
|
207
|
+
childPid = child.pid;
|
|
208
|
+
const finish = (status, signal) => {
|
|
209
|
+
if (settled) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
settled = true;
|
|
213
|
+
if (timeout) {
|
|
214
|
+
clearTimeout(timeout);
|
|
215
|
+
}
|
|
216
|
+
resolve({
|
|
217
|
+
status,
|
|
218
|
+
signal,
|
|
219
|
+
error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
|
|
220
|
+
stdout: stdout.toSnapshot(),
|
|
221
|
+
stderr: stderr.toSnapshot(),
|
|
222
|
+
pid: childPid,
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
child.stdout?.on('data', (chunk) => {
|
|
226
|
+
stdout.append(chunk);
|
|
227
|
+
writeStreamChunk(reporter, 'stdout', chunk);
|
|
228
|
+
});
|
|
229
|
+
child.stderr?.on('data', (chunk) => {
|
|
230
|
+
stderr.append(chunk);
|
|
231
|
+
writeStreamChunk(reporter, 'stderr', chunk);
|
|
232
|
+
});
|
|
233
|
+
child.once('error', (error) => {
|
|
234
|
+
childError = error;
|
|
235
|
+
});
|
|
236
|
+
child.once('close', (status, signal) => {
|
|
237
|
+
finish(status, signal);
|
|
238
|
+
});
|
|
239
|
+
timeout = setTimeout(() => {
|
|
240
|
+
timedOut = true;
|
|
241
|
+
terminateProcessTree(childPid);
|
|
242
|
+
}, timeoutSeconds * 1000);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
182
245
|
function runShellCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
|
|
183
246
|
return spawnSync(command ?? '', {
|
|
184
247
|
cwd,
|
|
@@ -192,6 +255,61 @@ function runShellCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
|
|
|
192
255
|
windowsHide: true,
|
|
193
256
|
});
|
|
194
257
|
}
|
|
258
|
+
function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
|
|
259
|
+
return new Promise((resolve) => {
|
|
260
|
+
const stdout = new BoundedOutputBuffer(stdoutTailBytes);
|
|
261
|
+
const stderr = new BoundedOutputBuffer(stderrTailBytes);
|
|
262
|
+
let settled = false;
|
|
263
|
+
let timedOut = false;
|
|
264
|
+
let childError;
|
|
265
|
+
let childPid;
|
|
266
|
+
let timeout;
|
|
267
|
+
const child = spawn(command ?? '', {
|
|
268
|
+
cwd,
|
|
269
|
+
env,
|
|
270
|
+
shell: true,
|
|
271
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
272
|
+
windowsHide: true,
|
|
273
|
+
detached: process.platform !== 'win32',
|
|
274
|
+
});
|
|
275
|
+
childPid = child.pid;
|
|
276
|
+
const finish = (status, signal) => {
|
|
277
|
+
if (settled) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
settled = true;
|
|
281
|
+
if (timeout) {
|
|
282
|
+
clearTimeout(timeout);
|
|
283
|
+
}
|
|
284
|
+
resolve({
|
|
285
|
+
status,
|
|
286
|
+
signal,
|
|
287
|
+
error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
|
|
288
|
+
stdout: stdout.toSnapshot(),
|
|
289
|
+
stderr: stderr.toSnapshot(),
|
|
290
|
+
pid: childPid,
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
child.stdout?.on('data', (chunk) => {
|
|
294
|
+
stdout.append(chunk);
|
|
295
|
+
writeStreamChunk(reporter, 'stdout', chunk);
|
|
296
|
+
});
|
|
297
|
+
child.stderr?.on('data', (chunk) => {
|
|
298
|
+
stderr.append(chunk);
|
|
299
|
+
writeStreamChunk(reporter, 'stderr', chunk);
|
|
300
|
+
});
|
|
301
|
+
child.once('error', (error) => {
|
|
302
|
+
childError = error;
|
|
303
|
+
});
|
|
304
|
+
child.once('close', (status, signal) => {
|
|
305
|
+
finish(status, signal);
|
|
306
|
+
});
|
|
307
|
+
timeout = setTimeout(() => {
|
|
308
|
+
timedOut = true;
|
|
309
|
+
terminateProcessTree(childPid);
|
|
310
|
+
}, timeoutSeconds * 1000);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
195
313
|
function getRunStatus(error, exitCode, successExitCodes) {
|
|
196
314
|
const errorWithCode = error;
|
|
197
315
|
if (errorWithCode?.code === 'ETIMEDOUT') {
|
|
@@ -281,7 +399,9 @@ export function getRunHelp(lang = 'en') {
|
|
|
281
399
|
* invariant: Execution requires configured status, oneshot lifecycle, agent_allowed policy, and closed stdin.
|
|
282
400
|
* risk: config, security, state
|
|
283
401
|
*/
|
|
284
|
-
export async function runRun(args, reporter, lang = 'en') {
|
|
402
|
+
export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
403
|
+
const executorStartedAtMs = performance.now();
|
|
404
|
+
const profiler = new RunProfiler();
|
|
285
405
|
if (args.includes('--help') || args.includes('-h')) {
|
|
286
406
|
reporter.stdout(getRunHelp(lang));
|
|
287
407
|
return 0;
|
|
@@ -310,36 +430,69 @@ export async function runRun(args, reporter, lang = 'en') {
|
|
|
310
430
|
printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: extra[0] }), 'mf run --help', getRunHelp(lang), lang);
|
|
311
431
|
return 1;
|
|
312
432
|
}
|
|
313
|
-
const projectRoot = resolveMustflowRoot();
|
|
314
|
-
const contract = readCommandContract(projectRoot);
|
|
315
|
-
const plan = createRunPlan(projectRoot, contract, intentName);
|
|
433
|
+
const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
|
|
434
|
+
const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
|
|
435
|
+
const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, intentName, { testTargets: options.testTargets }));
|
|
316
436
|
if (previewMode) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
437
|
+
profiler.measure('preview_render', () => {
|
|
438
|
+
if (json) {
|
|
439
|
+
reporter.stdout(JSON.stringify(createRunPreview(plan, previewMode), null, 2));
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
reporter.stdout(renderRunPreviewText(plan, previewMode));
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
profiler.writeLatest({
|
|
446
|
+
projectRoot,
|
|
447
|
+
intent: intentName,
|
|
448
|
+
status: plan.ok ? 'previewed' : 'blocked',
|
|
449
|
+
previewMode,
|
|
450
|
+
});
|
|
323
451
|
return plan.ok ? 0 : 1;
|
|
324
452
|
}
|
|
325
453
|
if (!plan.ok) {
|
|
326
454
|
reportRunPlanFailure(plan, reporter, lang);
|
|
455
|
+
profiler.writeLatest({
|
|
456
|
+
projectRoot,
|
|
457
|
+
intent: intentName,
|
|
458
|
+
status: 'blocked',
|
|
459
|
+
previewMode: null,
|
|
460
|
+
});
|
|
327
461
|
return 1;
|
|
328
462
|
}
|
|
329
|
-
const runReceiptPolicy = resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot));
|
|
330
|
-
const env = createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist });
|
|
463
|
+
const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
|
|
464
|
+
const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
|
|
331
465
|
const lifecycleValue = plan.lifecycle ?? 'oneshot';
|
|
332
466
|
const runPolicyValue = plan.runPolicy ?? 'agent_allowed';
|
|
333
|
-
const writeTracker = startRunWriteTracking(projectRoot, contract, intentName);
|
|
467
|
+
const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName));
|
|
468
|
+
const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
|
|
469
|
+
const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
|
|
470
|
+
let streamedOutput = false;
|
|
471
|
+
const childStartedAtMs = performance.now();
|
|
334
472
|
const startedAt = new Date();
|
|
335
|
-
const result =
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
473
|
+
const result = await profiler.measureAsync('child_command', async () => {
|
|
474
|
+
if (plan.commandArgv && isMustflowBuiltinIntent(plan.intent)) {
|
|
475
|
+
const builtinResult = await runBuiltinArgvInProcess(plan.commandArgv, plan.cwd, lang);
|
|
476
|
+
if (builtinResult) {
|
|
477
|
+
return builtinResult;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (plan.commandArgv) {
|
|
481
|
+
if (!json) {
|
|
482
|
+
streamedOutput = true;
|
|
483
|
+
return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
|
|
484
|
+
}
|
|
485
|
+
return runArgvCommand(plan.argvCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
|
|
486
|
+
}
|
|
487
|
+
if (!json) {
|
|
488
|
+
streamedOutput = true;
|
|
489
|
+
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
|
|
490
|
+
}
|
|
491
|
+
return runShellCommand(plan.shellCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
|
|
492
|
+
});
|
|
493
|
+
const childDurationMs = performance.now() - childStartedAtMs;
|
|
341
494
|
const finishedAt = new Date();
|
|
342
|
-
const writeDrift = finishRunWriteTracking(writeTracker);
|
|
495
|
+
const writeDrift = profiler.measure('write_drift_after', () => finishRunWriteTracking(writeTracker));
|
|
343
496
|
const exitCode = typeof result.status === 'number' ? result.status : null;
|
|
344
497
|
const runStatus = getRunStatus(result.error, exitCode, plan.successExitCodes);
|
|
345
498
|
let killMethod = null;
|
|
@@ -347,7 +500,7 @@ export async function runRun(args, reporter, lang = 'en') {
|
|
|
347
500
|
killMethod = getKillMethod();
|
|
348
501
|
terminateProcessTree(result.pid);
|
|
349
502
|
}
|
|
350
|
-
const receipt = createRunReceipt({
|
|
503
|
+
const receipt = profiler.measure('receipt_create', () => createRunReceipt({
|
|
351
504
|
intent: intentName,
|
|
352
505
|
status: runStatus,
|
|
353
506
|
timedOut: runStatus === 'timed_out',
|
|
@@ -372,16 +525,36 @@ export async function runRun(args, reporter, lang = 'en') {
|
|
|
372
525
|
stdout: result.stdout,
|
|
373
526
|
stderr: result.stderr,
|
|
374
527
|
writeDrift,
|
|
528
|
+
executorOverheadMs: Math.max(0, Math.round((performance.now() - executorStartedAtMs - childDurationMs) * 1000) / 1000),
|
|
529
|
+
phaseTimings: profiler.getReceiptPhases(),
|
|
530
|
+
selectionSummary: {
|
|
531
|
+
strategy: plan.testTargets.length > 0 ? 'project_test_selection' : 'direct_intent',
|
|
532
|
+
changed_file_count: 0,
|
|
533
|
+
changed_surface_counts: {},
|
|
534
|
+
selected_target_count: Math.max(1, plan.testTargets.length),
|
|
535
|
+
fallback_used: false,
|
|
536
|
+
},
|
|
375
537
|
stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
|
|
376
538
|
stderrTailBytes: runReceiptPolicy.stderrTailBytes,
|
|
539
|
+
}));
|
|
540
|
+
if (options.writeLatestReceipt !== false) {
|
|
541
|
+
profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));
|
|
542
|
+
}
|
|
543
|
+
profiler.measure('performance_history_write', () => recordRunPerformanceHistory(projectRoot, receipt));
|
|
544
|
+
profiler.writeLatest({
|
|
545
|
+
projectRoot,
|
|
546
|
+
intent: intentName,
|
|
547
|
+
status: runStatus,
|
|
548
|
+
previewMode: null,
|
|
377
549
|
});
|
|
378
|
-
writeRunReceipt(projectRoot, receipt);
|
|
379
550
|
if (json) {
|
|
380
551
|
reporter.stdout(JSON.stringify(receipt, null, 2));
|
|
381
552
|
return runStatus === 'passed' ? 0 : 1;
|
|
382
553
|
}
|
|
383
|
-
|
|
384
|
-
|
|
554
|
+
if (!streamedOutput) {
|
|
555
|
+
emitOutput(reporter, result.stdout, 'stdout');
|
|
556
|
+
emitOutput(reporter, result.stderr, 'stderr');
|
|
557
|
+
}
|
|
385
558
|
if (result.error) {
|
|
386
559
|
const errorWithCode = result.error;
|
|
387
560
|
if (errorWithCode.code === 'ETIMEDOUT') {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
|
|
2
|
+
import { t } from '../lib/i18n.js';
|
|
3
|
+
import { checkNpmLatestVersion } from '../lib/npm-version-check.js';
|
|
4
|
+
import { readPackageMetadata } from '../lib/package-info.js';
|
|
5
|
+
import { runUpdate } from './update.js';
|
|
6
|
+
export function getUpgradeHelp(lang = 'en') {
|
|
7
|
+
return renderHelp({
|
|
8
|
+
usage: 'mf upgrade [options]',
|
|
9
|
+
summary: t(lang, 'upgrade.help.summary'),
|
|
10
|
+
options: [
|
|
11
|
+
{ label: '--dry-run', description: t(lang, 'upgrade.help.option.dryRun') },
|
|
12
|
+
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
13
|
+
],
|
|
14
|
+
examples: ['mf upgrade', 'mf upgrade --dry-run'],
|
|
15
|
+
exitCodes: [
|
|
16
|
+
{ label: '0', description: t(lang, 'upgrade.help.exit.ok') },
|
|
17
|
+
{ label: '1', description: t(lang, 'upgrade.help.exit.fail') },
|
|
18
|
+
],
|
|
19
|
+
}, lang);
|
|
20
|
+
}
|
|
21
|
+
function printPackageCheck(check, reporter, lang) {
|
|
22
|
+
reporter.stdout(`${check.packageName} ${check.currentVersion}`);
|
|
23
|
+
reporter.stdout(check.updateAvailable
|
|
24
|
+
? t(lang, 'version.check.latestAvailable', { version: check.latestVersion })
|
|
25
|
+
: t(lang, 'version.check.upToDate', { version: check.latestVersion }));
|
|
26
|
+
if (check.updateAvailable) {
|
|
27
|
+
reporter.stdout('');
|
|
28
|
+
reporter.stdout(t(lang, 'version.check.updateCommand'));
|
|
29
|
+
reporter.stdout(check.updateCommand);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function runUpgrade(args, reporter, lang = 'en') {
|
|
33
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
34
|
+
reporter.stdout(getUpgradeHelp(lang));
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
const supported = new Set(['--dry-run']);
|
|
38
|
+
const unsupported = args.filter((arg) => !supported.has(arg));
|
|
39
|
+
if (unsupported.length > 0) {
|
|
40
|
+
printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf upgrade --help', getUpgradeHelp(lang), lang);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
const dryRun = args.includes('--dry-run');
|
|
44
|
+
reporter.stdout(t(lang, 'upgrade.title'));
|
|
45
|
+
reporter.stdout('');
|
|
46
|
+
reporter.stdout(t(lang, 'upgrade.packageSection'));
|
|
47
|
+
try {
|
|
48
|
+
const packageCheck = await checkNpmLatestVersion(readPackageMetadata());
|
|
49
|
+
printPackageCheck(packageCheck, reporter, lang);
|
|
50
|
+
if (packageCheck.updateAvailable) {
|
|
51
|
+
reporter.stdout('');
|
|
52
|
+
reporter.stdout(t(lang, 'upgrade.packageUpdateRequired'));
|
|
53
|
+
reporter.stdout(t(lang, 'upgrade.noFilesWritten'));
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
reporter.stderr(renderCliError(t(lang, 'upgrade.warning.versionCheckFailed', { message }), 'mf version --check', lang));
|
|
60
|
+
reporter.stderr(t(lang, 'upgrade.warning.continueWithBundledTemplate'));
|
|
61
|
+
}
|
|
62
|
+
reporter.stdout('');
|
|
63
|
+
reporter.stdout(t(lang, 'upgrade.projectSection'));
|
|
64
|
+
return runUpdate([dryRun ? '--dry-run' : '--apply'], reporter, lang);
|
|
65
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createClassifyOutput } from './classify.js';
|
|
4
4
|
import { runRun } from './run.js';
|
|
@@ -9,6 +9,8 @@ import { t } from '../lib/i18n.js';
|
|
|
9
9
|
import { readLocalCommandEffectGraph, readLocalPathSurfaces, } from '../lib/local-index.js';
|
|
10
10
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
11
11
|
const VERIFY_SCHEMA_VERSION = '1';
|
|
12
|
+
const VERIFY_RUN_DIR = path.join('.mustflow', 'state', 'runs', 'verify-latest');
|
|
13
|
+
const LATEST_RUN_RECEIPT_PATH = path.join('.mustflow', 'state', 'runs', 'latest.json');
|
|
12
14
|
function createBufferedOutput() {
|
|
13
15
|
const stdout = [];
|
|
14
16
|
const stderr = [];
|
|
@@ -139,6 +141,13 @@ function parseVerifyArgs(args) {
|
|
|
139
141
|
function uniqueStrings(values) {
|
|
140
142
|
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
|
|
141
143
|
}
|
|
144
|
+
function toPosixPath(value) {
|
|
145
|
+
return value.split(path.sep).join('/');
|
|
146
|
+
}
|
|
147
|
+
function sanitizeIntentFilePart(value) {
|
|
148
|
+
const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
149
|
+
return sanitized.length > 0 ? sanitized.slice(0, 80) : 'intent';
|
|
150
|
+
}
|
|
142
151
|
function readStringArray(value) {
|
|
143
152
|
if (!Array.isArray(value)) {
|
|
144
153
|
return [];
|
|
@@ -343,13 +352,17 @@ function candidateResultKey(candidate) {
|
|
|
343
352
|
? `intent:${candidate.intent}`
|
|
344
353
|
: `missing:${candidate.reason}:${candidate.skipReason ?? ''}:${candidate.detail ?? ''}`;
|
|
345
354
|
}
|
|
346
|
-
function createSkippedResults(candidates, scheduledIntents) {
|
|
355
|
+
function createSkippedResults(candidates, scheduledIntents, gaps) {
|
|
347
356
|
const seen = new Set();
|
|
348
357
|
const results = [];
|
|
358
|
+
const activeGapReasons = new Set(gaps.map((gap) => gap.reason));
|
|
349
359
|
for (const candidate of candidates) {
|
|
350
360
|
if (candidate.status === 'runnable' || (candidate.intent && scheduledIntents.has(candidate.intent))) {
|
|
351
361
|
continue;
|
|
352
362
|
}
|
|
363
|
+
if (candidate.candidateState === 'gap' && !activeGapReasons.has(candidate.reason)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
353
366
|
const key = candidateResultKey(candidate);
|
|
354
367
|
if (seen.has(key)) {
|
|
355
368
|
continue;
|
|
@@ -363,9 +376,19 @@ function createSkippedResults(candidates, scheduledIntents) {
|
|
|
363
376
|
}
|
|
364
377
|
return results;
|
|
365
378
|
}
|
|
366
|
-
|
|
379
|
+
function testTargetsByScheduledIntent(report) {
|
|
380
|
+
return new Map(report.test_selection.selected
|
|
381
|
+
.filter((candidate) => candidate.status === 'runnable' &&
|
|
382
|
+
candidate.testTargetsApplied &&
|
|
383
|
+
candidate.appliedTestTargets.length > 0)
|
|
384
|
+
.map((candidate) => [candidate.intent, candidate.appliedTestTargets]));
|
|
385
|
+
}
|
|
386
|
+
async function runVerificationIntent(intent, lang, testTargets = []) {
|
|
367
387
|
const output = createBufferedOutput();
|
|
368
|
-
const exitCode = await runRun([intent, '--json'], output.reporter, lang
|
|
388
|
+
const exitCode = await runRun([intent, '--json'], output.reporter, lang, {
|
|
389
|
+
writeLatestReceipt: false,
|
|
390
|
+
testTargets,
|
|
391
|
+
});
|
|
369
392
|
const rawStdout = output.stdout().trim();
|
|
370
393
|
let receipt = null;
|
|
371
394
|
let status = exitCode === 0 ? 'passed' : 'failed';
|
|
@@ -419,17 +442,64 @@ function getVerificationStatus(summary) {
|
|
|
419
442
|
}
|
|
420
443
|
return 'passed';
|
|
421
444
|
}
|
|
445
|
+
function writeVerifyRunReceipts(projectRoot, output) {
|
|
446
|
+
const runDir = path.join(projectRoot, VERIFY_RUN_DIR);
|
|
447
|
+
const intentDir = path.join(runDir, 'intents');
|
|
448
|
+
const receipts = [];
|
|
449
|
+
rmSync(runDir, { recursive: true, force: true });
|
|
450
|
+
mkdirSync(intentDir, { recursive: true });
|
|
451
|
+
for (const [index, result] of output.results.entries()) {
|
|
452
|
+
let receiptPath = null;
|
|
453
|
+
if (result.intent && result.receipt) {
|
|
454
|
+
const fileName = `${String(index + 1).padStart(3, '0')}-${sanitizeIntentFilePart(result.intent)}.json`;
|
|
455
|
+
const absoluteReceiptPath = path.join(intentDir, fileName);
|
|
456
|
+
receiptPath = toPosixPath(path.join(VERIFY_RUN_DIR, 'intents', fileName));
|
|
457
|
+
writeFileSync(absoluteReceiptPath, `${JSON.stringify({ ...result.receipt, receipt_path: receiptPath }, null, 2)}\n`, 'utf8');
|
|
458
|
+
}
|
|
459
|
+
receipts.push({
|
|
460
|
+
intent: result.intent,
|
|
461
|
+
status: result.status,
|
|
462
|
+
skipped: result.skipped,
|
|
463
|
+
receipt_path: receiptPath,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
const manifest = {
|
|
467
|
+
schema_version: '1',
|
|
468
|
+
command: 'verify',
|
|
469
|
+
reason: output.reason,
|
|
470
|
+
reasons: output.reasons,
|
|
471
|
+
plan_source: output.plan_source,
|
|
472
|
+
status: output.status,
|
|
473
|
+
summary: output.summary,
|
|
474
|
+
receipts,
|
|
475
|
+
};
|
|
476
|
+
writeFileSync(path.join(runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
477
|
+
const latest = {
|
|
478
|
+
schema_version: '1',
|
|
479
|
+
command: 'verify',
|
|
480
|
+
kind: 'verify_run_summary',
|
|
481
|
+
reason: output.reason,
|
|
482
|
+
reasons: output.reasons,
|
|
483
|
+
plan_source: output.plan_source,
|
|
484
|
+
status: output.status,
|
|
485
|
+
summary: output.summary,
|
|
486
|
+
run_dir: toPosixPath(VERIFY_RUN_DIR),
|
|
487
|
+
manifest_path: toPosixPath(path.join(VERIFY_RUN_DIR, 'manifest.json')),
|
|
488
|
+
};
|
|
489
|
+
writeFileSync(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), `${JSON.stringify(latest, null, 2)}\n`, 'utf8');
|
|
490
|
+
}
|
|
422
491
|
async function createVerifyOutput(input, planSource, projectRoot, lang) {
|
|
423
492
|
const contract = readCommandContract(projectRoot);
|
|
424
493
|
const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
|
|
425
494
|
const scheduledIntents = new Set(report.schedule.entries.map((entry) => entry.intent));
|
|
495
|
+
const scheduledTestTargets = testTargetsByScheduledIntent(report);
|
|
426
496
|
const results = [];
|
|
427
497
|
for (const entry of report.schedule.entries) {
|
|
428
|
-
results.push(await runVerificationIntent(entry.intent, lang));
|
|
498
|
+
results.push(await runVerificationIntent(entry.intent, lang, scheduledTestTargets.get(entry.intent) ?? []));
|
|
429
499
|
}
|
|
430
|
-
results.push(...createSkippedResults(report.candidates, scheduledIntents));
|
|
500
|
+
results.push(...createSkippedResults(report.candidates, scheduledIntents, report.gaps));
|
|
431
501
|
const summary = summarizeResults(results);
|
|
432
|
-
|
|
502
|
+
const output = {
|
|
433
503
|
schema_version: VERIFY_SCHEMA_VERSION,
|
|
434
504
|
command: 'verify',
|
|
435
505
|
mustflow_root: projectRoot,
|
|
@@ -440,6 +510,8 @@ async function createVerifyOutput(input, planSource, projectRoot, lang) {
|
|
|
440
510
|
summary,
|
|
441
511
|
results,
|
|
442
512
|
};
|
|
513
|
+
writeVerifyRunReceipts(projectRoot, output);
|
|
514
|
+
return output;
|
|
443
515
|
}
|
|
444
516
|
async function createPlanOnlyOutput(input, projectRoot) {
|
|
445
517
|
const contract = readCommandContract(projectRoot);
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -27,6 +27,7 @@ export const enMessages = {
|
|
|
27
27
|
"command.contractLint.summary": "Lint the command contract",
|
|
28
28
|
"command.status.summary": "Show local mustflow install status",
|
|
29
29
|
"command.update.summary": "Preview or apply mustflow workflow updates",
|
|
30
|
+
"command.upgrade.summary": "Check the package version and safely update installed workflow files",
|
|
30
31
|
"command.map.summary": "Generate REPO_MAP.md",
|
|
31
32
|
"command.lineEndings.summary": "Inspect and normalize line-ending policy",
|
|
32
33
|
"command.run.summary": "Run a configured oneshot command",
|
|
@@ -658,6 +659,17 @@ Read these files before working:
|
|
|
658
659
|
"version.check.upToDate": "latest {version}; already up to date",
|
|
659
660
|
"version.check.updateCommand": "Update command:",
|
|
660
661
|
"version.error.checkFailed": "Could not check npm for a newer version: {message}",
|
|
662
|
+
"upgrade.help.summary": "Check whether the installed mustflow package is current, then safely apply bundled workflow template updates when possible.",
|
|
663
|
+
"upgrade.help.option.dryRun": "Check package status and print the project update plan without writing files",
|
|
664
|
+
"upgrade.help.exit.ok": "The package was current and the project update check completed",
|
|
665
|
+
"upgrade.help.exit.fail": "A package update is required, a project update blocker was found, or input was invalid",
|
|
666
|
+
"upgrade.title": "mustflow upgrade",
|
|
667
|
+
"upgrade.packageSection": "Package:",
|
|
668
|
+
"upgrade.projectSection": "Project template:",
|
|
669
|
+
"upgrade.packageUpdateRequired": "Update the mustflow package first, then run `mf upgrade` again.",
|
|
670
|
+
"upgrade.noFilesWritten": "No project files were written.",
|
|
671
|
+
"upgrade.warning.versionCheckFailed": "Could not check npm for a newer version: {message}",
|
|
672
|
+
"upgrade.warning.continueWithBundledTemplate": "Continuing with the bundled template in the current CLI.",
|
|
661
673
|
"classify.help.summary": "Classify changed paths, public surfaces, and required validation reasons without modifying files.",
|
|
662
674
|
"classify.help.option.changed": "Read paths from git status --short --untracked-files=all",
|
|
663
675
|
"classify.help.exit.ok": "Change classification was inspected and printed",
|