mustflow 2.17.0 → 2.18.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 CHANGED
@@ -354,7 +354,7 @@ Runnable work is declared in `.mustflow/config/commands.toml` so agents do not g
354
354
  - `run_policy = "agent_allowed"`
355
355
  - `stdin = "closed"`
356
356
 
357
- Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly.
357
+ Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly. `mf run` also rejects obvious long-running `argv` shapes, such as shell-wrapper background payloads, interpreter loops, package-manager development scripts, watchers, and development servers declared as one-shot commands.
358
358
 
359
359
  Use `mf verify --reason <event> --plan-only --json` to inspect matching verification intents, command eligibility, remaining gaps, and missing runnable coverage without executing commands. Use `mf run <intent> --dry-run --json` to inspect one resolved command intent without spawning a process or writing a run receipt. Plan-only verification includes a `decision_graph` that connects changed surfaces, classification reasons, command candidates, eligibility checks, effects, and gaps. When `.mustflow/cache/mustflow.sqlite` is fresh, scheduled entries also include read-only `effectGraph` metadata for write locks and lock conflicts. These graph rows are marked `explanation_only` and never grant command authority; `.mustflow/config/commands.toml` remains the only runnable command source.
360
360
 
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createChangeClassificationReport, } from '../../core/change-classification.js';
4
4
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
5
- import { readGitChangedFiles } from '../lib/git-changes.js';
5
+ import { requireGitChangedFiles } from '../lib/git-changes.js';
6
6
  import { t } from '../lib/i18n.js';
7
7
  import { resolveMustflowRoot } from '../lib/project-root.js';
8
8
  const CLASSIFY_SCHEMA_VERSION = '1';
@@ -67,7 +67,7 @@ function parseClassifyArgs(args) {
67
67
  return { json, changed, writePath, paths };
68
68
  }
69
69
  export function createClassifyOutput(projectRoot, source, paths) {
70
- const files = source === 'changed' ? readGitChangedFiles(projectRoot) : paths;
70
+ const files = source === 'changed' ? requireGitChangedFiles(projectRoot) : paths;
71
71
  return {
72
72
  schema_version: CLASSIFY_SCHEMA_VERSION,
73
73
  command: 'classify',
@@ -136,7 +136,17 @@ export function runClassify(args, reporter, lang = 'en') {
136
136
  return 1;
137
137
  }
138
138
  const projectRoot = resolveMustflowRoot();
139
- const output = createClassifyOutput(projectRoot, parsed.changed ? 'changed' : 'paths', parsed.paths);
139
+ let output;
140
+ try {
141
+ output = createClassifyOutput(projectRoot, parsed.changed ? 'changed' : 'paths', parsed.paths);
142
+ }
143
+ catch (error) {
144
+ const message = error instanceof Error && error.message === 'git_changed_files_unavailable'
145
+ ? t(lang, 'classify.error.changed_files_unavailable')
146
+ : t(lang, 'cli.common.invalidInput');
147
+ printUsageError(reporter, message, 'mf classify --help', getClassifyHelp(lang), lang);
148
+ return 1;
149
+ }
140
150
  if (parsed.writePath) {
141
151
  try {
142
152
  writeClassifyOutput(projectRoot, parsed.writePath, output);
@@ -682,7 +682,8 @@ async function renderStatusResponse(projectRoot) {
682
682
  const activeDocuments = listDocReviewEntries(projectRoot);
683
683
  const rawCommandContract = readDashboardCommandContract(projectRoot);
684
684
  const commandContract = await renderCommandContractResponse(projectRoot, rawCommandContract);
685
- const gitChangedFiles = readGitChangedFiles(projectRoot);
685
+ const gitChangedFilesResult = readGitChangedFiles(projectRoot);
686
+ const gitChangedFiles = gitChangedFilesResult.ok ? gitChangedFilesResult.files : [];
686
687
  const packageMetadata = readPackageMetadata();
687
688
  const verification = createDashboardVerificationSnapshot(projectRoot, rawCommandContract, commandContract.intents, gitChangedFiles, manifest.changedFiles, manifest.missingFiles);
688
689
  const readModel = await readLatestLocalVerificationReadModelQueries(projectRoot);
@@ -3,7 +3,7 @@ import { createChangeClassificationReport } from '../../core/change-classificati
3
3
  import { summarizeVersionImpact } from '../../core/version-impact.js';
4
4
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
5
5
  import { isRecord } from '../lib/command-contract.js';
6
- import { readGitChangedFiles } from '../lib/git-changes.js';
6
+ import { requireGitChangedFiles } from '../lib/git-changes.js';
7
7
  import { t } from '../lib/i18n.js';
8
8
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
9
  import { readTomlFile } from '../lib/toml.js';
@@ -56,7 +56,7 @@ function readPreferences(projectRoot) {
56
56
  }
57
57
  function createImpactOutput(projectRoot, parsed) {
58
58
  const source = parsed.changed ? 'changed' : 'paths';
59
- const files = parsed.changed ? readGitChangedFiles(projectRoot) : parsed.paths;
59
+ const files = parsed.changed ? requireGitChangedFiles(projectRoot) : parsed.paths;
60
60
  const classificationReport = createChangeClassificationReport(source, files);
61
61
  const versionSources = detectVersionSources(projectRoot);
62
62
  return {
@@ -110,7 +110,17 @@ export function runImpact(args, reporter, lang = 'en') {
110
110
  printUsageError(reporter, t(lang, 'impact.error.missingInput'), 'mf impact --help', getImpactHelp(lang), lang);
111
111
  return 1;
112
112
  }
113
- const output = createImpactOutput(resolveMustflowRoot(), parsed);
113
+ let output;
114
+ try {
115
+ output = createImpactOutput(resolveMustflowRoot(), parsed);
116
+ }
117
+ catch (error) {
118
+ const message = error instanceof Error && error.message === 'git_changed_files_unavailable'
119
+ ? t(lang, 'impact.error.changed_files_unavailable')
120
+ : t(lang, 'cli.common.invalidInput');
121
+ printUsageError(reporter, message, 'mf impact --help', getImpactHelp(lang), lang);
122
+ return 1;
123
+ }
114
124
  if (parsed.json) {
115
125
  reporter.stdout(JSON.stringify(output, null, 2));
116
126
  return 0;
@@ -24,7 +24,7 @@ function emitOutput(reporter, output, stream) {
24
24
  }
25
25
  reporter[stream](text);
26
26
  }
27
- function terminateProcessTree(pid) {
27
+ function signalProcessTree(pid, signal) {
28
28
  if (!pid || pid <= 0) {
29
29
  return;
30
30
  }
@@ -33,20 +33,73 @@ function terminateProcessTree(pid) {
33
33
  stdio: 'ignore',
34
34
  windowsHide: true,
35
35
  });
36
+ if (signal === 'SIGKILL') {
37
+ try {
38
+ process.kill(pid, signal);
39
+ }
40
+ catch {
41
+ // taskkill may already have terminated the direct child.
42
+ }
43
+ }
36
44
  return;
37
45
  }
38
46
  try {
39
- process.kill(-pid, 'SIGTERM');
47
+ process.kill(-pid, signal);
40
48
  }
41
49
  catch {
42
50
  try {
43
- process.kill(pid, 'SIGTERM');
51
+ process.kill(pid, signal);
44
52
  }
45
53
  catch {
46
54
  // The child may already be gone after Node's spawn timeout handling.
47
55
  }
48
56
  }
49
57
  }
58
+ function signalProcessTreeNonBlocking(pid, signal) {
59
+ if (!pid || pid <= 0) {
60
+ return;
61
+ }
62
+ if (process.platform === 'win32') {
63
+ const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
64
+ stdio: 'ignore',
65
+ windowsHide: true,
66
+ detached: true,
67
+ });
68
+ killer.unref();
69
+ if (signal === 'SIGKILL') {
70
+ try {
71
+ process.kill(pid, signal);
72
+ }
73
+ catch {
74
+ // taskkill may already have terminated the direct child.
75
+ }
76
+ }
77
+ return;
78
+ }
79
+ try {
80
+ process.kill(-pid, signal);
81
+ }
82
+ catch {
83
+ try {
84
+ process.kill(pid, signal);
85
+ }
86
+ catch {
87
+ // The child may already be gone after the timeout fired.
88
+ }
89
+ }
90
+ }
91
+ function terminateProcessTree(pid) {
92
+ signalProcessTree(pid, 'SIGTERM');
93
+ }
94
+ function forceTerminateProcessTree(pid) {
95
+ signalProcessTree(pid, 'SIGKILL');
96
+ }
97
+ function terminateProcessTreeNonBlocking(pid) {
98
+ signalProcessTreeNonBlocking(pid, 'SIGTERM');
99
+ }
100
+ function forceTerminateProcessTreeNonBlocking(pid) {
101
+ signalProcessTreeNonBlocking(pid, 'SIGKILL');
102
+ }
50
103
  function getKillMethod() {
51
104
  return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
52
105
  }
@@ -178,14 +231,14 @@ function writeStreamChunk(reporter, stream, chunk) {
178
231
  reporter.writeStdout(chunk);
179
232
  return;
180
233
  }
181
- reporter.stdout(chunk.toString().trimEnd());
234
+ reporter.stdout(chunk.toString());
182
235
  return;
183
236
  }
184
237
  if (reporter.writeStderr) {
185
238
  reporter.writeStderr(chunk);
186
239
  return;
187
240
  }
188
- reporter.stderr(chunk.toString().trimEnd());
241
+ reporter.stderr(chunk.toString());
189
242
  }
190
243
  function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
191
244
  return new Promise((resolve) => {
@@ -214,8 +267,8 @@ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBy
214
267
  clearTimeout(timeout);
215
268
  }
216
269
  resolve({
217
- status,
218
- signal,
270
+ status: timedOut ? null : status,
271
+ signal: timedOut ? null : signal,
219
272
  error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
220
273
  stdout: stdout.toSnapshot(),
221
274
  stderr: stderr.toSnapshot(),
@@ -238,7 +291,12 @@ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBy
238
291
  });
239
292
  timeout = setTimeout(() => {
240
293
  timedOut = true;
241
- terminateProcessTree(childPid);
294
+ child.stdout?.destroy();
295
+ child.stderr?.destroy();
296
+ child.unref();
297
+ terminateProcessTreeNonBlocking(childPid);
298
+ forceTerminateProcessTreeNonBlocking(childPid);
299
+ finish(null, null);
242
300
  }, timeoutSeconds * 1000);
243
301
  });
244
302
  }
@@ -282,8 +340,8 @@ function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailB
282
340
  clearTimeout(timeout);
283
341
  }
284
342
  resolve({
285
- status,
286
- signal,
343
+ status: timedOut ? null : status,
344
+ signal: timedOut ? null : signal,
287
345
  error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
288
346
  stdout: stdout.toSnapshot(),
289
347
  stderr: stderr.toSnapshot(),
@@ -306,7 +364,12 @@ function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailB
306
364
  });
307
365
  timeout = setTimeout(() => {
308
366
  timedOut = true;
309
- terminateProcessTree(childPid);
367
+ child.stdout?.destroy();
368
+ child.stderr?.destroy();
369
+ child.unref();
370
+ terminateProcessTreeNonBlocking(childPid);
371
+ forceTerminateProcessTreeNonBlocking(childPid);
372
+ finish(null, null);
310
373
  }, timeoutSeconds * 1000);
311
374
  });
312
375
  }
@@ -356,12 +419,24 @@ function reportRunPlanFailure(plan, reporter, lang) {
356
419
  detail: getRunPlanDetail(plan, lang, 'run.error.blockedShellBackgroundDetail'),
357
420
  });
358
421
  break;
422
+ case 'blocked_long_running_command_pattern':
423
+ message = t(lang, 'run.error.blockedLongRunningCommand', {
424
+ intent: plan.intentName,
425
+ detail: getRunPlanDetail(plan, lang, 'run.error.blockedLongRunningCommandDetail'),
426
+ });
427
+ break;
359
428
  case 'cwd_outside_project':
360
429
  message = t(lang, 'run.error.cwdOutsideProject', {
361
430
  intent: plan.intentName,
362
431
  detail: getRunPlanDetail(plan, lang, 'run.error.cwdOutsideProjectDetail'),
363
432
  });
364
433
  break;
434
+ case 'max_output_bytes_exceeds_limit':
435
+ message = t(lang, 'run.error.maxOutputBytes', {
436
+ intent: plan.intentName,
437
+ detail: getRunPlanDetail(plan, lang, 'run.error.maxOutputBytesDetail'),
438
+ });
439
+ break;
365
440
  case 'intent_not_table':
366
441
  default:
367
442
  message = t(lang, 'run.error.unknownIntent', { intent: plan.intentName });
@@ -764,6 +764,8 @@ export function planErrorMessageKey(code) {
764
764
  return 'verify.error.unsupported_plan_source';
765
765
  case 'plan_root_mismatch':
766
766
  return 'verify.error.plan_root_mismatch';
767
+ case 'git_changed_files_unavailable':
768
+ return 'verify.error.changed_files_unavailable';
767
769
  default:
768
770
  return 'verify.error.invalid_plan_file';
769
771
  }
@@ -1293,6 +1295,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1293
1295
  reasons: outputWithReceiptPaths.reasons,
1294
1296
  plan_source: outputWithReceiptPaths.plan_source,
1295
1297
  verification_plan_id: outputWithReceiptPaths.verification_plan_id,
1298
+ execution_status: outputWithReceiptPaths.execution_status,
1296
1299
  status: outputWithReceiptPaths.status,
1297
1300
  completion_verdict: outputWithReceiptPaths.completion_verdict,
1298
1301
  evidence_model: outputWithReceiptPaths.evidence_model,
@@ -1312,6 +1315,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1312
1315
  reasons: outputWithReceiptPaths.reasons,
1313
1316
  plan_source: outputWithReceiptPaths.plan_source,
1314
1317
  verification_plan_id: outputWithReceiptPaths.verification_plan_id,
1318
+ execution_status: outputWithReceiptPaths.execution_status,
1315
1319
  status: outputWithReceiptPaths.status,
1316
1320
  completion_verdict: outputWithReceiptPaths.completion_verdict,
1317
1321
  evidence_model: outputWithReceiptPaths.evidence_model,
@@ -1403,6 +1407,7 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
1403
1407
  reasons: input.reasons,
1404
1408
  plan_source: planSource,
1405
1409
  verification_plan_id: verificationPlanId,
1410
+ execution_status: status,
1406
1411
  status,
1407
1412
  completion_verdict: completionVerdict,
1408
1413
  evidence_model: evidenceModel,
@@ -1538,6 +1543,9 @@ export async function runVerify(args, reporter, lang = 'en') {
1538
1543
  let reproEvidence = null;
1539
1544
  let externalChecks = [];
1540
1545
  try {
1546
+ if (parsed.writePlan) {
1547
+ resolvePlanPath(projectRoot, parsed.writePlan);
1548
+ }
1541
1549
  if (parsed.changed) {
1542
1550
  const changedInput = createInputFromChanged(projectRoot);
1543
1551
  input = changedInput.input;
@@ -1587,5 +1595,5 @@ export async function runVerify(args, reporter, lang = 'en') {
1587
1595
  else {
1588
1596
  reporter.stdout(renderVerifyOutput(output, lang));
1589
1597
  }
1590
- return output.status === 'passed' ? 0 : 1;
1598
+ return output.completion_verdict.status === 'verified' ? 0 : 1;
1591
1599
  }
@@ -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,6 +746,7 @@ 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",
@@ -768,6 +774,7 @@ Read these files before working:
768
774
  "verify.error.plan_root_mismatch": "Classification report must come from this mustflow root",
769
775
  "verify.error.missing_plan_reasons": "Classification report must include summary.validationReasons",
770
776
  "verify.error.plan_path_outside_root": "Classification report path must stay inside the mustflow root",
777
+ "verify.error.changed_files_unavailable": "Unable to inspect changed files with git status",
771
778
  "verify.error.invalid_repro_evidence_file": "Repro evidence must be a readable JSON summary with structured evidence fields",
772
779
  "verify.error.unsupported_repro_evidence_source": "Repro evidence input must use command repro-evidence",
773
780
  "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,6 +746,7 @@ 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",
@@ -768,6 +774,7 @@ Lee estos archivos antes de trabajar:
768
774
  "verify.error.plan_root_mismatch": "El informe de clasificación debe provenir de esta raíz mustflow",
769
775
  "verify.error.missing_plan_reasons": "El informe de clasificación debe incluir summary.validationReasons",
770
776
  "verify.error.plan_path_outside_root": "La ruta del informe de clasificación debe permanecer dentro de la raíz mustflow",
777
+ "verify.error.changed_files_unavailable": "No se pudieron inspeccionar los archivos cambiados con git status",
771
778
  "verify.error.invalid_repro_evidence_file": "La evidencia de reproducción debe ser un resumen JSON legible con campos de evidencia estructurados",
772
779
  "verify.error.unsupported_repro_evidence_source": "La entrada de evidencia de reproducción debe usar command repro-evidence",
773
780
  "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,6 +746,7 @@ 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",
@@ -768,6 +774,7 @@ Lisez ces fichiers avant de travailler :
768
774
  "verify.error.plan_root_mismatch": "Le rapport de classification doit venir de cette racine mustflow",
769
775
  "verify.error.missing_plan_reasons": "Le rapport de classification doit inclure summary.validationReasons",
770
776
  "verify.error.plan_path_outside_root": "Le chemin du rapport de classification doit rester dans la racine mustflow",
777
+ "verify.error.changed_files_unavailable": "Impossible d'inspecter les fichiers modifies avec git status",
771
778
  "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
779
  "verify.error.unsupported_repro_evidence_source": "L'entrée de preuve de reproduction doit utiliser command repro-evidence",
773
780
  "verify.error.invalid_external_evidence_file": "La preuve externe doit être un résumé JSON lisible avec checks",
@@ -664,8 +664,12 @@ export const hiMessages = {
664
664
  "run.error.unsafeIntentDetail": "shell-safe इंटेंट नाम इस्तेमाल करें।",
665
665
  "run.error.blockedShellBackground": 'इंटेंट "{intent}" अवरुद्ध है। {detail}',
666
666
  "run.error.blockedShellBackgroundDetail": "Shell commands background work शुरू नहीं कर सकतीं।",
667
+ "run.error.blockedLongRunningCommand": 'इंटेंट "{intent}" अवरुद्ध है। {detail}',
668
+ "run.error.blockedLongRunningCommandDetail": "argv में finite one-shot command होना चाहिए, development server, watcher, shell wrapper, interpreter loop, या background process नहीं।",
667
669
  "run.error.cwdOutsideProject": 'कमांड "{intent}" का cwd अमान्य है: {detail}',
668
670
  "run.error.cwdOutsideProjectDetail": "Intent cwd current root के अंदर रहना चाहिए।",
671
+ "run.error.maxOutputBytes": 'कमांड "{intent}" में max_output_bytes अमान्य है। {detail}',
672
+ "run.error.maxOutputBytesDetail": "Output limit अनुमत maximum के अंदर रहनी चाहिए।",
669
673
  "run.error.conflictingPreviewModes": "--dry-run या --plan-only में से एक इस्तेमाल करें, दोनों नहीं",
670
674
  "run.error.timedOut": 'कमांड "{intent}" {seconds} सेकंड बाद time out हुई',
671
675
  "run.error.startFailed": 'कमांड "{intent}" शुरू नहीं हो सकी: {message}',
@@ -727,6 +731,7 @@ export const hiMessages = {
727
731
  "classify.source.changed": "बदली फ़ाइलें",
728
732
  "classify.source.paths": "दिए गए पथ",
729
733
  "classify.error.missingInput": "--changed या कम से कम एक पथ दें",
734
+ "classify.error.changed_files_unavailable": "git status से बदली फ़ाइलें नहीं पढ़ी जा सकीं",
730
735
  "classify.error.write_path_outside_root": "Classification report path mustflow root के अंदर रहना चाहिए",
731
736
  "impact.help.summary": "फ़ाइल बदले बिना बताएं कि बदले पथ package या template version decision मांगते हैं या नहीं.",
732
737
  "impact.help.option.changed": "git status --short --untracked-files=all से पथ पढ़ें",
@@ -741,6 +746,7 @@ export const hiMessages = {
741
746
  "impact.label.affectedVersionSources": "Affected version sources",
742
747
  "impact.label.affectedSurfaces": "Affected surfaces",
743
748
  "impact.error.missingInput": "--changed या कम से कम एक पथ दें",
749
+ "impact.error.changed_files_unavailable": "git status से बदली फ़ाइलें नहीं पढ़ी जा सकीं",
744
750
  "verify.help.summary": "required_after metadata से चुने गए configured verification intents चलाएँ।",
745
751
  "verify.help.option.reason": "Verify करने के लिए required_after reason चुनें",
746
752
  "verify.help.option.fromClassification": "इस repository के अंदर mf classify report से verification reasons पढ़ें",
@@ -768,6 +774,7 @@ export const hiMessages = {
768
774
  "verify.error.plan_root_mismatch": "Classification report इसी mustflow root से आना चाहिए",
769
775
  "verify.error.missing_plan_reasons": "Classification report में summary.validationReasons होना चाहिए",
770
776
  "verify.error.plan_path_outside_root": "Classification report path mustflow root के अंदर रहना चाहिए",
777
+ "verify.error.changed_files_unavailable": "git status से बदली फ़ाइलें नहीं पढ़ी जा सकीं",
771
778
  "verify.error.invalid_repro_evidence_file": "Repro evidence structured evidence fields वाला readable JSON summary होना चाहिए",
772
779
  "verify.error.unsupported_repro_evidence_source": "Repro evidence input को command repro-evidence इस्तेमाल करना चाहिए",
773
780
  "verify.error.invalid_external_evidence_file": "External evidence checks वाला readable JSON summary होना चाहिए",
@@ -664,8 +664,12 @@ export const koMessages = {
664
664
  "run.error.unsafeIntentDetail": "셸에서 안전한 명령 의도 이름을 사용하세요.",
665
665
  "run.error.blockedShellBackground": '명령 의도 "{intent}"가 차단되었습니다. {detail}',
666
666
  "run.error.blockedShellBackgroundDetail": "셸 명령은 백그라운드 작업을 시작하면 안 됩니다.",
667
+ "run.error.blockedLongRunningCommand": '명령 의도 "{intent}"가 차단되었습니다. {detail}',
668
+ "run.error.blockedLongRunningCommandDetail": "argv는 개발 서버, 감시 명령, 셸 래퍼, 인터프리터 반복 작업, 백그라운드 프로세스가 아니라 끝나는 단발성 명령이어야 합니다.",
667
669
  "run.error.cwdOutsideProject": '명령 "{intent}"의 실행 위치(cwd)가 올바르지 않습니다: {detail}',
668
670
  "run.error.cwdOutsideProjectDetail": "명령 실행 위치(cwd)는 현재 루트 안에 있어야 합니다.",
671
+ "run.error.maxOutputBytes": '명령 "{intent}"의 max_output_bytes 값이 올바르지 않습니다. {detail}',
672
+ "run.error.maxOutputBytesDetail": "출력 상한은 허용된 최댓값 안에 있어야 합니다.",
669
673
  "run.error.conflictingPreviewModes": "--dry-run과 --plan-only 중 하나만 사용하세요",
670
674
  "run.error.timedOut": '명령 "{intent}"가 {seconds}초 뒤 시간 초과되었습니다',
671
675
  "run.error.startFailed": '명령 "{intent}"를 시작하지 못했습니다: {message}',
@@ -727,6 +731,7 @@ export const koMessages = {
727
731
  "classify.source.changed": "변경 파일",
728
732
  "classify.source.paths": "지정한 경로",
729
733
  "classify.error.missingInput": "--changed 또는 하나 이상의 경로를 지정하세요",
734
+ "classify.error.changed_files_unavailable": "git status로 변경 파일을 확인할 수 없습니다",
730
735
  "classify.error.write_path_outside_root": "분류 보고서 경로는 mustflow 루트 안에 있어야 합니다",
731
736
  "impact.help.summary": "파일을 수정하지 않고 변경 경로가 패키지나 템플릿 버전 결정을 요구하는지 보고합니다.",
732
737
  "impact.help.option.changed": "git status --short --untracked-files=all에서 경로를 읽습니다",
@@ -741,6 +746,7 @@ export const koMessages = {
741
746
  "impact.label.affectedVersionSources": "영향받은 버전 기준 원본",
742
747
  "impact.label.affectedSurfaces": "영향받은 공개 표면",
743
748
  "impact.error.missingInput": "--changed 또는 하나 이상의 경로를 지정하세요",
749
+ "impact.error.changed_files_unavailable": "git status로 변경 파일을 확인할 수 없습니다",
744
750
  "verify.help.summary": "required_after 메타데이터로 선택된 설정된 검증 의도를 실행합니다.",
745
751
  "verify.help.option.reason": "검증할 required_after 이유를 지정합니다",
746
752
  "verify.help.option.fromClassification": "이 저장소 안의 mf classify 보고서에서 검증 이유를 읽습니다",
@@ -768,6 +774,7 @@ export const koMessages = {
768
774
  "verify.error.plan_root_mismatch": "분류 보고서는 현재 mustflow 루트에서 나온 것이어야 합니다",
769
775
  "verify.error.missing_plan_reasons": "분류 보고서에는 summary.validationReasons가 있어야 합니다",
770
776
  "verify.error.plan_path_outside_root": "분류 보고서 경로는 mustflow 루트 안에 있어야 합니다",
777
+ "verify.error.changed_files_unavailable": "git status로 변경 파일을 확인할 수 없습니다",
771
778
  "verify.error.invalid_repro_evidence_file": "재현 증거는 구조화된 증거 필드를 포함한 읽을 수 있는 JSON 요약이어야 합니다",
772
779
  "verify.error.unsupported_repro_evidence_source": "재현 증거 입력은 command repro-evidence를 사용해야 합니다",
773
780
  "verify.error.invalid_external_evidence_file": "외부 증거는 checks를 포함한 읽을 수 있는 JSON 요약이어야 합니다",
@@ -664,8 +664,12 @@ export const zhMessages = {
664
664
  "run.error.unsafeIntentDetail": "请使用 shell 安全的意图名称。",
665
665
  "run.error.blockedShellBackground": '意图 "{intent}" 已被阻止。{detail}',
666
666
  "run.error.blockedShellBackgroundDetail": "Shell 命令不得启动后台工作。",
667
+ "run.error.blockedLongRunningCommand": '意图 "{intent}" 已被阻止。{detail}',
668
+ "run.error.blockedLongRunningCommandDetail": "argv 必须描述会结束的单次命令,而不是开发服务器、监听命令、shell 包装器、解释器循环或后台进程。",
667
669
  "run.error.cwdOutsideProject": '命令 "{intent}" 的 cwd 无效:{detail}',
668
670
  "run.error.cwdOutsideProjectDetail": "意图 cwd 必须位于当前根目录内。",
671
+ "run.error.maxOutputBytes": '命令 "{intent}" 的 max_output_bytes 无效。{detail}',
672
+ "run.error.maxOutputBytesDetail": "输出限制必须保持在允许的最大值内。",
669
673
  "run.error.conflictingPreviewModes": "只能使用 --dry-run 或 --plan-only,不能同时使用",
670
674
  "run.error.timedOut": '命令 "{intent}" 在 {seconds} 秒后超时',
671
675
  "run.error.startFailed": '命令 "{intent}" 启动失败:{message}',
@@ -727,6 +731,7 @@ export const zhMessages = {
727
731
  "classify.source.changed": "变更文件",
728
732
  "classify.source.paths": "指定路径",
729
733
  "classify.error.missingInput": "请指定 --changed 或至少一个路径",
734
+ "classify.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
730
735
  "classify.error.write_path_outside_root": "分类报告路径必须位于 mustflow 根目录内",
731
736
  "impact.help.summary": "在不修改文件的情况下报告变更路径是否需要包或模板版本决策。",
732
737
  "impact.help.option.changed": "从 git status --short --untracked-files=all 读取路径",
@@ -741,6 +746,7 @@ export const zhMessages = {
741
746
  "impact.label.affectedVersionSources": "受影响的版本来源",
742
747
  "impact.label.affectedSurfaces": "受影响的公开表面",
743
748
  "impact.error.missingInput": "请指定 --changed 或至少一个路径",
749
+ "impact.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
744
750
  "verify.help.summary": "运行由 required_after 元数据选出的已配置验证意图。",
745
751
  "verify.help.option.reason": "选择要验证的 required_after 原因",
746
752
  "verify.help.option.fromClassification": "从此仓库内的 mf classify 报告读取验证原因",
@@ -768,6 +774,7 @@ export const zhMessages = {
768
774
  "verify.error.plan_root_mismatch": "分类报告必须来自当前 mustflow 根目录",
769
775
  "verify.error.missing_plan_reasons": "分类报告必须包含 summary.validationReasons",
770
776
  "verify.error.plan_path_outside_root": "分类报告路径必须位于 mustflow 根目录内",
777
+ "verify.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
771
778
  "verify.error.invalid_repro_evidence_file": "复现证据必须是包含结构化证据字段的可读取 JSON 摘要",
772
779
  "verify.error.unsupported_repro_evidence_source": "复现证据输入必须使用 command repro-evidence",
773
780
  "verify.error.invalid_external_evidence_file": "外部证据必须是包含 checks 的可读取 JSON 摘要",
@@ -1,5 +1,13 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { parseGitStatusOutput } from '../../core/change-classification.js';
3
+ export class GitChangedFilesError extends Error {
4
+ result;
5
+ constructor(result) {
6
+ super('git_changed_files_unavailable');
7
+ this.name = 'GitChangedFilesError';
8
+ this.result = result;
9
+ }
10
+ }
3
11
  export function readGitChangedFiles(projectRoot) {
4
12
  const result = spawnSync('git', ['status', '--short', '--untracked-files=all'], {
5
13
  cwd: projectRoot,
@@ -7,7 +15,22 @@ export function readGitChangedFiles(projectRoot) {
7
15
  windowsHide: true,
8
16
  });
9
17
  if (result.status !== 0 || typeof result.stdout !== 'string') {
10
- return [];
18
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
19
+ const message = result.error?.message ??
20
+ (stderr || (result.status === null ? 'git status did not complete' : `git status exited with code ${result.status}`));
21
+ return {
22
+ ok: false,
23
+ message,
24
+ status: result.status,
25
+ stderr,
26
+ };
27
+ }
28
+ return { ok: true, files: parseGitStatusOutput(result.stdout) };
29
+ }
30
+ export function requireGitChangedFiles(projectRoot) {
31
+ const result = readGitChangedFiles(projectRoot);
32
+ if (!result.ok) {
33
+ throw new GitChangedFilesError(result);
11
34
  }
12
- return parseGitStatusOutput(result.stdout);
35
+ return result.files;
13
36
  }
@@ -1,4 +1,4 @@
1
- export const LOCAL_INDEX_SCHEMA_VERSION = '19';
1
+ export const LOCAL_INDEX_SCHEMA_VERSION = '20';
2
2
  export const LOCAL_INDEX_PARSER_VERSION = '1';
3
3
  export const DEFAULT_DATABASE_RELATIVE_PATH = '.mustflow/cache/mustflow.sqlite';
4
4
  export const LATEST_RUN_STATE_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
@@ -22,6 +22,9 @@ export const SEARCH_MATCH_CONTEXT_AFTER_CHARS = 96;
22
22
  export const SEARCH_MATCH_TRUNCATION_MARKER = '...';
23
23
  export const SEARCH_NGRAM_MIN_LENGTH = 2;
24
24
  export const SEARCH_NGRAM_MAX_LENGTH = 3;
25
+ export const SEARCH_NGRAM_MAX_TOKEN_CHARS = 64;
26
+ export const SEARCH_NGRAM_MAX_GRAMS_PER_TARGET = 512;
27
+ export const SOURCE_INDEX_MAX_FILE_BYTES = 262144;
25
28
  export const SEARCH_BACKEND_FTS5 = 'fts5';
26
29
  export const SEARCH_BACKEND_TABLE_SCAN = 'table_scan';
27
30
  export const TEST_DISABLE_FTS5_ENV = 'MUSTFLOW_TEST_DISABLE_FTS5';