mustflow 2.17.0 → 2.18.2

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 (42) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/commands/classify.js +13 -3
  3. package/dist/cli/commands/dashboard.js +2 -1
  4. package/dist/cli/commands/explain-verify.js +2 -2
  5. package/dist/cli/commands/impact.js +13 -3
  6. package/dist/cli/commands/run.js +156 -104
  7. package/dist/cli/commands/verify.js +157 -45
  8. package/dist/cli/i18n/en.js +10 -1
  9. package/dist/cli/i18n/es.js +10 -1
  10. package/dist/cli/i18n/fr.js +10 -1
  11. package/dist/cli/i18n/hi.js +10 -1
  12. package/dist/cli/i18n/ko.js +10 -1
  13. package/dist/cli/i18n/zh.js +10 -1
  14. package/dist/cli/lib/git-changes.js +25 -2
  15. package/dist/cli/lib/local-index/constants.js +4 -1
  16. package/dist/cli/lib/local-index/index.js +22 -5
  17. package/dist/cli/lib/repo-map.js +90 -30
  18. package/dist/cli/lib/run-plan.js +25 -2
  19. package/dist/cli/lib/validation/index.js +2 -1
  20. package/dist/core/atomic-state-write.js +31 -0
  21. package/dist/core/bounded-output.js +23 -1
  22. package/dist/core/check-issues.js +3 -0
  23. package/dist/core/command-contract-rules.js +104 -2
  24. package/dist/core/command-contract-validation.js +71 -9
  25. package/dist/core/command-intent-eligibility.js +9 -1
  26. package/dist/core/command-output-limits.js +5 -0
  27. package/dist/core/completion-verdict.js +2 -1
  28. package/dist/core/contract-lint.js +10 -1
  29. package/dist/core/public-json-contracts.js +1 -1
  30. package/dist/core/run-receipt.js +20 -13
  31. package/dist/core/source-anchors.js +96 -24
  32. package/dist/core/verification-evidence.js +4 -1
  33. package/package.json +1 -1
  34. package/schemas/README.md +4 -4
  35. package/schemas/change-verification-report.schema.json +2 -1
  36. package/schemas/contract-lint-report.schema.json +2 -1
  37. package/schemas/explain-report.schema.json +1 -0
  38. package/schemas/latest-run-pointer.schema.json +1 -0
  39. package/schemas/run-receipt.schema.json +26 -3
  40. package/schemas/verify-report.schema.json +2 -1
  41. package/schemas/verify-run-manifest.schema.json +2 -1
  42. package/templates/default/manifest.toml +1 -1
@@ -1,10 +1,11 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { createClassifyOutput } from './classify.js';
5
5
  import { runRun } from './run.js';
6
6
  import { createChangeVerificationReport, } from '../../core/change-verification.js';
7
7
  import { createVerifyCompletionVerdict, } from '../../core/completion-verdict.js';
8
+ import { atomicWriteJsonFile, createStateRunId } from '../../core/atomic-state-write.js';
8
9
  import { createExternalEvidenceRisks, } from '../../core/external-evidence.js';
9
10
  import { createRepeatedFailureRisks, createVerificationFailureFingerprint, updateRepeatedFailureState, } from '../../core/repeated-failure.js';
10
11
  import { countReproEvidenceVerdictEffects, createReproEvidenceRisks, } from '../../core/repro-evidence.js';
@@ -17,9 +18,9 @@ import { t } from '../lib/i18n.js';
17
18
  import { readLocalCommandEffectGraph, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
18
19
  import { resolveMustflowRoot } from '../lib/project-root.js';
19
20
  const VERIFY_SCHEMA_VERSION = '1';
20
- const VERIFY_RUN_DIR = path.join('.mustflow', 'state', 'runs', 'verify-latest');
21
- const VERIFY_MANIFEST_PATH = path.join(VERIFY_RUN_DIR, 'manifest.json');
22
- const LATEST_RUN_RECEIPT_PATH = path.join('.mustflow', 'state', 'runs', 'latest.json');
21
+ const RUN_STATE_DIR = path.join('.mustflow', 'state', 'runs');
22
+ const LATEST_RUN_RECEIPT_PATH = path.join(RUN_STATE_DIR, 'latest.json');
23
+ const DEFAULT_VERIFY_PARALLELISM = 1;
23
24
  function createBufferedOutput() {
24
25
  const stdout = [];
25
26
  const stderr = [];
@@ -52,6 +53,7 @@ export function getVerifyHelp(lang = 'en') {
52
53
  { label: '--write-plan <path>', description: t(lang, 'verify.help.option.writePlan') },
53
54
  { label: '--repro-evidence <path>', description: t(lang, 'verify.help.option.reproEvidence') },
54
55
  { label: '--external-evidence <path>', description: t(lang, 'verify.help.option.externalEvidence') },
56
+ { label: '--parallel <count>', description: t(lang, 'verify.help.option.parallel') },
55
57
  { label: '--plan-only', description: t(lang, 'verify.help.option.planOnly') },
56
58
  { label: '--json', description: t(lang, 'cli.option.json') },
57
59
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
@@ -81,6 +83,7 @@ function parseVerifyArgs(args) {
81
83
  let json = false;
82
84
  let planOnly = false;
83
85
  let changed = false;
86
+ let parallelism = DEFAULT_VERIFY_PARALLELISM;
84
87
  for (let index = 0; index < args.length; index += 1) {
85
88
  const arg = args[index];
86
89
  if (arg === '--json') {
@@ -95,6 +98,19 @@ function parseVerifyArgs(args) {
95
98
  changed = true;
96
99
  continue;
97
100
  }
101
+ if (arg === '--parallel') {
102
+ const value = args[index + 1];
103
+ if (!value || value.startsWith('-')) {
104
+ return { json, planOnly, changed, reason, parallelism, error: 'missing_parallel_value' };
105
+ }
106
+ const parsedParallelism = parseVerifyParallelism(value);
107
+ if (parsedParallelism === null) {
108
+ return { json, planOnly, changed, reason, parallelism, error: 'invalid_parallel_value' };
109
+ }
110
+ parallelism = parsedParallelism;
111
+ index += 1;
112
+ continue;
113
+ }
98
114
  if (arg === '--reason') {
99
115
  const value = args[index + 1];
100
116
  if (!value || value.startsWith('-')) {
@@ -195,6 +211,15 @@ function parseVerifyArgs(args) {
195
211
  reason = value;
196
212
  continue;
197
213
  }
214
+ if (arg.startsWith('--parallel=')) {
215
+ const value = arg.slice('--parallel='.length);
216
+ const parsedParallelism = parseVerifyParallelism(value);
217
+ if (parsedParallelism === null) {
218
+ return { json, planOnly, changed, reason, parallelism, error: 'invalid_parallel_value' };
219
+ }
220
+ parallelism = parsedParallelism;
221
+ continue;
222
+ }
198
223
  if (arg.startsWith('--from-plan=')) {
199
224
  const value = arg.slice('--from-plan='.length);
200
225
  if (value.length === 0) {
@@ -289,7 +314,25 @@ function parseVerifyArgs(args) {
289
314
  error: `unexpected:${arg}`,
290
315
  };
291
316
  }
292
- return { json, planOnly, changed, reason, fromClassification, fromPlan, writePlan, reproEvidence, externalEvidence };
317
+ return {
318
+ json,
319
+ planOnly,
320
+ changed,
321
+ reason,
322
+ fromClassification,
323
+ fromPlan,
324
+ writePlan,
325
+ reproEvidence,
326
+ externalEvidence,
327
+ parallelism,
328
+ };
329
+ }
330
+ function parseVerifyParallelism(value) {
331
+ if (!/^[1-9][0-9]*$/u.test(value)) {
332
+ return null;
333
+ }
334
+ const parsed = Number(value);
335
+ return Number.isSafeInteger(parsed) ? parsed : null;
293
336
  }
294
337
  function uniqueStrings(values) {
295
338
  return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
@@ -297,6 +340,18 @@ function uniqueStrings(values) {
297
340
  function toPosixPath(value) {
298
341
  return value.split(path.sep).join('/');
299
342
  }
343
+ function createVerifyRunStatePaths(projectRoot) {
344
+ const runDir = toPosixPath(path.join(RUN_STATE_DIR, createStateRunId('verify')));
345
+ const manifestPath = toPosixPath(path.join(runDir, 'manifest.json'));
346
+ const absoluteRunDir = path.join(projectRoot, runDir);
347
+ return {
348
+ runDir,
349
+ manifestPath,
350
+ absoluteRunDir,
351
+ absoluteIntentDir: path.join(absoluteRunDir, 'intents'),
352
+ absoluteManifestPath: path.join(projectRoot, manifestPath),
353
+ };
354
+ }
300
355
  function sanitizeIntentFilePart(value) {
301
356
  const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
302
357
  return sanitized.length > 0 ? sanitized.slice(0, 80) : 'intent';
@@ -445,7 +500,7 @@ function resolvePlanPath(projectRoot, inputPath) {
445
500
  }
446
501
  return resolved;
447
502
  }
448
- export function readInputFromPlan(projectRoot, inputPath) {
503
+ export function readInputFromClassificationReport(projectRoot, inputPath) {
449
504
  let parsed;
450
505
  const planPath = resolvePlanPath(projectRoot, inputPath);
451
506
  try {
@@ -764,6 +819,8 @@ export function planErrorMessageKey(code) {
764
819
  return 'verify.error.unsupported_plan_source';
765
820
  case 'plan_root_mismatch':
766
821
  return 'verify.error.plan_root_mismatch';
822
+ case 'git_changed_files_unavailable':
823
+ return 'verify.error.changed_files_unavailable';
767
824
  default:
768
825
  return 'verify.error.invalid_plan_file';
769
826
  }
@@ -834,7 +891,8 @@ async function runVerificationIntent(intent, lang, verificationPlanId, testTarge
834
891
  if (receiptStatus === 'passed' ||
835
892
  receiptStatus === 'failed' ||
836
893
  receiptStatus === 'timed_out' ||
837
- receiptStatus === 'start_failed') {
894
+ receiptStatus === 'start_failed' ||
895
+ receiptStatus === 'output_limit_exceeded') {
838
896
  status = receiptStatus;
839
897
  }
840
898
  }
@@ -854,12 +912,52 @@ async function runVerificationIntent(intent, lang, verificationPlanId, testTarge
854
912
  receipt,
855
913
  };
856
914
  }
915
+ function entriesForScheduleBatch(entries, batch) {
916
+ const batchIntents = new Set(batch.intents);
917
+ return entries.filter((entry) => batchIntents.has(entry.intent));
918
+ }
919
+ async function runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets) {
920
+ const results = [];
921
+ for (const entry of entries) {
922
+ results.push(await runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? []));
923
+ }
924
+ return results;
925
+ }
926
+ async function runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets) {
927
+ const results = [];
928
+ for (let index = 0; index < entries.length; index += parallelism) {
929
+ const chunk = entries.slice(index, index + parallelism);
930
+ results.push(...(await Promise.all(chunk.map((entry) => runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? [])))));
931
+ }
932
+ return results;
933
+ }
934
+ async function runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism) {
935
+ if (parallelism <= DEFAULT_VERIFY_PARALLELISM) {
936
+ return runVerificationEntriesSequentially(report.schedule.entries, lang, verificationPlanId, scheduledTestTargets);
937
+ }
938
+ const results = [];
939
+ for (const batch of report.schedule.batches) {
940
+ const entries = entriesForScheduleBatch(report.schedule.entries, batch);
941
+ if (entries.length === 0) {
942
+ continue;
943
+ }
944
+ if (entries.length > 1 && entries.every((entry) => entry.parallelEligible)) {
945
+ results.push(...(await runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets)));
946
+ continue;
947
+ }
948
+ results.push(...(await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets)));
949
+ }
950
+ return results;
951
+ }
857
952
  function summarizeResults(results) {
858
953
  const ran = results.filter((result) => !result.skipped).length;
859
954
  const passed = results.filter((result) => result.status === 'passed').length;
860
955
  const skipped = results.filter((result) => result.skipped).length;
861
956
  const failed = results.filter((result) => !result.skipped &&
862
- (result.status === 'failed' || result.status === 'timed_out' || result.status === 'start_failed')).length;
957
+ (result.status === 'failed' ||
958
+ result.status === 'timed_out' ||
959
+ result.status === 'start_failed' ||
960
+ result.status === 'output_limit_exceeded')).length;
863
961
  return {
864
962
  matched: results.filter((result) => result.intent !== null).length,
865
963
  ran,
@@ -908,11 +1006,15 @@ function timedOutForResult(result) {
908
1006
  return result.status === 'timed_out' || resultSummary?.timed_out === true;
909
1007
  }
910
1008
  function errorKindForResult(result) {
911
- return stringField(resultSummaryForResult(result)?.error_kind) ?? (result.status === 'start_failed' ? 'start_failed' : null);
1009
+ return (stringField(resultSummaryForResult(result)?.error_kind) ??
1010
+ (result.status === 'start_failed' || result.status === 'output_limit_exceeded' ? result.status : null));
912
1011
  }
913
1012
  function failedResults(results) {
914
1013
  return results.filter((result) => !result.skipped &&
915
- (result.status === 'failed' || result.status === 'timed_out' || result.status === 'start_failed'));
1014
+ (result.status === 'failed' ||
1015
+ result.status === 'timed_out' ||
1016
+ result.status === 'start_failed' ||
1017
+ result.status === 'output_limit_exceeded'));
916
1018
  }
917
1019
  function createFailureFingerprintForVerify(input) {
918
1020
  const failures = failedResults(input.results);
@@ -1024,7 +1126,10 @@ function createCriteriaEvidence(report, results) {
1024
1126
  .filter((intent) => intent !== null);
1025
1127
  const gapCount = report.gaps.filter((gap) => gap.reason === requirement.reason).length;
1026
1128
  const selectedResults = selectedIntents.map((intent) => resultForSelectedIntent(results, intent));
1027
- if (selectedResults.some((result) => result?.status === 'failed' || result?.status === 'timed_out' || result?.status === 'start_failed')) {
1129
+ if (selectedResults.some((result) => result?.status === 'failed' ||
1130
+ result?.status === 'timed_out' ||
1131
+ result?.status === 'start_failed' ||
1132
+ result?.status === 'output_limit_exceeded')) {
1028
1133
  return { ...current, contradicted: current.contradicted + 1 };
1029
1134
  }
1030
1135
  if (gapCount > 0 || (selectedIntents.length === 0 && skippedIntents.length > 0)) {
@@ -1179,20 +1284,18 @@ function createVerificationPlanId(report, contract) {
1179
1284
  return hashTextSha256(stableJson(fingerprintSource));
1180
1285
  }
1181
1286
  function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks) {
1182
- const runDir = path.join(projectRoot, VERIFY_RUN_DIR);
1183
- const intentDir = path.join(runDir, 'intents');
1287
+ const statePaths = createVerifyRunStatePaths(projectRoot);
1184
1288
  const receipts = [];
1185
1289
  const results = [];
1186
- rmSync(runDir, { recursive: true, force: true });
1187
- mkdirSync(intentDir, { recursive: true });
1290
+ mkdirSync(statePaths.absoluteIntentDir, { recursive: true });
1188
1291
  for (const [index, result] of output.results.entries()) {
1189
1292
  let receiptPath = null;
1190
1293
  let receiptSha256 = null;
1191
1294
  let receipt = result.receipt;
1192
1295
  if (result.intent && result.receipt) {
1193
1296
  const fileName = `${String(index + 1).padStart(3, '0')}-${sanitizeIntentFilePart(result.intent)}.json`;
1194
- const absoluteReceiptPath = path.join(intentDir, fileName);
1195
- receiptPath = toPosixPath(path.join(VERIFY_RUN_DIR, 'intents', fileName));
1297
+ const absoluteReceiptPath = path.join(statePaths.absoluteIntentDir, fileName);
1298
+ receiptPath = toPosixPath(path.join(statePaths.runDir, 'intents', fileName));
1196
1299
  receipt = {
1197
1300
  ...result.receipt,
1198
1301
  verification_plan_id: output.verification_plan_id,
@@ -1200,7 +1303,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1200
1303
  };
1201
1304
  const receiptContent = `${JSON.stringify(receipt, null, 2)}\n`;
1202
1305
  receiptSha256 = hashTextSha256(receiptContent);
1203
- writeFileSync(absoluteReceiptPath, receiptContent, 'utf8');
1306
+ atomicWriteJsonFile(absoluteReceiptPath, receipt);
1204
1307
  }
1205
1308
  receipts.push({
1206
1309
  intent: result.intent,
@@ -1270,6 +1373,8 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1270
1373
  completion_verdict: completionVerdict,
1271
1374
  failure_fingerprint: failureFingerprint,
1272
1375
  repeated_failure_summary: repeatedFailureSummary,
1376
+ run_dir: statePaths.runDir,
1377
+ manifest_path: statePaths.manifestPath,
1273
1378
  results,
1274
1379
  evidence_model: createVerifyEvidenceModel({
1275
1380
  report,
@@ -1293,6 +1398,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1293
1398
  reasons: outputWithReceiptPaths.reasons,
1294
1399
  plan_source: outputWithReceiptPaths.plan_source,
1295
1400
  verification_plan_id: outputWithReceiptPaths.verification_plan_id,
1401
+ execution_status: outputWithReceiptPaths.execution_status,
1296
1402
  status: outputWithReceiptPaths.status,
1297
1403
  completion_verdict: outputWithReceiptPaths.completion_verdict,
1298
1404
  evidence_model: outputWithReceiptPaths.evidence_model,
@@ -1303,7 +1409,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1303
1409
  ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
1304
1410
  receipts,
1305
1411
  };
1306
- writeFileSync(path.join(runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
1412
+ atomicWriteJsonFile(statePaths.absoluteManifestPath, manifest);
1307
1413
  const latest = {
1308
1414
  schema_version: '1',
1309
1415
  command: 'verify',
@@ -1312,6 +1418,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1312
1418
  reasons: outputWithReceiptPaths.reasons,
1313
1419
  plan_source: outputWithReceiptPaths.plan_source,
1314
1420
  verification_plan_id: outputWithReceiptPaths.verification_plan_id,
1421
+ execution_status: outputWithReceiptPaths.execution_status,
1315
1422
  status: outputWithReceiptPaths.status,
1316
1423
  completion_verdict: outputWithReceiptPaths.completion_verdict,
1317
1424
  evidence_model: outputWithReceiptPaths.evidence_model,
@@ -1320,13 +1427,13 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1320
1427
  summary: outputWithReceiptPaths.summary,
1321
1428
  ...(outputWithReceiptPaths.repro_evidence ? { repro_evidence: outputWithReceiptPaths.repro_evidence } : {}),
1322
1429
  ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
1323
- run_dir: toPosixPath(VERIFY_RUN_DIR),
1324
- manifest_path: toPosixPath(VERIFY_MANIFEST_PATH),
1430
+ run_dir: statePaths.runDir,
1431
+ manifest_path: statePaths.manifestPath,
1325
1432
  };
1326
- writeFileSync(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), `${JSON.stringify(latest, null, 2)}\n`, 'utf8');
1433
+ atomicWriteJsonFile(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), latest);
1327
1434
  return outputWithReceiptPaths;
1328
1435
  }
1329
- async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = []) {
1436
+ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = [], parallelism = DEFAULT_VERIFY_PARALLELISM) {
1330
1437
  const contract = readCommandContract(projectRoot);
1331
1438
  const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
1332
1439
  const verificationPlanId = createVerificationPlanId(report, contract);
@@ -1339,10 +1446,7 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
1339
1446
  const reproEvidenceRisks = createReproEvidenceRisks(reproEvidence, { verificationPlanId });
1340
1447
  const reproEvidenceVerdictEffects = countReproEvidenceVerdictEffects(reproEvidenceRisks);
1341
1448
  const externalEvidenceRisks = createExternalEvidenceRisks(externalChecks);
1342
- const results = [];
1343
- for (const entry of report.schedule.entries) {
1344
- results.push(await runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? []));
1345
- }
1449
+ const results = await runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism);
1346
1450
  results.push(...createSkippedResults(report.candidates, scheduledIntents, report.gaps));
1347
1451
  const summary = summarizeResults(results);
1348
1452
  const status = getVerificationStatus(summary);
@@ -1403,6 +1507,7 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
1403
1507
  reasons: input.reasons,
1404
1508
  plan_source: planSource,
1405
1509
  verification_plan_id: verificationPlanId,
1510
+ execution_status: status,
1406
1511
  status,
1407
1512
  completion_verdict: completionVerdict,
1408
1513
  evidence_model: evidenceModel,
@@ -1411,8 +1516,8 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
1411
1516
  summary,
1412
1517
  ...(reproEvidence ? { repro_evidence: reproEvidence } : {}),
1413
1518
  ...(externalChecks.length > 0 ? { external_checks: externalChecks } : {}),
1414
- run_dir: toPosixPath(VERIFY_RUN_DIR),
1415
- manifest_path: toPosixPath(VERIFY_MANIFEST_PATH),
1519
+ run_dir: '',
1520
+ manifest_path: '',
1416
1521
  results,
1417
1522
  };
1418
1523
  return writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks);
@@ -1486,19 +1591,23 @@ export async function runVerify(args, reporter, lang = 'en') {
1486
1591
  if (parsed.error) {
1487
1592
  const message = parsed.error === 'missing_reason_value'
1488
1593
  ? t(lang, 'cli.error.missingValue', { option: '--reason' })
1489
- : parsed.error === 'missing_from_classification_value'
1490
- ? t(lang, 'cli.error.missingValue', { option: '--from-classification' })
1491
- : parsed.error === 'missing_from_plan_value'
1492
- ? t(lang, 'cli.error.missingValue', { option: '--from-plan' })
1493
- : parsed.error === 'missing_write_plan_value'
1494
- ? t(lang, 'cli.error.missingValue', { option: '--write-plan' })
1495
- : parsed.error === 'missing_repro_evidence_value'
1496
- ? t(lang, 'cli.error.missingValue', { option: '--repro-evidence' })
1497
- : parsed.error === 'missing_external_evidence_value'
1498
- ? t(lang, 'cli.error.missingValue', { option: '--external-evidence' })
1499
- : parsed.error.startsWith('unexpected:')
1500
- ? t(lang, 'cli.error.unexpectedArgument', { argument: parsed.error.slice('unexpected:'.length) })
1501
- : t(lang, 'cli.error.unknownOption', { option: parsed.error });
1594
+ : parsed.error === 'missing_parallel_value'
1595
+ ? t(lang, 'cli.error.missingValue', { option: '--parallel' })
1596
+ : parsed.error === 'invalid_parallel_value'
1597
+ ? t(lang, 'verify.error.invalidParallel')
1598
+ : parsed.error === 'missing_from_classification_value'
1599
+ ? t(lang, 'cli.error.missingValue', { option: '--from-classification' })
1600
+ : parsed.error === 'missing_from_plan_value'
1601
+ ? t(lang, 'cli.error.missingValue', { option: '--from-plan' })
1602
+ : parsed.error === 'missing_write_plan_value'
1603
+ ? t(lang, 'cli.error.missingValue', { option: '--write-plan' })
1604
+ : parsed.error === 'missing_repro_evidence_value'
1605
+ ? t(lang, 'cli.error.missingValue', { option: '--repro-evidence' })
1606
+ : parsed.error === 'missing_external_evidence_value'
1607
+ ? t(lang, 'cli.error.missingValue', { option: '--external-evidence' })
1608
+ : parsed.error.startsWith('unexpected:')
1609
+ ? t(lang, 'cli.error.unexpectedArgument', { argument: parsed.error.slice('unexpected:'.length) })
1610
+ : t(lang, 'cli.error.unknownOption', { option: parsed.error });
1502
1611
  printUsageError(reporter, message, 'mf verify --help', getVerifyHelp(lang), lang);
1503
1612
  return 1;
1504
1613
  }
@@ -1538,13 +1647,16 @@ export async function runVerify(args, reporter, lang = 'en') {
1538
1647
  let reproEvidence = null;
1539
1648
  let externalChecks = [];
1540
1649
  try {
1650
+ if (parsed.writePlan) {
1651
+ resolvePlanPath(projectRoot, parsed.writePlan);
1652
+ }
1541
1653
  if (parsed.changed) {
1542
1654
  const changedInput = createInputFromChanged(projectRoot);
1543
1655
  input = changedInput.input;
1544
1656
  changedPlan = changedInput.plan;
1545
1657
  }
1546
1658
  else if (parsed.fromClassification || parsed.fromPlan) {
1547
- input = readInputFromPlan(projectRoot, (parsed.fromClassification ?? parsed.fromPlan));
1659
+ input = readInputFromClassificationReport(projectRoot, (parsed.fromClassification ?? parsed.fromPlan));
1548
1660
  }
1549
1661
  else {
1550
1662
  input = {
@@ -1580,12 +1692,12 @@ export async function runVerify(args, reporter, lang = 'en') {
1580
1692
  reporter.stdout(JSON.stringify(await createPlanOnlyOutput(input, projectRoot), null, 2));
1581
1693
  return 0;
1582
1694
  }
1583
- const output = await createVerifyOutput(input, parsed.fromClassification ?? parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang, reproEvidence, externalChecks);
1695
+ const output = await createVerifyOutput(input, parsed.fromClassification ?? parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang, reproEvidence, externalChecks, parsed.parallelism ?? DEFAULT_VERIFY_PARALLELISM);
1584
1696
  if (parsed.json) {
1585
1697
  reporter.stdout(JSON.stringify(output, null, 2));
1586
1698
  }
1587
1699
  else {
1588
1700
  reporter.stdout(renderVerifyOutput(output, lang));
1589
1701
  }
1590
- return output.status === 'passed' ? 0 : 1;
1702
+ return output.completion_verdict.status === 'verified' ? 0 : 1;
1591
1703
  }
@@ -664,8 +664,12 @@ Read these files before working:
664
664
  "run.error.unsafeIntentDetail": "Use a shell-safe intent name.",
665
665
  "run.error.blockedShellBackground": 'Intent "{intent}" is blocked. {detail}',
666
666
  "run.error.blockedShellBackgroundDetail": "Shell commands must not spawn background work.",
667
+ "run.error.blockedLongRunningCommand": 'Intent "{intent}" is blocked. {detail}',
668
+ "run.error.blockedLongRunningCommandDetail": "Command argv must describe a finite one-shot command, not a development server, watcher, shell wrapper, interpreter loop, or background process.",
667
669
  "run.error.cwdOutsideProject": 'Command "{intent}" has an invalid cwd: {detail}',
668
670
  "run.error.cwdOutsideProjectDetail": "Intent cwd must stay inside the current root.",
671
+ "run.error.maxOutputBytes": 'Command "{intent}" has invalid max_output_bytes. {detail}',
672
+ "run.error.maxOutputBytesDetail": "The output limit must stay within the allowed maximum.",
669
673
  "run.error.conflictingPreviewModes": "Use either --dry-run or --plan-only, not both",
670
674
  "run.error.timedOut": 'Command "{intent}" timed out after {seconds} seconds',
671
675
  "run.error.startFailed": 'Command "{intent}" failed to start: {message}',
@@ -727,6 +731,7 @@ Read these files before working:
727
731
  "classify.source.changed": "changed files",
728
732
  "classify.source.paths": "explicit paths",
729
733
  "classify.error.missingInput": "Specify --changed or at least one path",
734
+ "classify.error.changed_files_unavailable": "Unable to inspect changed files with git status",
730
735
  "classify.error.write_path_outside_root": "Classification report path must stay inside the mustflow root",
731
736
  "impact.help.summary": "Report whether changed paths require a package or template version decision without modifying files.",
732
737
  "impact.help.option.changed": "Read paths from git status --short --untracked-files=all",
@@ -741,14 +746,16 @@ Read these files before working:
741
746
  "impact.label.affectedVersionSources": "Affected version sources",
742
747
  "impact.label.affectedSurfaces": "Affected surfaces",
743
748
  "impact.error.missingInput": "Specify --changed or at least one path",
749
+ "impact.error.changed_files_unavailable": "Unable to inspect changed files with git status",
744
750
  "verify.help.summary": "Run configured verification intents selected by required_after metadata.",
745
751
  "verify.help.option.reason": "Select the required_after reason to verify",
746
752
  "verify.help.option.fromClassification": "Read verification reasons from an mf classify report inside this repository",
747
- "verify.help.option.fromPlan": "Compatibility alias for --from-classification",
753
+ "verify.help.option.fromPlan": "Deprecated compatibility alias for --from-classification; it still expects an mf classify report",
748
754
  "verify.help.option.changed": "Classify current Git changes and verify the matching reasons",
749
755
  "verify.help.option.writePlan": "Compatibility option that writes the changed-file classification report",
750
756
  "verify.help.option.reproEvidence": "Read structured bug-fix reproduction evidence from a repository-local JSON summary",
751
757
  "verify.help.option.externalEvidence": "Read lower-authority external CI evidence from a repository-local JSON summary",
758
+ "verify.help.option.parallel": "Run safe non-conflicting schedule batches with up to this many commands; default is 1",
752
759
  "verify.help.option.planOnly": "Print the verification plan without running commands; requires --json",
753
760
  "verify.help.exit.ok": "All selected verification intents passed",
754
761
  "verify.help.exit.fail": "Verification failed, was partial, was blocked, or input was invalid",
@@ -763,11 +770,13 @@ Read these files before working:
763
770
  "verify.error.planOnlyJson": "--plan-only requires --json",
764
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence cannot be used with --plan-only",
765
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence cannot be used with --plan-only",
773
+ "verify.error.invalidParallel": "--parallel must be a positive integer",
766
774
  "verify.error.invalid_plan_file": "Classification report must be a readable JSON file",
767
775
  "verify.error.unsupported_plan_source": "Verification input must be an mf classify report",
768
776
  "verify.error.plan_root_mismatch": "Classification report must come from this mustflow root",
769
777
  "verify.error.missing_plan_reasons": "Classification report must include summary.validationReasons",
770
778
  "verify.error.plan_path_outside_root": "Classification report path must stay inside the mustflow root",
779
+ "verify.error.changed_files_unavailable": "Unable to inspect changed files with git status",
771
780
  "verify.error.invalid_repro_evidence_file": "Repro evidence must be a readable JSON summary with structured evidence fields",
772
781
  "verify.error.unsupported_repro_evidence_source": "Repro evidence input must use command repro-evidence",
773
782
  "verify.error.invalid_external_evidence_file": "External evidence must be a readable JSON summary with checks",
@@ -664,8 +664,12 @@ Lee estos archivos antes de trabajar:
664
664
  "run.error.unsafeIntentDetail": "Usa un nombre de intención seguro para shell.",
665
665
  "run.error.blockedShellBackground": 'La intención "{intent}" está bloqueada. {detail}',
666
666
  "run.error.blockedShellBackgroundDetail": "Los comandos de shell no deben iniciar trabajo en segundo plano.",
667
+ "run.error.blockedLongRunningCommand": 'La intención "{intent}" está bloqueada. {detail}',
668
+ "run.error.blockedLongRunningCommandDetail": "argv debe describir un comando finito de una sola ejecución, no un servidor de desarrollo, watcher, envoltorio de shell, bucle de intérprete o proceso en segundo plano.",
667
669
  "run.error.cwdOutsideProject": 'El comando "{intent}" tiene un cwd no válido: {detail}',
668
670
  "run.error.cwdOutsideProjectDetail": "El cwd de la intención debe permanecer dentro de la raíz actual.",
671
+ "run.error.maxOutputBytes": 'El comando "{intent}" tiene max_output_bytes no válido. {detail}',
672
+ "run.error.maxOutputBytesDetail": "El límite de salida debe permanecer dentro del máximo permitido.",
669
673
  "run.error.conflictingPreviewModes": "Usa --dry-run o --plan-only, no ambos",
670
674
  "run.error.timedOut": 'El comando "{intent}" agotó el tiempo después de {seconds} segundos',
671
675
  "run.error.startFailed": 'No se pudo iniciar el comando "{intent}": {message}',
@@ -727,6 +731,7 @@ Lee estos archivos antes de trabajar:
727
731
  "classify.source.changed": "archivos cambiados",
728
732
  "classify.source.paths": "rutas explicitas",
729
733
  "classify.error.missingInput": "Indica --changed o al menos una ruta",
734
+ "classify.error.changed_files_unavailable": "No se pudieron inspeccionar los archivos cambiados con git status",
730
735
  "classify.error.write_path_outside_root": "La ruta del informe de clasificacion debe permanecer dentro de la raiz mustflow",
731
736
  "impact.help.summary": "Informa si las rutas cambiadas requieren una decision de version de paquete o plantilla sin modificar archivos.",
732
737
  "impact.help.option.changed": "Lee rutas desde git status --short --untracked-files=all",
@@ -741,14 +746,16 @@ Lee estos archivos antes de trabajar:
741
746
  "impact.label.affectedVersionSources": "Fuentes de version afectadas",
742
747
  "impact.label.affectedSurfaces": "Superficies afectadas",
743
748
  "impact.error.missingInput": "Indica --changed o al menos una ruta",
749
+ "impact.error.changed_files_unavailable": "No se pudieron inspeccionar los archivos cambiados con git status",
744
750
  "verify.help.summary": "Ejecuta intenciones de verificación configuradas seleccionadas por metadatos required_after.",
745
751
  "verify.help.option.reason": "Selecciona la razón required_after que se debe verificar",
746
752
  "verify.help.option.fromClassification": "Lee razones de verificación desde un informe de mf classify dentro de este repositorio",
747
- "verify.help.option.fromPlan": "Alias de compatibilidad para --from-classification",
753
+ "verify.help.option.fromPlan": "Alias de compatibilidad obsoleto para --from-classification; todavía espera un informe de mf classify",
748
754
  "verify.help.option.changed": "Clasifica los cambios actuales de Git y verifica las razones correspondientes",
749
755
  "verify.help.option.writePlan": "Opción de compatibilidad que escribe el informe de clasificación de cambios",
750
756
  "verify.help.option.reproEvidence": "Lee evidencia estructurada de reproducción de errores desde un resumen JSON local del repositorio",
751
757
  "verify.help.option.externalEvidence": "Lee evidencia de CI externa de menor autoridad desde un resumen JSON local del repositorio",
758
+ "verify.help.option.parallel": "Ejecuta lotes programados seguros y sin conflictos con hasta esta cantidad de comandos; el valor predeterminado es 1",
752
759
  "verify.help.option.planOnly": "Imprime el plan de verificación sin ejecutar comandos; requiere --json",
753
760
  "verify.help.exit.ok": "Todas las intenciones de verificación seleccionadas pasaron",
754
761
  "verify.help.exit.fail": "La verificación falló, fue parcial, quedó bloqueada o la entrada no fue válida",
@@ -763,11 +770,13 @@ Lee estos archivos antes de trabajar:
763
770
  "verify.error.planOnlyJson": "--plan-only requiere --json",
764
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence no se puede usar con --plan-only",
765
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence no se puede usar con --plan-only",
773
+ "verify.error.invalidParallel": "--parallel debe ser un entero positivo",
766
774
  "verify.error.invalid_plan_file": "El informe de clasificación debe ser un archivo JSON legible",
767
775
  "verify.error.unsupported_plan_source": "La entrada de verificación debe ser un informe de mf classify",
768
776
  "verify.error.plan_root_mismatch": "El informe de clasificación debe provenir de esta raíz mustflow",
769
777
  "verify.error.missing_plan_reasons": "El informe de clasificación debe incluir summary.validationReasons",
770
778
  "verify.error.plan_path_outside_root": "La ruta del informe de clasificación debe permanecer dentro de la raíz mustflow",
779
+ "verify.error.changed_files_unavailable": "No se pudieron inspeccionar los archivos cambiados con git status",
771
780
  "verify.error.invalid_repro_evidence_file": "La evidencia de reproducción debe ser un resumen JSON legible con campos de evidencia estructurados",
772
781
  "verify.error.unsupported_repro_evidence_source": "La entrada de evidencia de reproducción debe usar command repro-evidence",
773
782
  "verify.error.invalid_external_evidence_file": "La evidencia externa debe ser un resumen JSON legible con checks",
@@ -664,8 +664,12 @@ Lisez ces fichiers avant de travailler :
664
664
  "run.error.unsafeIntentDetail": "Utilisez un nom d’intention sûr pour le shell.",
665
665
  "run.error.blockedShellBackground": 'L’intention "{intent}" est bloquée. {detail}',
666
666
  "run.error.blockedShellBackgroundDetail": "Les commandes shell ne doivent pas lancer de travail en arrière-plan.",
667
+ "run.error.blockedLongRunningCommand": 'L’intention "{intent}" est bloquée. {detail}',
668
+ "run.error.blockedLongRunningCommandDetail": "argv doit décrire une commande ponctuelle finie, pas un serveur de développement, un watcher, un wrapper shell, une boucle d'interpréteur ou un processus en arrière-plan.",
667
669
  "run.error.cwdOutsideProject": 'La commande "{intent}" a un cwd non valide : {detail}',
668
670
  "run.error.cwdOutsideProjectDetail": "Le cwd de l’intention doit rester dans la racine actuelle.",
671
+ "run.error.maxOutputBytes": 'La commande "{intent}" a une valeur max_output_bytes non valide. {detail}',
672
+ "run.error.maxOutputBytesDetail": "La limite de sortie doit rester dans le maximum autorisé.",
669
673
  "run.error.conflictingPreviewModes": "Utilisez --dry-run ou --plan-only, pas les deux",
670
674
  "run.error.timedOut": 'La commande "{intent}" a expiré après {seconds} secondes',
671
675
  "run.error.startFailed": 'Impossible de démarrer la commande "{intent}" : {message}',
@@ -727,6 +731,7 @@ Lisez ces fichiers avant de travailler :
727
731
  "classify.source.changed": "fichiers modifies",
728
732
  "classify.source.paths": "chemins explicites",
729
733
  "classify.error.missingInput": "Indiquez --changed ou au moins un chemin",
734
+ "classify.error.changed_files_unavailable": "Impossible d'inspecter les fichiers modifies avec git status",
730
735
  "classify.error.write_path_outside_root": "Le chemin du rapport de classification doit rester dans la racine mustflow",
731
736
  "impact.help.summary": "Signale si les chemins modifies exigent une decision de version de paquet ou de modele sans modifier les fichiers.",
732
737
  "impact.help.option.changed": "Lire les chemins depuis git status --short --untracked-files=all",
@@ -741,14 +746,16 @@ Lisez ces fichiers avant de travailler :
741
746
  "impact.label.affectedVersionSources": "Sources de version affectees",
742
747
  "impact.label.affectedSurfaces": "Surfaces affectees",
743
748
  "impact.error.missingInput": "Indiquez --changed ou au moins un chemin",
749
+ "impact.error.changed_files_unavailable": "Impossible d'inspecter les fichiers modifies avec git status",
744
750
  "verify.help.summary": "Exécute les intentions de vérification configurées sélectionnées par les métadonnées required_after.",
745
751
  "verify.help.option.reason": "Sélectionne la raison required_after à vérifier",
746
752
  "verify.help.option.fromClassification": "Lit les raisons de vérification depuis un rapport mf classify dans ce dépôt",
747
- "verify.help.option.fromPlan": "Alias de compatibilité pour --from-classification",
753
+ "verify.help.option.fromPlan": "Alias de compatibilité obsolète pour --from-classification; il attend toujours un rapport mf classify",
748
754
  "verify.help.option.changed": "Classe les changements Git actuels et vérifie les raisons correspondantes",
749
755
  "verify.help.option.writePlan": "Option de compatibilité qui écrit le rapport de classification des changements",
750
756
  "verify.help.option.reproEvidence": "Lit une preuve structurée de reproduction de bogue depuis un résumé JSON local au dépôt",
751
757
  "verify.help.option.externalEvidence": "Lit une preuve CI externe de moindre autorité depuis un résumé JSON local au dépôt",
758
+ "verify.help.option.parallel": "Exécute les lots planifiés sûrs et sans conflit avec au plus ce nombre de commandes ; valeur par défaut : 1",
752
759
  "verify.help.option.planOnly": "Affiche le plan de vérification sans exécuter de commandes; nécessite --json",
753
760
  "verify.help.exit.ok": "Toutes les intentions de vérification sélectionnées ont réussi",
754
761
  "verify.help.exit.fail": "La vérification a échoué, est partielle, est bloquée ou l'entrée est invalide",
@@ -763,11 +770,13 @@ Lisez ces fichiers avant de travailler :
763
770
  "verify.error.planOnlyJson": "--plan-only nécessite --json",
764
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence ne peut pas être utilisé avec --plan-only",
765
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence ne peut pas être utilisé avec --plan-only",
773
+ "verify.error.invalidParallel": "--parallel doit être un entier positif",
766
774
  "verify.error.invalid_plan_file": "Le rapport de classification doit être un fichier JSON lisible",
767
775
  "verify.error.unsupported_plan_source": "L'entrée de vérification doit être un rapport mf classify",
768
776
  "verify.error.plan_root_mismatch": "Le rapport de classification doit venir de cette racine mustflow",
769
777
  "verify.error.missing_plan_reasons": "Le rapport de classification doit inclure summary.validationReasons",
770
778
  "verify.error.plan_path_outside_root": "Le chemin du rapport de classification doit rester dans la racine mustflow",
779
+ "verify.error.changed_files_unavailable": "Impossible d'inspecter les fichiers modifies avec git status",
771
780
  "verify.error.invalid_repro_evidence_file": "La preuve de reproduction doit être un résumé JSON lisible avec des champs de preuve structurés",
772
781
  "verify.error.unsupported_repro_evidence_source": "L'entrée de preuve de reproduction doit utiliser command repro-evidence",
773
782
  "verify.error.invalid_external_evidence_file": "La preuve externe doit être un résumé JSON lisible avec checks",