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.
Files changed (44) hide show
  1. package/README.md +12 -2
  2. package/dist/cli/commands/run.js +221 -48
  3. package/dist/cli/commands/upgrade.js +65 -0
  4. package/dist/cli/commands/verify.js +79 -7
  5. package/dist/cli/i18n/en.js +12 -0
  6. package/dist/cli/i18n/es.js +12 -0
  7. package/dist/cli/i18n/fr.js +12 -0
  8. package/dist/cli/i18n/hi.js +12 -0
  9. package/dist/cli/i18n/ko.js +12 -0
  10. package/dist/cli/i18n/zh.js +12 -0
  11. package/dist/cli/index.js +27 -46
  12. package/dist/cli/lib/command-registry.js +5 -0
  13. package/dist/cli/lib/dashboard-html.js +1 -1
  14. package/dist/cli/lib/local-index.js +11 -8
  15. package/dist/cli/lib/reporter.js +6 -0
  16. package/dist/cli/lib/run-plan.js +20 -3
  17. package/dist/cli/lib/validation.js +110 -1
  18. package/dist/core/bounded-output.js +38 -0
  19. package/dist/core/change-classification.js +6 -2
  20. package/dist/core/change-verification.js +240 -6
  21. package/dist/core/check-issues.js +6 -0
  22. package/dist/core/command-contract-validation.js +20 -0
  23. package/dist/core/command-effects.js +13 -0
  24. package/dist/core/contract-lint.js +95 -1
  25. package/dist/core/dashboard-verification.js +8 -0
  26. package/dist/core/public-json-contracts.js +7 -0
  27. package/dist/core/run-performance-history.js +307 -0
  28. package/dist/core/run-profile.js +87 -0
  29. package/dist/core/run-receipt.js +171 -4
  30. package/dist/core/run-write-drift.js +18 -2
  31. package/dist/core/skill-route-alignment.js +90 -0
  32. package/dist/core/test-selection.js +224 -0
  33. package/dist/core/verification-decision-graph.js +67 -0
  34. package/dist/core/verification-scheduler.js +96 -2
  35. package/package.json +1 -1
  36. package/schemas/README.md +6 -2
  37. package/schemas/change-verification-report.schema.json +153 -3
  38. package/schemas/commands.schema.json +47 -1
  39. package/schemas/contract-lint-report.schema.json +51 -0
  40. package/schemas/dashboard-export.schema.json +273 -0
  41. package/schemas/explain-report.schema.json +2 -0
  42. package/schemas/run-receipt.schema.json +109 -0
  43. package/templates/default/common/.mustflow/config/commands.toml +1 -1
  44. package/templates/default/manifest.toml +1 -1
@@ -27,6 +27,7 @@ export const esMessages = {
27
27
  "command.contractLint.summary": "Revisa el contrato de comandos",
28
28
  "command.status.summary": "Muestra el estado de la instalación local de mustflow",
29
29
  "command.update.summary": "Previsualiza o aplica actualizaciones del flujo de trabajo mustflow",
30
+ "command.upgrade.summary": "Comprueba la versión del paquete y actualiza con seguridad los archivos de flujo instalados",
30
31
  "command.map.summary": "Genera REPO_MAP.md",
31
32
  "command.lineEndings.summary": "Inspecciona y normaliza la política de finales de línea",
32
33
  "command.run.summary": "Ejecuta un comando configurado de una sola ejecución",
@@ -658,6 +659,17 @@ Lee estos archivos antes de trabajar:
658
659
  "version.check.upToDate": "última versión {version}; ya está actualizado",
659
660
  "version.check.updateCommand": "Comando de actualización:",
660
661
  "version.error.checkFailed": "No se pudo consultar npm para una versión nueva: {message}",
662
+ "upgrade.help.summary": "Comprueba si el paquete mustflow instalado está actualizado y luego aplica de forma segura las actualizaciones de la plantilla incluida cuando sea posible.",
663
+ "upgrade.help.option.dryRun": "Comprueba el estado del paquete e imprime el plan de actualización del proyecto sin escribir archivos",
664
+ "upgrade.help.exit.ok": "El paquete estaba actualizado y la comprobación de actualización del proyecto terminó",
665
+ "upgrade.help.exit.fail": "Hace falta actualizar el paquete, hay un bloqueo de actualización del proyecto o la entrada es inválida",
666
+ "upgrade.title": "mustflow upgrade",
667
+ "upgrade.packageSection": "Paquete:",
668
+ "upgrade.projectSection": "Plantilla del proyecto:",
669
+ "upgrade.packageUpdateRequired": "Actualiza primero el paquete mustflow y luego vuelve a ejecutar `mf upgrade`.",
670
+ "upgrade.noFilesWritten": "No se escribieron archivos del proyecto.",
671
+ "upgrade.warning.versionCheckFailed": "No se pudo consultar npm para una versión nueva: {message}",
672
+ "upgrade.warning.continueWithBundledTemplate": "Continuando con la plantilla incluida en el CLI actual.",
661
673
  "classify.help.summary": "Clasifica rutas cambiadas, superficies publicas y razones de verificacion sin modificar archivos.",
662
674
  "classify.help.option.changed": "Lee rutas desde git status --short --untracked-files=all",
663
675
  "classify.help.exit.ok": "La clasificacion de cambios fue inspeccionada e impresa",
@@ -27,6 +27,7 @@ export const frMessages = {
27
27
  "command.contractLint.summary": "Vérifie le contrat de commandes",
28
28
  "command.status.summary": "Affiche l'état de l'installation locale de mustflow",
29
29
  "command.update.summary": "Prévisualise ou applique les mises à jour du flux de travail mustflow",
30
+ "command.upgrade.summary": "Vérifie la version du paquet et met à jour en sécurité les fichiers de workflow installés",
30
31
  "command.map.summary": "Génère REPO_MAP.md",
31
32
  "command.lineEndings.summary": "Inspecte et normalise la politique de fins de ligne",
32
33
  "command.run.summary": "Exécute une commande configurée à exécution unique",
@@ -658,6 +659,17 @@ Lisez ces fichiers avant de travailler :
658
659
  "version.check.upToDate": "dernière version {version}; déjà à jour",
659
660
  "version.check.updateCommand": "Commande de mise à jour :",
660
661
  "version.error.checkFailed": "Impossible de vérifier une nouvelle version sur npm : {message}",
662
+ "upgrade.help.summary": "Vérifie si le paquet mustflow installé est à jour, puis applique en sécurité les mises à jour du modèle inclus quand c'est possible.",
663
+ "upgrade.help.option.dryRun": "Vérifie l'état du paquet et affiche le plan de mise à jour du projet sans écrire de fichiers",
664
+ "upgrade.help.exit.ok": "Le paquet était à jour et la vérification de mise à jour du projet est terminée",
665
+ "upgrade.help.exit.fail": "Une mise à jour du paquet est requise, un blocage de mise à jour du projet existe, ou l'entrée est invalide",
666
+ "upgrade.title": "mustflow upgrade",
667
+ "upgrade.packageSection": "Paquet :",
668
+ "upgrade.projectSection": "Modèle de projet :",
669
+ "upgrade.packageUpdateRequired": "Mettez d'abord à jour le paquet mustflow, puis relancez `mf upgrade`.",
670
+ "upgrade.noFilesWritten": "Aucun fichier du projet n'a été écrit.",
671
+ "upgrade.warning.versionCheckFailed": "Impossible de vérifier une nouvelle version sur npm : {message}",
672
+ "upgrade.warning.continueWithBundledTemplate": "Poursuite avec le modèle inclus dans le CLI actuel.",
661
673
  "classify.help.summary": "Classe les chemins modifies, les surfaces publiques et les raisons de verification sans modifier les fichiers.",
662
674
  "classify.help.option.changed": "Lire les chemins depuis git status --short --untracked-files=all",
663
675
  "classify.help.exit.ok": "La classification des changements a ete inspectee et affichee",
@@ -27,6 +27,7 @@ export const hiMessages = {
27
27
  "command.contractLint.summary": "कमांड अनुबंध की जाँच करें",
28
28
  "command.status.summary": "स्थानीय mustflow इंस्टॉल स्थिति दिखाएँ",
29
29
  "command.update.summary": "mustflow वर्कफ़्लो अपडेट का पूर्वावलोकन करें या लागू करें",
30
+ "command.upgrade.summary": "Package version जाँचें और installed workflow files सुरक्षित रूप से update करें",
30
31
  "command.map.summary": "REPO_MAP.md बनाएँ",
31
32
  "command.lineEndings.summary": "लाइन-एंडिंग नीति की जाँच और सामान्यीकरण करें",
32
33
  "command.run.summary": "कॉन्फ़िगर की गई एक-बार चलने वाली कमांड चलाएँ",
@@ -658,6 +659,17 @@ export const hiMessages = {
658
659
  "version.check.upToDate": "latest {version}; पहले से up to date",
659
660
  "version.check.updateCommand": "Update command:",
660
661
  "version.error.checkFailed": "npm पर नया version जाँचा नहीं जा सका: {message}",
662
+ "upgrade.help.summary": "जाँचें कि installed mustflow package current है या नहीं, फिर संभव होने पर current CLI में bundled workflow template updates सुरक्षित रूप से लागू करें.",
663
+ "upgrade.help.option.dryRun": "Package status जाँचें और files लिखे बिना project update plan प्रिंट करें",
664
+ "upgrade.help.exit.ok": "Package current था और project update check पूरा हुआ",
665
+ "upgrade.help.exit.fail": "Package update चाहिए, project update blocker मिला, या input invalid है",
666
+ "upgrade.title": "mustflow upgrade",
667
+ "upgrade.packageSection": "Package:",
668
+ "upgrade.projectSection": "Project template:",
669
+ "upgrade.packageUpdateRequired": "पहले mustflow package update करें, फिर `mf upgrade` दोबारा चलाएँ.",
670
+ "upgrade.noFilesWritten": "कोई project file नहीं लिखी गई.",
671
+ "upgrade.warning.versionCheckFailed": "npm पर नया version जाँचा नहीं जा सका: {message}",
672
+ "upgrade.warning.continueWithBundledTemplate": "Current CLI में bundled template के साथ आगे बढ़ रहे हैं.",
661
673
  "classify.help.summary": "फ़ाइल बदले बिना बदले पथ, सार्वजनिक सतह और जरूरी सत्यापन कारण वर्गीकृत करें.",
662
674
  "classify.help.option.changed": "git status --short --untracked-files=all से पथ पढ़ें",
663
675
  "classify.help.exit.ok": "बदलाव वर्गीकरण जांचकर प्रिंट किया गया",
@@ -27,6 +27,7 @@ export const koMessages = {
27
27
  "command.contractLint.summary": "명령 계약을 점검합니다",
28
28
  "command.status.summary": "로컬 mustflow 설치 상태를 출력합니다",
29
29
  "command.update.summary": "mustflow 워크플로우 갱신을 미리 보거나 적용합니다",
30
+ "command.upgrade.summary": "패키지 버전을 확인하고 설치된 워크플로우 파일을 안전하게 갱신합니다",
30
31
  "command.map.summary": "REPO_MAP.md를 작성합니다",
31
32
  "command.lineEndings.summary": "줄바꿈 정책을 검사하고 정규화합니다",
32
33
  "command.run.summary": "설정된 일회성 명령을 실행합니다",
@@ -658,6 +659,17 @@ export const koMessages = {
658
659
  "version.check.upToDate": "최신 {version}; 이미 최신 상태입니다",
659
660
  "version.check.updateCommand": "업데이트 명령:",
660
661
  "version.error.checkFailed": "npm에서 새 버전을 확인하지 못했습니다: {message}",
662
+ "upgrade.help.summary": "설치된 mustflow 패키지가 최신인지 확인한 뒤, 가능하면 현재 CLI에 포함된 워크플로우 템플릿 갱신을 안전하게 적용합니다.",
663
+ "upgrade.help.option.dryRun": "패키지 상태를 확인하고 파일을 쓰지 않은 채 프로젝트 갱신 계획을 출력합니다",
664
+ "upgrade.help.exit.ok": "패키지가 최신이고 프로젝트 갱신 확인이 완료되었습니다",
665
+ "upgrade.help.exit.fail": "패키지 업데이트가 필요하거나, 프로젝트 갱신 차단 항목이 있거나, 입력이 잘못되었습니다",
666
+ "upgrade.title": "mustflow upgrade",
667
+ "upgrade.packageSection": "패키지:",
668
+ "upgrade.projectSection": "프로젝트 템플릿:",
669
+ "upgrade.packageUpdateRequired": "mustflow 패키지를 먼저 업데이트한 뒤 `mf upgrade`를 다시 실행하세요.",
670
+ "upgrade.noFilesWritten": "프로젝트 파일은 쓰지 않았습니다.",
671
+ "upgrade.warning.versionCheckFailed": "npm에서 새 버전을 확인하지 못했습니다: {message}",
672
+ "upgrade.warning.continueWithBundledTemplate": "현재 CLI에 포함된 템플릿으로 계속 진행합니다.",
661
673
  "classify.help.summary": "파일을 수정하지 않고 변경 경로, 공개 표면, 필요한 검증 이유를 분류합니다.",
662
674
  "classify.help.option.changed": "git status --short --untracked-files=all에서 경로를 읽습니다",
663
675
  "classify.help.exit.ok": "변경 분류를 확인하고 출력했습니다",
@@ -27,6 +27,7 @@ export const zhMessages = {
27
27
  "command.contractLint.summary": "检查命令契约",
28
28
  "command.status.summary": "显示本地 mustflow 安装状态",
29
29
  "command.update.summary": "预览或应用 mustflow 工作流更新",
30
+ "command.upgrade.summary": "检查包版本并安全更新已安装的工作流文件",
30
31
  "command.map.summary": "生成 REPO_MAP.md",
31
32
  "command.lineEndings.summary": "检查并规范化换行符策略",
32
33
  "command.run.summary": "运行已配置的一次性命令",
@@ -658,6 +659,17 @@ export const zhMessages = {
658
659
  "version.check.upToDate": "最新版本 {version};已是最新",
659
660
  "version.check.updateCommand": "更新命令:",
660
661
  "version.error.checkFailed": "无法从 npm 检查新版本:{message}",
662
+ "upgrade.help.summary": "检查已安装的 mustflow 包是否为最新,然后在可行时安全应用当前 CLI 捆绑的工作流模板更新。",
663
+ "upgrade.help.option.dryRun": "检查包状态并打印项目更新计划,不写入文件",
664
+ "upgrade.help.exit.ok": "包已是最新,项目更新检查已完成",
665
+ "upgrade.help.exit.fail": "需要先更新包、项目更新存在阻塞项,或输入无效",
666
+ "upgrade.title": "mustflow upgrade",
667
+ "upgrade.packageSection": "包:",
668
+ "upgrade.projectSection": "项目模板:",
669
+ "upgrade.packageUpdateRequired": "请先更新 mustflow 包,然后再次运行 `mf upgrade`。",
670
+ "upgrade.noFilesWritten": "未写入项目文件。",
671
+ "upgrade.warning.versionCheckFailed": "无法从 npm 检查新版本:{message}",
672
+ "upgrade.warning.continueWithBundledTemplate": "继续使用当前 CLI 捆绑的模板。",
661
673
  "classify.help.summary": "在不修改文件的情况下分类变更路径、公开表面和所需验证原因。",
662
674
  "classify.help.option.changed": "从 git status --short --untracked-files=all 读取路径",
663
675
  "classify.help.exit.ok": "已检查并输出变更分类",
package/dist/cli/index.js CHANGED
@@ -2,29 +2,6 @@
2
2
  import { realpathSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { runAdapters } from './commands/adapters.js';
6
- import { runCheck } from './commands/check.js';
7
- import { runClassify } from './commands/classify.js';
8
- import { runContractLint } from './commands/contract-lint.js';
9
- import { runContext } from './commands/context.js';
10
- import { runDashboard } from './commands/dashboard.js';
11
- import { runDoctor } from './commands/doctor.js';
12
- import { runDocs } from './commands/docs.js';
13
- import { runExplain } from './commands/explain.js';
14
- import { runHelp } from './commands/help.js';
15
- import { runHandoff } from './commands/handoff.js';
16
- import { runImpact } from './commands/impact.js';
17
- import { runInit } from './commands/init.js';
18
- import { runIndex } from './commands/index.js';
19
- import { runLineEndings } from './commands/line-endings.js';
20
- import { runMap } from './commands/map.js';
21
- import { runRun } from './commands/run.js';
22
- import { runSearch } from './commands/search.js';
23
- import { runStatus } from './commands/status.js';
24
- import { runUpdate } from './commands/update.js';
25
- import { runVerify } from './commands/verify.js';
26
- import { runVersion } from './commands/version.js';
27
- import { runVersionSources } from './commands/version-sources.js';
28
5
  import { COMMAND_DEFINITIONS } from './lib/command-registry.js';
29
6
  import { renderCliError, renderHelp } from './lib/cli-output.js';
30
7
  import { DEFAULT_CLI_LANG, SUPPORTED_CLI_LANGS, isCliLang, t } from './lib/i18n.js';
@@ -59,6 +36,7 @@ function getTopLevelHelp(lang) {
59
36
  'mf search mustflow_check',
60
37
  'mf explain authority AGENTS.md',
61
38
  'mf impact --changed',
39
+ 'mf upgrade --dry-run',
62
40
  'mf verify --changed --plan-only --json',
63
41
  'mf verify --reason code_change',
64
42
  'mf line-endings check',
@@ -125,73 +103,76 @@ export async function runCli(argv, reporter = consoleReporter) {
125
103
  return 0;
126
104
  }
127
105
  if (command === '--version' || command === '-v' || command === 'version') {
128
- return runVersion(args, reporter, parsed.lang);
106
+ return (await import('./commands/version.js')).runVersion(args, reporter, parsed.lang);
129
107
  }
130
108
  if (command === 'init') {
131
- return runInit(args, reporter, parsed.lang);
109
+ return (await import('./commands/init.js')).runInit(args, reporter, parsed.lang);
132
110
  }
133
111
  if (command === 'adapters') {
134
- return runAdapters(args, reporter, parsed.lang);
112
+ return (await import('./commands/adapters.js')).runAdapters(args, reporter, parsed.lang);
135
113
  }
136
114
  if (command === 'check') {
137
- return runCheck(args, reporter, parsed.lang);
115
+ return (await import('./commands/check.js')).runCheck(args, reporter, parsed.lang);
138
116
  }
139
117
  if (command === 'classify') {
140
- return runClassify(args, reporter, parsed.lang);
118
+ return (await import('./commands/classify.js')).runClassify(args, reporter, parsed.lang);
141
119
  }
142
120
  if (command === 'contract-lint') {
143
- return runContractLint(args, reporter, parsed.lang);
121
+ return (await import('./commands/contract-lint.js')).runContractLint(args, reporter, parsed.lang);
144
122
  }
145
123
  if (command === 'status') {
146
- return runStatus(args, reporter, parsed.lang);
124
+ return (await import('./commands/status.js')).runStatus(args, reporter, parsed.lang);
147
125
  }
148
126
  if (command === 'update') {
149
- return runUpdate(args, reporter, parsed.lang);
127
+ return (await import('./commands/update.js')).runUpdate(args, reporter, parsed.lang);
128
+ }
129
+ if (command === 'upgrade') {
130
+ return (await import('./commands/upgrade.js')).runUpgrade(args, reporter, parsed.lang);
150
131
  }
151
132
  if (command === 'map') {
152
- return runMap(args, reporter, parsed.lang);
133
+ return (await import('./commands/map.js')).runMap(args, reporter, parsed.lang);
153
134
  }
154
135
  if (command === 'line-endings') {
155
- return runLineEndings(args, reporter, parsed.lang);
136
+ return (await import('./commands/line-endings.js')).runLineEndings(args, reporter, parsed.lang);
156
137
  }
157
138
  if (command === 'run') {
158
- return runRun(args, reporter, parsed.lang);
139
+ return (await import('./commands/run.js')).runRun(args, reporter, parsed.lang);
159
140
  }
160
141
  if (command === 'context') {
161
- return runContext(args, reporter, parsed.lang);
142
+ return (await import('./commands/context.js')).runContext(args, reporter, parsed.lang);
162
143
  }
163
144
  if (command === 'doctor') {
164
- return runDoctor(args, reporter, parsed.lang);
145
+ return (await import('./commands/doctor.js')).runDoctor(args, reporter, parsed.lang);
165
146
  }
166
147
  if (command === 'docs') {
167
- return runDocs(args, reporter, parsed.lang);
148
+ return (await import('./commands/docs.js')).runDocs(args, reporter, parsed.lang);
168
149
  }
169
150
  if (command === 'handoff') {
170
- return runHandoff(args, reporter, parsed.lang);
151
+ return (await import('./commands/handoff.js')).runHandoff(args, reporter, parsed.lang);
171
152
  }
172
153
  if (command === 'index') {
173
- return runIndex(args, reporter, parsed.lang);
154
+ return (await import('./commands/index.js')).runIndex(args, reporter, parsed.lang);
174
155
  }
175
156
  if (command === 'search') {
176
- return runSearch(args, reporter, parsed.lang);
157
+ return (await import('./commands/search.js')).runSearch(args, reporter, parsed.lang);
177
158
  }
178
159
  if (command === 'dashboard') {
179
- return runDashboard(args, reporter, parsed.lang);
160
+ return (await import('./commands/dashboard.js')).runDashboard(args, reporter, parsed.lang);
180
161
  }
181
162
  if (command === 'version-sources') {
182
- return runVersionSources(args, reporter, parsed.lang);
163
+ return (await import('./commands/version-sources.js')).runVersionSources(args, reporter, parsed.lang);
183
164
  }
184
165
  if (command === 'verify') {
185
- return runVerify(args, reporter, parsed.lang);
166
+ return (await import('./commands/verify.js')).runVerify(args, reporter, parsed.lang);
186
167
  }
187
168
  if (command === 'explain') {
188
- return runExplain(args, reporter, parsed.lang);
169
+ return (await import('./commands/explain.js')).runExplain(args, reporter, parsed.lang);
189
170
  }
190
171
  if (command === 'impact') {
191
- return runImpact(args, reporter, parsed.lang);
172
+ return (await import('./commands/impact.js')).runImpact(args, reporter, parsed.lang);
192
173
  }
193
174
  if (command === 'help') {
194
- return runHelp(args, reporter, parsed.lang);
175
+ return (await import('./commands/help.js')).runHelp(args, reporter, parsed.lang);
195
176
  }
196
177
  reporter.stderr(renderCliError(t(parsed.lang, 'cli.error.unknownCommand', { command }), 'mf --help', parsed.lang));
197
178
  reporter.stdout(getTopLevelHelp(parsed.lang));
@@ -34,6 +34,11 @@ export const COMMAND_DEFINITIONS = [
34
34
  usage: 'mf update',
35
35
  summaryKey: 'command.update.summary',
36
36
  },
37
+ {
38
+ id: 'upgrade',
39
+ usage: 'mf upgrade',
40
+ summaryKey: 'command.upgrade.summary',
41
+ },
37
42
  {
38
43
  id: 'map',
39
44
  usage: 'mf map',
@@ -1078,7 +1078,7 @@ function renderVerificationPanel() {
1078
1078
  if (!entry) continue;
1079
1079
  const effects = document.createElement("div");
1080
1080
  effects.className = "verification-files";
1081
- effects.textContent = message("dashboard.verification.effects") + ": " + entry.effects.map((effect) => effect.mode + " " + effect.path + " [" + effect.lock + "]").join(", ");
1081
+ effects.textContent = message("dashboard.verification.effects") + ": " + entry.effects.map((effect) => effect.mode + " " + (effect.path || effect.lock) + " [" + effect.lock + "]").join(", ");
1082
1082
  details.appendChild(effects);
1083
1083
  if (entry.conflicts.length > 0) {
1084
1084
  const conflicts = document.createElement("div");
@@ -8,7 +8,7 @@ import { readTomlFile } from './toml.js';
8
8
  import { collectSourceAnchorIndexRecords, } from '../../core/source-anchor-status.js';
9
9
  import { normalizeCommandEffects } from '../../core/command-effects.js';
10
10
  import { listChangeClassificationRuleDescriptors } from '../../core/change-classification.js';
11
- const LOCAL_INDEX_SCHEMA_VERSION = '12';
11
+ const LOCAL_INDEX_SCHEMA_VERSION = '13';
12
12
  const LOCAL_INDEX_PARSER_VERSION = '1';
13
13
  const DEFAULT_DATABASE_RELATIVE_PATH = '.mustflow/cache/mustflow.sqlite';
14
14
  const LOCAL_INDEX_CONTENT_MODE = 'metadata_and_snippets';
@@ -752,10 +752,9 @@ CREATE TABLE command_effects (
752
752
  source TEXT NOT NULL,
753
753
  access TEXT NOT NULL,
754
754
  mode TEXT NOT NULL,
755
- path TEXT NOT NULL,
755
+ path TEXT,
756
756
  lock TEXT NOT NULL,
757
- concurrency TEXT NOT NULL,
758
- PRIMARY KEY (intent, source, access, mode, path, lock, concurrency)
757
+ concurrency TEXT NOT NULL
759
758
  );
760
759
 
761
760
  CREATE VIEW command_write_locks AS
@@ -1014,7 +1013,7 @@ function populateSearchTables(database, capabilities, documents, skills, skillRo
1014
1013
  }
1015
1014
  for (const intent of commandIntents) {
1016
1015
  const effects = intent.effects
1017
- .flatMap((effect) => [effect.lock, effect.path, effect.mode, effect.access, effect.concurrency])
1016
+ .flatMap((effect) => [effect.lock, effect.path ?? '', effect.mode, effect.access, effect.concurrency])
1018
1017
  .join(' ');
1019
1018
  insertSearchNgrams(database, 'command_intent', intent.name, [intent.name, intent.status, intent.lifecycle ?? '', intent.runPolicy ?? '', intent.description ?? '', effects], 'command_intent');
1020
1019
  if (capabilities.backend === SEARCH_BACKEND_FTS5) {
@@ -1123,7 +1122,9 @@ function populateDatabase(database, capabilities, documents, skills, skillRoutes
1123
1122
  insertDocumentTerm(database, '.mustflow/config/commands.toml', intent.description, 'command_description');
1124
1123
  for (const effect of intent.effects) {
1125
1124
  database.run('INSERT INTO command_effects (intent, source, access, mode, path, lock, concurrency) VALUES (?, ?, ?, ?, ?, ?, ?)', [effect.intent, effect.source, effect.access, effect.mode, effect.path, effect.lock, effect.concurrency]);
1126
- insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.path, 'command_effect_path');
1125
+ if (effect.path !== null) {
1126
+ insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.path, 'command_effect_path');
1127
+ }
1127
1128
  insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.lock, 'command_effect_lock');
1128
1129
  insertDocumentTerm(database, '.mustflow/config/commands.toml', effect.mode, 'command_effect_mode');
1129
1130
  }
@@ -1624,7 +1625,7 @@ function getCommandEffects(database, intent) {
1624
1625
  source: toSearchString(row.source),
1625
1626
  access: toSearchString(row.access),
1626
1627
  mode: toSearchString(row.mode),
1627
- path: toSearchString(row.path),
1628
+ path: row.path === null || row.path === undefined ? null : toSearchString(row.path),
1628
1629
  lock: toSearchString(row.lock),
1629
1630
  concurrency: toSearchString(row.concurrency),
1630
1631
  }));
@@ -1826,7 +1827,9 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
1826
1827
  const description = toSearchString(row.description);
1827
1828
  const effects = getCommandEffects(database, name);
1828
1829
  const effectLocks = [...new Set(effects.map((effect) => effect.lock))].sort((left, right) => left.localeCompare(right));
1829
- const effectPaths = [...new Set(effects.map((effect) => effect.path))].sort((left, right) => left.localeCompare(right));
1830
+ const effectPaths = [
1831
+ ...new Set(effects.map((effect) => effect.path).filter((effectPath) => effectPath !== null)),
1832
+ ].sort((left, right) => left.localeCompare(right));
1830
1833
  const effectModes = [...new Set(effects.map((effect) => effect.mode))].sort((left, right) => left.localeCompare(right));
1831
1834
  const primaryFields = [name];
1832
1835
  const secondaryFields = [status, lifecycle, runPolicy, description, ...effectLocks, ...effectPaths, ...effectModes];
@@ -5,4 +5,10 @@ export const consoleReporter = {
5
5
  stderr(message) {
6
6
  console.error(message);
7
7
  },
8
+ writeStdout(chunk) {
9
+ process.stdout.write(chunk);
10
+ },
11
+ writeStderr(chunk) {
12
+ process.stderr.write(chunk);
13
+ },
8
14
  };
@@ -26,6 +26,17 @@ function getRelativeProjectPath(projectRoot, targetPath) {
26
26
  const relativePath = path.relative(projectRoot, targetPath);
27
27
  return relativePath.length > 0 ? toPosixPath(relativePath) : '.';
28
28
  }
29
+ function normalizeTestTargets(values) {
30
+ return [
31
+ ...new Set((values ?? [])
32
+ .map((value) => value.trim().replace(/\\/g, '/'))
33
+ .filter((value) => value.length > 0 && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value))
34
+ .filter((value) => value.split('/').every((segment) => segment.length > 0 && segment !== '.' && segment !== '..'))),
35
+ ].sort((left, right) => left.localeCompare(right));
36
+ }
37
+ function commandAcceptsTestTargets(intent) {
38
+ return isRecord(intent.selection) && intent.selection.accepts_test_targets === true;
39
+ }
29
40
  function shouldUseShellForArgvExecutable(executablePath) {
30
41
  return process.platform === 'win32' && executablePath.toLowerCase().endsWith('.cmd');
31
42
  }
@@ -83,6 +94,7 @@ function readRunIntentMetadata(contract, intent) {
83
94
  destructive: readBoolean(intent, 'destructive'),
84
95
  envPolicy: env.policy,
85
96
  envAllowlist: env.allowlist,
97
+ testTargets: [],
86
98
  };
87
99
  }
88
100
  function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonCode, detail) {
@@ -114,9 +126,10 @@ function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonC
114
126
  destructive: metadata?.destructive,
115
127
  envPolicy: metadata?.envPolicy ?? null,
116
128
  envAllowlist: metadata?.envAllowlist ?? [],
129
+ testTargets: [],
117
130
  };
118
131
  }
119
- export function createRunPlan(projectRoot, contract, intentName) {
132
+ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
120
133
  const rawIntent = contract.intents[intentName];
121
134
  const eligibility = evaluateCommandIntentEligibility(intentName, rawIntent);
122
135
  if (!isRecord(rawIntent)) {
@@ -133,6 +146,8 @@ export function createRunPlan(projectRoot, contract, intentName) {
133
146
  catch (error) {
134
147
  return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, 'cwd_outside_project', error instanceof Error ? error.message : String(error));
135
148
  }
149
+ const testTargets = commandAcceptsTestTargets(rawIntent) ? normalizeTestTargets(options.testTargets) : [];
150
+ const commandArgv = metadata.commandArgv && testTargets.length > 0 ? [...metadata.commandArgv, ...testTargets] : metadata.commandArgv;
136
151
  if (!metadata.timeoutSeconds || !metadata.mode) {
137
152
  return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, !metadata.timeoutSeconds ? 'missing_timeout' : 'missing_command_source', !metadata.timeoutSeconds ? 'Intent timeout_seconds is missing or invalid.' : 'Intent does not define argv or shell cmd.');
138
153
  }
@@ -153,16 +168,17 @@ export function createRunPlan(projectRoot, contract, intentName) {
153
168
  timeoutSeconds: metadata.timeoutSeconds,
154
169
  maxOutputBytes: metadata.maxOutputBytes,
155
170
  successExitCodes: metadata.successExitCodes,
156
- commandArgv: metadata.commandArgv,
171
+ commandArgv,
157
172
  shellCommand: metadata.shellCommand,
158
173
  mode: metadata.mode,
159
- argvCommand: metadata.commandArgv ? resolveArgvCommand(rawIntent, metadata.commandArgv) : undefined,
174
+ argvCommand: commandArgv ? resolveArgvCommand(rawIntent, commandArgv) : undefined,
160
175
  writes: metadata.writes,
161
176
  effects: metadata.effects,
162
177
  network: metadata.network,
163
178
  destructive: metadata.destructive,
164
179
  envPolicy: metadata.envPolicy,
165
180
  envAllowlist: metadata.envAllowlist,
181
+ testTargets,
166
182
  };
167
183
  }
168
184
  export function createRunPreview(plan, previewMode) {
@@ -195,6 +211,7 @@ export function createRunPreview(plan, previewMode) {
195
211
  destructive: plan.destructive,
196
212
  env_policy: plan.envPolicy,
197
213
  env_allowlist: plan.envAllowlist,
214
+ test_targets: plan.testTargets,
198
215
  success_exit_codes: plan.successExitCodes,
199
216
  };
200
217
  }
@@ -4,7 +4,7 @@ import { isRecord } from './command-contract.js';
4
4
  import { validateCommandContractConfig, validateCommandContractStrictDefaults, } from '../../core/command-contract-validation.js';
5
5
  import { ALLOWED_RETENTION_ON_LIMIT, ALLOWED_RETENTION_STORES, DEFAULT_RETENTION_LIMITS, readNestedRetentionTable, readRetentionTable, resolveRetentionLimits, } from '../../core/retention-policy.js';
6
6
  import { formatManagedMarkdownLabel, getManagedMarkdownExpectation, } from '../../core/authority-resolution.js';
7
- import { SKILL_INDEX_ROUTE_COLUMN_COUNT, SKILL_INDEX_ROUTE_COLUMNS, SKILL_INDEX_SKILL_PATH_COLUMN_INDEX, findSkillIndexRoutePathColumn, parseSkillIndexRoutes, readBacktickValues, } from '../../core/skill-route-alignment.js';
7
+ import { SKILL_INDEX_ROUTE_COLUMN_COUNT, SKILL_INDEX_ROUTE_COLUMNS, SKILL_INDEX_SKILL_PATH_COLUMN_INDEX, findSkillRouteConflictWarnings, findSkillIndexRoutePathColumn, parseSkillIndexRoutes, readBacktickValues, } from '../../core/skill-route-alignment.js';
8
8
  import { validateTemplateVersionSync } from '../../core/release-version-validation.js';
9
9
  import { validateSourceAnchorsInProject } from '../../core/source-anchor-validation.js';
10
10
  import { listFilesRecursive, toPosixPath } from './filesystem.js';
@@ -30,6 +30,7 @@ const REQUIRED_SKILL_SECTION_IDS = [
30
30
  'output-format',
31
31
  ];
32
32
  const SKILL_SECTION_MARKER_PATTERN = /^<!--\s*mustflow-section:\s*([a-z][a-z0-9-]*)\s*-->\s*\r?\n##\s+.+$/gimu;
33
+ const TEST_SELECTION_CONFIG_PATH = '.mustflow/config/test-selection.toml';
33
34
  const REQUIRED_FILES = [
34
35
  'AGENTS.md',
35
36
  '.mustflow/config/mustflow.toml',
@@ -99,6 +100,22 @@ const FORBIDDEN_VERIFICATION_SELECTION_AUTHORITY_FIELDS = [
99
100
  'destructive',
100
101
  ];
101
102
  const ALLOWED_VERIFICATION_SELECTION_STRATEGIES = new Set(['risk_based', 'targeted', 'full']);
103
+ const ALLOWED_TEST_SELECTION_RISKS = new Set(['low', 'medium', 'high', 'release_sensitive', 'security_sensitive']);
104
+ const FORBIDDEN_TEST_SELECTION_COMMAND_AUTHORITY_FIELDS = [
105
+ 'argv',
106
+ 'cmd',
107
+ 'command',
108
+ 'commands',
109
+ 'command_intents',
110
+ 'destructive',
111
+ 'intents',
112
+ 'lifecycle',
113
+ 'network',
114
+ 'required_after',
115
+ 'run_policy',
116
+ 'status',
117
+ 'writes',
118
+ ];
102
119
  const TEST_AUTHORING_BOOLEAN_FIELDS = ['prefer_existing_tests', 'require_new_test_rationale'];
103
120
  const ALLOWED_TEST_AUTHORING_POLICIES = new Set(TEST_AUTHORING_POLICIES);
104
121
  const ALLOWED_TESTING_POLICIES = new Set(['behavior_contract']);
@@ -1079,6 +1096,9 @@ function validateSkillIndexRoutes(projectRoot, commandsToml, skillFiles, issues)
1079
1096
  const routedSkillPaths = new Set();
1080
1097
  const expectedSkillPaths = new Set(skillFiles.map((relativePath) => `.mustflow/skills/${relativePath}`));
1081
1098
  const seenSkillPaths = new Set();
1099
+ for (const warning of findSkillRouteConflictWarnings(skillRoutes)) {
1100
+ pushStrictWarning(issues, `${SKILL_INDEX_PATH} ${warning}`);
1101
+ }
1082
1102
  for (const route of skillRoutes) {
1083
1103
  if (!route.skillPath.startsWith('.mustflow/skills/') || !route.skillPath.endsWith('/SKILL.md')) {
1084
1104
  pushStrictIssue(issues, `${SKILL_INDEX_PATH} route "${route.skillPath}" must point to .mustflow/skills/<name>/SKILL.md`);
@@ -1223,6 +1243,94 @@ function validateStrictVerificationSelectionAuthority(preferencesToml, issues) {
1223
1243
  }
1224
1244
  }
1225
1245
  }
1246
+ function validateNoTestSelectionCommandAuthorityFields(label, table, issues) {
1247
+ for (const field of FORBIDDEN_TEST_SELECTION_COMMAND_AUTHORITY_FIELDS) {
1248
+ if (hasOwn(table, field)) {
1249
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label}.${field} cannot define command authority; use .mustflow/config/commands.toml`);
1250
+ }
1251
+ }
1252
+ }
1253
+ function validateTestSelectionIntentReference(value, label, commandsToml, issues) {
1254
+ if (typeof value !== 'string' || value.trim().length === 0) {
1255
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} must be a command intent name`);
1256
+ return;
1257
+ }
1258
+ if (!isDeclaredCommandIntent(commandsToml, value)) {
1259
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} references unknown command intent "${value}"`);
1260
+ return;
1261
+ }
1262
+ if (!isConfiguredCommandIntent(commandsToml, value)) {
1263
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} references command intent "${value}" that is not configured`);
1264
+ }
1265
+ }
1266
+ function validateTestSelectionRule(rule, index, commandsToml, issues) {
1267
+ const label = `rules[${index}]`;
1268
+ if (!isRecord(rule)) {
1269
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} must be a TOML table`);
1270
+ return;
1271
+ }
1272
+ validateNoTestSelectionCommandAuthorityFields(label, rule, issues);
1273
+ validateRequiredStringField(rule, 'id', `${TEST_SELECTION_CONFIG_PATH} ${label}.id`, issues);
1274
+ validateRequiredStringField(rule, 'reason', `${TEST_SELECTION_CONFIG_PATH} ${label}.reason`, issues);
1275
+ validateRequiredStringField(rule, 'risk', `${TEST_SELECTION_CONFIG_PATH} ${label}.risk`, issues);
1276
+ validateAllowedStringField(rule, 'risk', `${TEST_SELECTION_CONFIG_PATH} ${label}.risk`, ALLOWED_TEST_SELECTION_RISKS, issues);
1277
+ const match = validateNestedTable(rule, 'match', `${TEST_SELECTION_CONFIG_PATH} ${label}.match`, issues);
1278
+ if (!hasOwn(rule, 'match')) {
1279
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} must define match`);
1280
+ }
1281
+ if (match) {
1282
+ validateNoTestSelectionCommandAuthorityFields(`${label}.match`, match, issues);
1283
+ validatePathArrayField(match, 'paths', `${TEST_SELECTION_CONFIG_PATH} ${label}.match.paths`, issues);
1284
+ validateStringArrayField(match, 'surfaces', `${TEST_SELECTION_CONFIG_PATH} ${label}.match.surfaces`, issues);
1285
+ if (!hasOwn(match, 'paths')) {
1286
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label}.match must define paths`);
1287
+ }
1288
+ if (!hasOwn(match, 'surfaces')) {
1289
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label}.match must define surfaces`);
1290
+ }
1291
+ }
1292
+ const select = validateNestedTable(rule, 'select', `${TEST_SELECTION_CONFIG_PATH} ${label}.select`, issues);
1293
+ if (!hasOwn(rule, 'select')) {
1294
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} must define select`);
1295
+ }
1296
+ if (select) {
1297
+ validateNoTestSelectionCommandAuthorityFields(`${label}.select`, select, issues);
1298
+ validateTestSelectionIntentReference(select.intent, `${label}.select.intent`, commandsToml, issues);
1299
+ validateTestSelectionIntentReference(select.fallback_intent, `${label}.select.fallback_intent`, commandsToml, issues);
1300
+ validatePathArrayField(select, 'test_targets', `${TEST_SELECTION_CONFIG_PATH} ${label}.select.test_targets`, issues);
1301
+ }
1302
+ }
1303
+ function validateStrictTestSelectionConfig(projectRoot, commandsToml, issues) {
1304
+ const configPath = path.join(projectRoot, ...TEST_SELECTION_CONFIG_PATH.split('/'));
1305
+ if (!existsSync(configPath)) {
1306
+ return;
1307
+ }
1308
+ let parsed;
1309
+ try {
1310
+ parsed = readTomlFile(configPath);
1311
+ }
1312
+ catch (error) {
1313
+ const message = error instanceof Error ? error.message : String(error);
1314
+ pushStrictIssue(issues, `Invalid TOML in ${TEST_SELECTION_CONFIG_PATH}: ${message}`);
1315
+ return;
1316
+ }
1317
+ if (!isRecord(parsed)) {
1318
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} must contain a TOML table`);
1319
+ return;
1320
+ }
1321
+ validateNoTestSelectionCommandAuthorityFields('root', parsed, issues);
1322
+ validateRequiredStringField(parsed, 'schema_version', `${TEST_SELECTION_CONFIG_PATH}.schema_version`, issues);
1323
+ if (parsed.schema_version !== '1') {
1324
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH}.schema_version must be "1"`);
1325
+ }
1326
+ if (!Array.isArray(parsed.rules) || parsed.rules.length === 0) {
1327
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} must define [[rules]]`);
1328
+ return;
1329
+ }
1330
+ for (const [index, rule] of parsed.rules.entries()) {
1331
+ validateTestSelectionRule(rule, index, commandsToml, issues);
1332
+ }
1333
+ }
1226
1334
  function validateStrictCandidateContractModelConfigs(projectRoot, issues) {
1227
1335
  for (const model of getContractModelDefinitions()) {
1228
1336
  const configPath = path.join(projectRoot, ...model.filePath.split('/'));
@@ -1619,6 +1727,7 @@ function validateStrict(projectRoot, parsed, issues) {
1619
1727
  validateStrictReleaseVersioningAuthority(parsed.preferencesToml, issues);
1620
1728
  validateStrictVerificationSelectionAuthority(parsed.preferencesToml, issues);
1621
1729
  validateStrictCandidateContractModelConfigs(projectRoot, issues);
1730
+ validateStrictTestSelectionConfig(projectRoot, parsed.commandsToml, issues);
1622
1731
  validateStrictVersionSources(projectRoot, parsed.preferencesToml, parsed.versioningToml, issues);
1623
1732
  validateStrictTemplateVersionSync(projectRoot, parsed.preferencesToml, issues);
1624
1733
  validateStrictManagedMarkdownIdentities(projectRoot, issues);
@@ -0,0 +1,38 @@
1
+ export class BoundedOutputBuffer {
2
+ #maxTailBytes;
3
+ #chunks = [];
4
+ #tailBytes = 0;
5
+ #bytes = 0;
6
+ constructor(maxTailBytes) {
7
+ this.#maxTailBytes = Math.max(0, maxTailBytes);
8
+ }
9
+ append(chunk) {
10
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
11
+ this.#bytes += buffer.byteLength;
12
+ if (this.#maxTailBytes === 0 || buffer.byteLength === 0) {
13
+ return;
14
+ }
15
+ this.#chunks.push(buffer);
16
+ this.#tailBytes += buffer.byteLength;
17
+ while (this.#tailBytes > this.#maxTailBytes && this.#chunks.length > 0) {
18
+ const first = this.#chunks[0];
19
+ const overflow = this.#tailBytes - this.#maxTailBytes;
20
+ if (!first) {
21
+ break;
22
+ }
23
+ if (first.byteLength <= overflow) {
24
+ this.#chunks.shift();
25
+ this.#tailBytes -= first.byteLength;
26
+ continue;
27
+ }
28
+ this.#chunks[0] = first.subarray(overflow);
29
+ this.#tailBytes -= overflow;
30
+ }
31
+ }
32
+ toSnapshot() {
33
+ return {
34
+ bytes: this.#bytes,
35
+ tail: Buffer.concat(this.#chunks, this.#tailBytes).toString('utf8'),
36
+ };
37
+ }
38
+ }