mustflow 2.23.0 → 2.25.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 (80) hide show
  1. package/README.md +12 -2
  2. package/dist/cli/commands/adapters.js +11 -9
  3. package/dist/cli/commands/api.js +263 -113
  4. package/dist/cli/commands/check.js +11 -7
  5. package/dist/cli/commands/classify.js +16 -42
  6. package/dist/cli/commands/context.js +18 -31
  7. package/dist/cli/commands/contract-lint.js +12 -7
  8. package/dist/cli/commands/dashboard.js +65 -114
  9. package/dist/cli/commands/docs.js +43 -26
  10. package/dist/cli/commands/doctor.js +11 -7
  11. package/dist/cli/commands/evidence.js +642 -0
  12. package/dist/cli/commands/explain-verify.js +1 -59
  13. package/dist/cli/commands/explain.js +84 -36
  14. package/dist/cli/commands/handoff.js +13 -17
  15. package/dist/cli/commands/impact.js +14 -20
  16. package/dist/cli/commands/index.js +15 -9
  17. package/dist/cli/commands/init.js +56 -70
  18. package/dist/cli/commands/line-endings.js +15 -9
  19. package/dist/cli/commands/map.js +30 -42
  20. package/dist/cli/commands/next.js +300 -0
  21. package/dist/cli/commands/onboard.js +136 -0
  22. package/dist/cli/commands/run.js +47 -42
  23. package/dist/cli/commands/search.js +43 -69
  24. package/dist/cli/commands/status.js +9 -6
  25. package/dist/cli/commands/update.js +16 -10
  26. package/dist/cli/commands/upgrade.js +9 -6
  27. package/dist/cli/commands/verify/args.js +55 -249
  28. package/dist/cli/commands/verify.js +2 -1
  29. package/dist/cli/commands/version-sources.js +9 -6
  30. package/dist/cli/commands/version.js +9 -6
  31. package/dist/cli/commands/workspace.js +564 -0
  32. package/dist/cli/i18n/en.js +60 -1
  33. package/dist/cli/i18n/es.js +60 -1
  34. package/dist/cli/i18n/fr.js +60 -1
  35. package/dist/cli/i18n/hi.js +60 -1
  36. package/dist/cli/i18n/ko.js +60 -1
  37. package/dist/cli/i18n/zh.js +60 -1
  38. package/dist/cli/index.js +28 -25
  39. package/dist/cli/lib/agent-context.js +8 -9
  40. package/dist/cli/lib/command-registry.js +24 -0
  41. package/dist/cli/lib/dashboard-html/client-script.js +1 -1
  42. package/dist/cli/lib/local-index/database-path.js +5 -0
  43. package/dist/cli/lib/local-index/database-read.js +88 -0
  44. package/dist/cli/lib/local-index/effect-graph-read-model.js +112 -0
  45. package/dist/cli/lib/local-index/freshness.js +60 -0
  46. package/dist/cli/lib/local-index/index.js +12 -1866
  47. package/dist/cli/lib/local-index/path-surface-read-model.js +134 -0
  48. package/dist/cli/lib/local-index/populate.js +474 -0
  49. package/dist/cli/lib/local-index/schema.js +413 -0
  50. package/dist/cli/lib/local-index/search-read-model.js +533 -0
  51. package/dist/cli/lib/local-index/search-text.js +79 -0
  52. package/dist/cli/lib/option-parser.js +93 -0
  53. package/dist/cli/lib/repo-map.js +2 -2
  54. package/dist/cli/lib/run-plan.js +5 -22
  55. package/dist/core/change-verification.js +11 -5
  56. package/dist/core/command-effects.js +1 -3
  57. package/dist/core/command-intent-eligibility.js +14 -0
  58. package/dist/core/command-preconditions.js +8 -4
  59. package/dist/core/command-run-constraints.js +43 -0
  60. package/dist/core/public-json-contracts.js +57 -0
  61. package/dist/core/test-selection.js +8 -2
  62. package/dist/core/verification-plan.js +32 -4
  63. package/package.json +1 -1
  64. package/schemas/README.md +16 -0
  65. package/schemas/api-serve-response.schema.json +89 -0
  66. package/schemas/change-verification-report.schema.json +4 -1
  67. package/schemas/contract-lint-report.schema.json +1 -0
  68. package/schemas/evidence-report.schema.json +287 -0
  69. package/schemas/explain-report.schema.json +4 -0
  70. package/schemas/next-report.schema.json +121 -0
  71. package/schemas/onboard-commands-report.schema.json +100 -0
  72. package/schemas/workspace-command-catalog.schema.json +172 -0
  73. package/schemas/workspace-status.schema.json +141 -0
  74. package/schemas/workspace-verification-plan.schema.json +195 -0
  75. package/templates/default/i18n.toml +1 -1
  76. package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
  77. package/templates/default/locales/en/.mustflow/skills/clarifying-question-gate/SKILL.md +183 -0
  78. package/templates/default/locales/en/.mustflow/skills/routes.toml +7 -1
  79. package/templates/default/locales/en/.mustflow/skills/structure-discovery-gate/SKILL.md +63 -20
  80. package/templates/default/manifest.toml +8 -1
package/README.md CHANGED
@@ -18,7 +18,7 @@ The core concept is straightforward: place `AGENTS.md` at the project root and k
18
18
 
19
19
  - Use mustflow in your repository: start with [Quick start](#quick-start), then review the [no-guessing workflow](#no-guessing-workflow) and [`examples/minimal-js/`](examples/minimal-js/).
20
20
  - Contribute to mustflow: read [CONTRIBUTING.md](CONTRIBUTING.md), then run only configured command intents from [`.mustflow/config/commands.toml`](.mustflow/config/commands.toml).
21
- - Build an AI coding tool or agent harness: use `AGENTS.md` and `mf context --json` for repository context, then consume JSON output and schemas from `mf classify`, `mf verify`, `mf run`, `mf dashboard`, and [`schemas/`](schemas/).
21
+ - Build an AI coding tool or agent harness: use `AGENTS.md` and `mf context --json` for repository context, then consume JSON output and schemas from `mf api`, `mf classify`, `mf verify`, `mf run`, `mf dashboard`, and [`schemas/`](schemas/).
22
22
 
23
23
  ## No-guessing workflow
24
24
 
@@ -248,6 +248,12 @@ mf run mustflow_update_apply
248
248
  | `mf adapters status` | Inspect existing host-specific instruction and adapter files without generating adapter files or granting command authority. |
249
249
  | `mf classify --changed` | Classify changed paths, public surfaces, and validation reasons. Add `--write <path>` to save the classification report. |
250
250
  | `mf contract-lint` | Inspect `.mustflow/config/commands.toml` for command-contract errors and warnings without running commands. Add `--suggest` to print non-runnable candidate snippets from existing command files. |
251
+ | `mf onboard commands` | Suggest review-only command-intent snippets from package.json, Makefile, or justfile without writing files or granting command authority. |
252
+ | `mf next` | Inspect install state, changed files, verification coverage, and command-contract gaps, then print the next safe mustflow action without running commands. |
253
+ | `mf evidence` | Summarize changed-file verification requirements, latest evidence, receipts, remaining risks, and gaps without running commands. |
254
+ | `mf workspace status` | Inspect configured workspace roots and nested repository contract readiness without granting parent-to-child command authority. |
255
+ | `mf workspace command-catalog` | Aggregate per-repository command intent availability with safe `mf run` entrypoints and no raw command strings. |
256
+ | `mf workspace verify --changed --plan-only` | Aggregate per-repository changed-file verification plans without running commands or granting parent-to-child command authority. |
251
257
  | `mf doctor` | Inspect the current mustflow root without writing files. |
252
258
  | `mf api workspace-summary --json` | Print a stable, read-only JSON summary for coding agents and external harnesses. |
253
259
  | `mf api command-catalog --json` | Print command intent availability and safe `mf run` entrypoints without exposing raw command strings. |
@@ -256,6 +262,7 @@ mf run mustflow_update_apply
256
262
  | `mf api diff-risk --changed --json` | Print a compact changed-file risk, verification summary, and read-only residual correction signals. |
257
263
  | `mf api health --json` | Print a compact workspace health report for quick agent gating. |
258
264
  | `mf api locks --json` | Print active `mf run` locks for multi-session coordination. |
265
+ | `mf api serve --stdio` | Serve the same read-only API reports as newline-delimited JSON responses over stdin/stdout. |
259
266
  | `mf docs review list` | Show documents still waiting for prose review after agent edits. |
260
267
  | `mf docs review add <path>` | Add or refresh a document review queue entry. |
261
268
  | `mf docs review comment <path>` | Add multiline review guidance to an existing queue entry. |
@@ -287,7 +294,7 @@ mf run mustflow_update_apply
287
294
  | `mf explain skills` | Explain the strict skill index/body alignment summary used by `mf doctor --strict`. |
288
295
  | `mf explain surface [path]` | Explain how a path maps to public-surface and validation categories. |
289
296
 
290
- Automation and agents should use `--json` output instead of parsing human-facing text. Published JSON Schemas for stable outputs live in `schemas/`.
297
+ Automation and agents should use `--json` output or `mf api serve --stdio` JSONL responses instead of parsing human-facing text. Published JSON Schemas for stable outputs live in `schemas/`.
291
298
 
292
299
  ## Command execution policy
293
300
 
@@ -366,6 +373,9 @@ These are ideas not yet officially supported:
366
373
  ## Development
367
374
 
368
375
  Development commands in this repository use Bun. Users do not need Bun to run `mf` in their own projects.
376
+ The mustflow source repository dogfoods the installed workflow files at the repository root: agents
377
+ should read `AGENTS.md`, `.mustflow/docs/agent-workflow.md`, and `.mustflow/config/commands.toml`
378
+ before changing code or running verification.
369
379
 
370
380
  ```sh
371
381
  bun install
@@ -1,7 +1,11 @@
1
1
  import { inspectAdapterCompatibility } from '../../core/adapter-compatibility.js';
2
2
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
3
3
  import { t } from '../lib/i18n.js';
4
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
5
  import { resolveMustflowRoot } from '../lib/project-root.js';
6
+ const ADAPTERS_STATUS_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ ];
5
9
  export function getAdaptersHelp(lang = 'en') {
6
10
  return renderHelp({
7
11
  usage: 'mf adapters status [options]',
@@ -19,9 +23,8 @@ export function getAdaptersHelp(lang = 'en') {
19
23
  }
20
24
  function parseAdaptersArgs(args, lang) {
21
25
  const [action, ...rest] = args;
22
- const json = rest.includes('--json');
23
- const supported = new Set(['--json']);
24
- const unsupported = rest.find((arg) => arg.startsWith('-') && !supported.has(arg));
26
+ const parsed = parseCliOptions(rest, ADAPTERS_STATUS_OPTIONS, { allowPositionals: true });
27
+ const json = hasParsedCliOption(parsed, '--json');
25
28
  if (!action) {
26
29
  return {
27
30
  action: 'status',
@@ -36,12 +39,11 @@ function parseAdaptersArgs(args, lang) {
36
39
  error: t(lang, 'adapters.error.unknownAction', { action }),
37
40
  };
38
41
  }
39
- if (unsupported) {
40
- return { action, json, error: t(lang, 'cli.error.unknownOption', { option: unsupported }) };
42
+ if (parsed.error) {
43
+ return { action, json, error: formatCliOptionParseError(parsed.error, lang) };
41
44
  }
42
- if (rest.some((arg) => !arg.startsWith('-'))) {
43
- const unexpected = rest.find((arg) => !arg.startsWith('-'));
44
- return { action, json, error: t(lang, 'cli.error.unexpectedArgument', { argument: unexpected }) };
45
+ if (parsed.positionals.length > 0) {
46
+ return { action, json, error: t(lang, 'cli.error.unexpectedArgument', { argument: parsed.positionals[0] ?? '' }) };
45
47
  }
46
48
  return { action, json };
47
49
  }
@@ -71,7 +73,7 @@ function renderAdapterReport(report, lang) {
71
73
  return lines.join('\n');
72
74
  }
73
75
  export function runAdapters(args, reporter, lang = 'en') {
74
- if (args.includes('--help') || args.includes('-h')) {
76
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
75
77
  reporter.stdout(getAdaptersHelp(lang));
76
78
  return 0;
77
79
  }
@@ -1,11 +1,13 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createInterface } from 'node:readline';
3
4
  import { createClassifyOutput } from './classify.js';
4
5
  import { listActiveRunLocks } from '../../core/active-run-locks.js';
5
6
  import { createChangeVerificationReport, } from '../../core/change-verification.js';
6
7
  import { readUtf8FileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
7
8
  import { createVerificationPlanId } from '../../core/verification-plan-id.js';
8
9
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
10
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
9
11
  import { getAgentContext } from '../lib/agent-context.js';
10
12
  import { isRecord, readCommandContract, readPositiveInteger, readString, readStringArray, } from '../lib/command-contract.js';
11
13
  import { readGitChangedFiles } from '../lib/git-changes.js';
@@ -20,58 +22,97 @@ const API_LATEST_EVIDENCE_SCHEMA_VERSION = '1';
20
22
  const API_DIFF_RISK_SCHEMA_VERSION = '1';
21
23
  const API_HEALTH_SCHEMA_VERSION = '1';
22
24
  const API_LOCKS_SCHEMA_VERSION = '1';
25
+ const API_SERVE_SCHEMA_VERSION = '1';
23
26
  const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
24
27
  const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
25
28
  const LOCKS_RELATIVE_PATH = '.mustflow/state/locks';
26
29
  const MUSTFLOW_JSON_MAX_BYTES = 1024 * 1024;
30
+ const API_JSON_ONLY_OPTIONS = [
31
+ { name: '--json', kind: 'boolean' },
32
+ { name: '--help', kind: 'boolean', aliases: ['-h'] },
33
+ ];
34
+ const API_CHANGED_JSON_OPTIONS = [
35
+ { name: '--changed', kind: 'boolean' },
36
+ { name: '--json', kind: 'boolean' },
37
+ { name: '--help', kind: 'boolean', aliases: ['-h'] },
38
+ ];
39
+ const API_SERVE_OPTIONS = [
40
+ { name: '--stdio', kind: 'boolean' },
41
+ { name: '--help', kind: 'boolean', aliases: ['-h'] },
42
+ ];
43
+ const API_REPORT_ACTIONS = [
44
+ {
45
+ action: 'workspace-summary',
46
+ requiresChanged: false,
47
+ helpKey: 'api.help.action.workspaceSummary',
48
+ example: 'mf api workspace-summary --json',
49
+ },
50
+ {
51
+ action: 'command-catalog',
52
+ requiresChanged: false,
53
+ helpKey: 'api.help.action.commandCatalog',
54
+ example: 'mf api command-catalog --json',
55
+ },
56
+ {
57
+ action: 'verification-plan',
58
+ requiresChanged: true,
59
+ helpKey: 'api.help.action.verificationPlan',
60
+ example: 'mf api verification-plan --changed --json',
61
+ },
62
+ {
63
+ action: 'latest-evidence',
64
+ requiresChanged: false,
65
+ helpKey: 'api.help.action.latestEvidence',
66
+ example: 'mf api latest-evidence --json',
67
+ },
68
+ {
69
+ action: 'diff-risk',
70
+ requiresChanged: true,
71
+ helpKey: 'api.help.action.diffRisk',
72
+ example: 'mf api diff-risk --changed --json',
73
+ },
74
+ {
75
+ action: 'health',
76
+ requiresChanged: false,
77
+ helpKey: 'api.help.action.health',
78
+ example: 'mf api health --json',
79
+ },
80
+ {
81
+ action: 'locks',
82
+ requiresChanged: false,
83
+ helpKey: 'api.help.action.locks',
84
+ example: 'mf api locks --json',
85
+ },
86
+ ];
87
+ const API_REPORT_ACTION_NAMES = new Set(API_REPORT_ACTIONS.map((spec) => spec.action));
88
+ function isApiReportAction(value) {
89
+ return API_REPORT_ACTION_NAMES.has(value);
90
+ }
91
+ function apiReportActionSpec(action) {
92
+ const spec = API_REPORT_ACTIONS.find((candidate) => candidate.action === action);
93
+ if (!spec) {
94
+ throw new Error(`Unknown API report action: ${action}`);
95
+ }
96
+ return spec;
97
+ }
27
98
  export function getApiHelp(lang = 'en') {
28
99
  return renderHelp({
29
100
  usage: 'mf api <action> [options]',
30
101
  summary: t(lang, 'api.help.summary'),
31
102
  commands: [
32
- {
33
- label: 'workspace-summary',
34
- description: t(lang, 'api.help.action.workspaceSummary'),
35
- },
36
- {
37
- label: 'command-catalog',
38
- description: t(lang, 'api.help.action.commandCatalog'),
39
- },
40
- {
41
- label: 'verification-plan',
42
- description: t(lang, 'api.help.action.verificationPlan'),
43
- },
44
- {
45
- label: 'latest-evidence',
46
- description: t(lang, 'api.help.action.latestEvidence'),
47
- },
48
- {
49
- label: 'diff-risk',
50
- description: t(lang, 'api.help.action.diffRisk'),
51
- },
52
- {
53
- label: 'health',
54
- description: t(lang, 'api.help.action.health'),
55
- },
56
- {
57
- label: 'locks',
58
- description: t(lang, 'api.help.action.locks'),
59
- },
103
+ ...API_REPORT_ACTIONS.map((spec) => ({
104
+ label: spec.action,
105
+ description: t(lang, spec.helpKey),
106
+ })),
107
+ { label: 'serve', description: t(lang, 'api.help.action.serve') },
60
108
  ],
61
109
  options: [
62
110
  { label: '--changed', description: t(lang, 'classify.help.option.changed') },
63
111
  { label: '--json', description: t(lang, 'cli.option.json') },
112
+ { label: '--stdio', description: t(lang, 'api.help.option.stdio') },
64
113
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
65
114
  ],
66
- examples: [
67
- 'mf api workspace-summary --json',
68
- 'mf api command-catalog --json',
69
- 'mf api verification-plan --changed --json',
70
- 'mf api latest-evidence --json',
71
- 'mf api diff-risk --changed --json',
72
- 'mf api health --json',
73
- 'mf api locks --json',
74
- ],
115
+ examples: [...API_REPORT_ACTIONS.map((spec) => spec.example), 'mf api serve --stdio'],
75
116
  exitCodes: [
76
117
  { label: '0', description: t(lang, 'api.help.exit.ok') },
77
118
  { label: '1', description: t(lang, 'cli.common.invalidInput') },
@@ -983,95 +1024,217 @@ function createLocksOutput() {
983
1024
  };
984
1025
  }
985
1026
  }
1027
+ function createApiReport(action) {
1028
+ switch (action) {
1029
+ case 'workspace-summary':
1030
+ return createWorkspaceSummaryOutput();
1031
+ case 'command-catalog':
1032
+ return createCommandCatalogOutput();
1033
+ case 'verification-plan':
1034
+ return createVerificationPlanOutput();
1035
+ case 'latest-evidence':
1036
+ return createLatestEvidenceOutput();
1037
+ case 'diff-risk':
1038
+ return createDiffRiskOutput();
1039
+ case 'health':
1040
+ return createHealthOutput();
1041
+ case 'locks':
1042
+ return createLocksOutput();
1043
+ }
1044
+ }
1045
+ function writeApiReport(action, reporter) {
1046
+ reporter.stdout(JSON.stringify(createApiReport(action), null, 2));
1047
+ }
1048
+ function createApiServePolicy() {
1049
+ return {
1050
+ mode: 'read_only',
1051
+ executes_commands: false,
1052
+ direct_commands_allowed: false,
1053
+ raw_output_included: false,
1054
+ hidden_reasoning_included: false,
1055
+ };
1056
+ }
1057
+ function createApiServeError(id, code, message) {
1058
+ return {
1059
+ schema_version: API_SERVE_SCHEMA_VERSION,
1060
+ command: 'api serve',
1061
+ transport: 'stdio',
1062
+ id,
1063
+ ok: false,
1064
+ policy: createApiServePolicy(),
1065
+ error: {
1066
+ code,
1067
+ message,
1068
+ },
1069
+ };
1070
+ }
1071
+ function createApiServeSuccess(id, result) {
1072
+ return {
1073
+ schema_version: API_SERVE_SCHEMA_VERSION,
1074
+ command: 'api serve',
1075
+ transport: 'stdio',
1076
+ id,
1077
+ ok: true,
1078
+ policy: createApiServePolicy(),
1079
+ result,
1080
+ };
1081
+ }
1082
+ function readApiServeId(request) {
1083
+ if (!isRecord(request) || !Object.hasOwn(request, 'id')) {
1084
+ return null;
1085
+ }
1086
+ const value = request.id;
1087
+ if (typeof value === 'string' || typeof value === 'number' || value === null) {
1088
+ return value;
1089
+ }
1090
+ return null;
1091
+ }
1092
+ function parseApiServeRequestLine(line) {
1093
+ let parsed;
1094
+ try {
1095
+ parsed = JSON.parse(line);
1096
+ }
1097
+ catch (error) {
1098
+ const message = error instanceof Error ? error.message : String(error);
1099
+ return {
1100
+ request: null,
1101
+ error: createApiServeError(null, 'invalid_json', `Invalid JSON request: ${message}`),
1102
+ };
1103
+ }
1104
+ const id = readApiServeId(parsed);
1105
+ if (!isRecord(parsed)) {
1106
+ return {
1107
+ request: null,
1108
+ error: createApiServeError(id, 'invalid_request', 'Request must be a JSON object.'),
1109
+ };
1110
+ }
1111
+ return {
1112
+ request: {
1113
+ id,
1114
+ action: parsed.action,
1115
+ changed: parsed.changed,
1116
+ },
1117
+ error: null,
1118
+ };
1119
+ }
1120
+ function createApiServeResponse(request) {
1121
+ const id = typeof request.id === 'string' || typeof request.id === 'number' || request.id === null ? request.id : null;
1122
+ if (typeof request.action !== 'string') {
1123
+ return createApiServeError(id, 'invalid_request', 'Request field "action" must be a string.');
1124
+ }
1125
+ if (!isApiReportAction(request.action)) {
1126
+ return createApiServeError(id, 'unknown_action', `Unknown api action: ${request.action}`);
1127
+ }
1128
+ const spec = apiReportActionSpec(request.action);
1129
+ if (spec.requiresChanged && request.changed !== true) {
1130
+ return createApiServeError(id, 'action_requires_changed', `${request.action} requires changed: true.`);
1131
+ }
1132
+ if (!spec.requiresChanged && request.changed === true) {
1133
+ return createApiServeError(id, 'action_does_not_accept_changed', `${request.action} does not accept changed: true.`);
1134
+ }
1135
+ try {
1136
+ return createApiServeSuccess(id, createApiReport(request.action));
1137
+ }
1138
+ catch (error) {
1139
+ return createApiServeError(id, 'report_unavailable', error instanceof Error ? error.message : String(error));
1140
+ }
1141
+ }
1142
+ function writeApiServeResponse(response, reporter) {
1143
+ const line = `${JSON.stringify(response)}\n`;
1144
+ if (reporter.writeStdout) {
1145
+ reporter.writeStdout(line);
1146
+ return;
1147
+ }
1148
+ reporter.stdout(line.trimEnd());
1149
+ }
1150
+ async function runApiServe(args, reporter, lang) {
1151
+ const parsed = parseCliOptions(args, API_SERVE_OPTIONS);
1152
+ if (hasParsedCliOption(parsed, '--help')) {
1153
+ reporter.stdout(getApiHelp(lang));
1154
+ return 0;
1155
+ }
1156
+ if (parsed.error) {
1157
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf api --help', getApiHelp(lang), lang);
1158
+ return 1;
1159
+ }
1160
+ if (!hasParsedCliOption(parsed, '--stdio')) {
1161
+ printUsageError(reporter, t(lang, 'api.error.serveRequiresStdio'), 'mf api --help', getApiHelp(lang), lang);
1162
+ return 1;
1163
+ }
1164
+ const input = createInterface({
1165
+ input: process.stdin,
1166
+ crlfDelay: Infinity,
1167
+ });
1168
+ for await (const rawLine of input) {
1169
+ const line = rawLine.trim();
1170
+ if (line.length === 0) {
1171
+ continue;
1172
+ }
1173
+ const parsed = parseApiServeRequestLine(line);
1174
+ if (parsed.error) {
1175
+ writeApiServeResponse(parsed.error, reporter);
1176
+ continue;
1177
+ }
1178
+ if (!parsed.request) {
1179
+ writeApiServeResponse(createApiServeError(null, 'invalid_request', 'Request must be a JSON object.'), reporter);
1180
+ continue;
1181
+ }
1182
+ writeApiServeResponse(createApiServeResponse(parsed.request), reporter);
1183
+ }
1184
+ return 0;
1185
+ }
986
1186
  function validateJsonOnlyAction(action, args, reporter, lang) {
987
- if (args.includes('--help') || args.includes('-h')) {
1187
+ const parsed = parseCliOptions(args, API_JSON_ONLY_OPTIONS);
1188
+ if (hasParsedCliOption(parsed, '--help')) {
988
1189
  reporter.stdout(getApiHelp(lang));
989
1190
  return false;
990
1191
  }
991
- const supported = new Set(['--json']);
992
- const unsupported = args.filter((arg) => !supported.has(arg));
993
- if (unsupported.length > 0) {
994
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf api --help', getApiHelp(lang), lang);
1192
+ if (parsed.error) {
1193
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf api --help', getApiHelp(lang), lang);
995
1194
  return false;
996
1195
  }
997
- if (!args.includes('--json')) {
1196
+ if (!hasParsedCliOption(parsed, '--json')) {
998
1197
  printUsageError(reporter, t(lang, 'api.error.actionRequiresJson', { action }), 'mf api --help', getApiHelp(lang), lang);
999
1198
  return false;
1000
1199
  }
1001
1200
  return true;
1002
1201
  }
1003
- function runWorkspaceSummary(args, reporter, lang) {
1004
- if (!validateJsonOnlyAction('workspace-summary', args, reporter, lang)) {
1005
- return args.includes('--help') || args.includes('-h') ? 0 : 1;
1006
- }
1007
- reporter.stdout(JSON.stringify(createWorkspaceSummaryOutput(), null, 2));
1008
- return 0;
1009
- }
1010
- function runCommandCatalog(args, reporter, lang) {
1011
- if (!validateJsonOnlyAction('command-catalog', args, reporter, lang)) {
1012
- return args.includes('--help') || args.includes('-h') ? 0 : 1;
1013
- }
1014
- reporter.stdout(JSON.stringify(createCommandCatalogOutput(), null, 2));
1015
- return 0;
1016
- }
1017
1202
  function validateChangedJsonAction(action, args, reporter, lang) {
1018
- if (args.includes('--help') || args.includes('-h')) {
1203
+ const parsed = parseCliOptions(args, API_CHANGED_JSON_OPTIONS);
1204
+ if (hasParsedCliOption(parsed, '--help')) {
1019
1205
  reporter.stdout(getApiHelp(lang));
1020
1206
  return false;
1021
1207
  }
1022
- const supported = new Set(['--changed', '--json']);
1023
- const unsupported = args.filter((arg) => !supported.has(arg));
1024
- if (unsupported.length > 0) {
1025
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf api --help', getApiHelp(lang), lang);
1208
+ if (parsed.error) {
1209
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf api --help', getApiHelp(lang), lang);
1026
1210
  return false;
1027
1211
  }
1028
- if (!args.includes('--json')) {
1212
+ if (!hasParsedCliOption(parsed, '--json')) {
1029
1213
  printUsageError(reporter, t(lang, 'api.error.actionRequiresJson', { action }), 'mf api --help', getApiHelp(lang), lang);
1030
1214
  return false;
1031
1215
  }
1032
- if (!args.includes('--changed')) {
1216
+ if (!hasParsedCliOption(parsed, '--changed')) {
1033
1217
  printUsageError(reporter, t(lang, 'api.error.actionRequiresChanged', { action }), 'mf api --help', getApiHelp(lang), lang);
1034
1218
  return false;
1035
1219
  }
1036
1220
  return true;
1037
1221
  }
1038
- function runVerificationPlan(args, reporter, lang) {
1039
- if (!validateChangedJsonAction('verification-plan', args, reporter, lang)) {
1040
- return args.includes('--help') || args.includes('-h') ? 0 : 1;
1041
- }
1042
- reporter.stdout(JSON.stringify(createVerificationPlanOutput(), null, 2));
1043
- return 0;
1044
- }
1045
- function runLatestEvidence(args, reporter, lang) {
1046
- if (!validateJsonOnlyAction('latest-evidence', args, reporter, lang)) {
1047
- return args.includes('--help') || args.includes('-h') ? 0 : 1;
1222
+ function runJsonOnlyApiReport(action, args, reporter, lang) {
1223
+ if (!validateJsonOnlyAction(action, args, reporter, lang)) {
1224
+ return hasCliOptionToken(args, '--help', ['-h']) ? 0 : 1;
1048
1225
  }
1049
- reporter.stdout(JSON.stringify(createLatestEvidenceOutput(), null, 2));
1226
+ writeApiReport(action, reporter);
1050
1227
  return 0;
1051
1228
  }
1052
- function runDiffRisk(args, reporter, lang) {
1053
- if (!validateChangedJsonAction('diff-risk', args, reporter, lang)) {
1054
- return args.includes('--help') || args.includes('-h') ? 0 : 1;
1229
+ function runChangedApiReport(action, args, reporter, lang) {
1230
+ if (!validateChangedJsonAction(action, args, reporter, lang)) {
1231
+ return hasCliOptionToken(args, '--help', ['-h']) ? 0 : 1;
1055
1232
  }
1056
- reporter.stdout(JSON.stringify(createDiffRiskOutput(), null, 2));
1057
- return 0;
1058
- }
1059
- function runHealth(args, reporter, lang) {
1060
- if (!validateJsonOnlyAction('health', args, reporter, lang)) {
1061
- return args.includes('--help') || args.includes('-h') ? 0 : 1;
1062
- }
1063
- reporter.stdout(JSON.stringify(createHealthOutput(), null, 2));
1064
- return 0;
1065
- }
1066
- function runLocks(args, reporter, lang) {
1067
- if (!validateJsonOnlyAction('locks', args, reporter, lang)) {
1068
- return args.includes('--help') || args.includes('-h') ? 0 : 1;
1069
- }
1070
- reporter.stdout(JSON.stringify(createLocksOutput(), null, 2));
1233
+ writeApiReport(action, reporter);
1071
1234
  return 0;
1072
1235
  }
1073
1236
  export function runApi(args, reporter, lang = 'en') {
1074
- if (args.includes('--help') || args.includes('-h')) {
1237
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
1075
1238
  reporter.stdout(getApiHelp(lang));
1076
1239
  return 0;
1077
1240
  }
@@ -1084,26 +1247,13 @@ export function runApi(args, reporter, lang = 'en') {
1084
1247
  printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: action }), 'mf api --help', getApiHelp(lang), lang);
1085
1248
  return 1;
1086
1249
  }
1087
- if (action === 'workspace-summary') {
1088
- return runWorkspaceSummary(rest, reporter, lang);
1089
- }
1090
- if (action === 'command-catalog') {
1091
- return runCommandCatalog(rest, reporter, lang);
1092
- }
1093
- if (action === 'verification-plan') {
1094
- return runVerificationPlan(rest, reporter, lang);
1095
- }
1096
- if (action === 'latest-evidence') {
1097
- return runLatestEvidence(rest, reporter, lang);
1098
- }
1099
- if (action === 'diff-risk') {
1100
- return runDiffRisk(rest, reporter, lang);
1101
- }
1102
- if (action === 'health') {
1103
- return runHealth(rest, reporter, lang);
1250
+ if (action === 'serve') {
1251
+ return runApiServe(rest, reporter, lang);
1104
1252
  }
1105
- if (action === 'locks') {
1106
- return runLocks(rest, reporter, lang);
1253
+ if (isApiReportAction(action)) {
1254
+ return apiReportActionSpec(action).requiresChanged
1255
+ ? runChangedApiReport(action, rest, reporter, lang)
1256
+ : runJsonOnlyApiReport(action, rest, reporter, lang);
1107
1257
  }
1108
1258
  printUsageError(reporter, t(lang, 'api.error.unknownAction', { action }), 'mf api --help', getApiHelp(lang), lang);
1109
1259
  return 1;
@@ -1,7 +1,12 @@
1
1
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
2
  import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
3
4
  import { resolveMustflowRoot } from '../lib/project-root.js';
4
5
  import { checkMustflowProjectReport } from '../lib/validation.js';
6
+ const CHECK_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--strict', kind: 'boolean' },
9
+ ];
5
10
  export function getCheckHelp(lang = 'en') {
6
11
  return renderHelp({
7
12
  usage: 'mf check [options]',
@@ -28,22 +33,21 @@ export function getCheckHelp(lang = 'en') {
28
33
  }, lang);
29
34
  }
30
35
  export function runCheck(args, reporter, lang = 'en') {
31
- if (args.includes('--help') || args.includes('-h')) {
36
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
32
37
  reporter.stdout(getCheckHelp(lang));
33
38
  return 0;
34
39
  }
35
- const supported = new Set(['--json', '--strict']);
36
- const unsupported = args.filter((arg) => !supported.has(arg));
37
- if (unsupported.length > 0) {
38
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf check --help', getCheckHelp(lang), lang);
40
+ const options = parseCliOptions(args, CHECK_OPTIONS);
41
+ if (options.error) {
42
+ printUsageError(reporter, formatCliOptionParseError(options.error, lang), 'mf check --help', getCheckHelp(lang), lang);
39
43
  return 1;
40
44
  }
41
- const strict = args.includes('--strict');
45
+ const strict = hasParsedCliOption(options, '--strict');
42
46
  const report = checkMustflowProjectReport(resolveMustflowRoot(), { strict });
43
47
  const issues = report.issues;
44
48
  const warnings = report.warnings;
45
49
  const ok = issues.length === 0;
46
- if (args.includes('--json')) {
50
+ if (hasParsedCliOption(options, '--json')) {
47
51
  reporter.stdout(JSON.stringify({
48
52
  ok,
49
53
  strict,