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
@@ -5,8 +5,14 @@ import { writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.j
5
5
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
6
6
  import { requireGitChangedFiles } from '../lib/git-changes.js';
7
7
  import { t } from '../lib/i18n.js';
8
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
8
9
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
10
  const CLASSIFY_SCHEMA_VERSION = '1';
11
+ const CLASSIFY_OPTIONS = [
12
+ { name: '--json', kind: 'boolean' },
13
+ { name: '--changed', kind: 'boolean' },
14
+ { name: '--write', kind: 'string' },
15
+ ];
10
16
  export function getClassifyHelp(lang = 'en') {
11
17
  return renderHelp({
12
18
  usage: 'mf classify --changed [options] | mf classify <path...> [options]',
@@ -29,43 +35,14 @@ export function getClassifyHelp(lang = 'en') {
29
35
  }, lang);
30
36
  }
31
37
  function parseClassifyArgs(args) {
32
- const paths = [];
33
- let json = false;
34
- let changed = false;
35
- let writePath;
36
- for (let index = 0; index < args.length; index += 1) {
37
- const arg = args[index];
38
- if (arg === '--json') {
39
- json = true;
40
- continue;
41
- }
42
- if (arg === '--changed') {
43
- changed = true;
44
- continue;
45
- }
46
- if (arg === '--write') {
47
- const value = args[index + 1];
48
- if (!value || value.startsWith('-')) {
49
- return { json, changed, writePath, paths, error: 'missing_write_value' };
50
- }
51
- writePath = value;
52
- index += 1;
53
- continue;
54
- }
55
- if (arg.startsWith('--write=')) {
56
- const value = arg.slice('--write='.length);
57
- if (value.length === 0) {
58
- return { json, changed, writePath, paths, error: 'missing_write_value' };
59
- }
60
- writePath = value;
61
- continue;
62
- }
63
- if (arg.startsWith('-')) {
64
- return { json, changed, writePath, paths, error: arg };
65
- }
66
- paths.push(arg);
67
- }
68
- return { json, changed, writePath, paths };
38
+ const parsed = parseCliOptions(args, CLASSIFY_OPTIONS, { allowPositionals: true });
39
+ return {
40
+ json: hasParsedCliOption(parsed, '--json'),
41
+ changed: hasParsedCliOption(parsed, '--changed'),
42
+ writePath: getParsedCliStringOption(parsed, '--write'),
43
+ paths: parsed.positionals,
44
+ error: parsed.error,
45
+ };
69
46
  }
70
47
  export function createClassifyOutput(projectRoot, source, paths) {
71
48
  const files = source === 'changed' ? requireGitChangedFiles(projectRoot) : paths;
@@ -116,16 +93,13 @@ function writeClassifyOutput(projectRoot, inputPath, output) {
116
93
  writeJsonFileInsideWithoutSymlinks(projectRoot, outputPath, output);
117
94
  }
118
95
  export function runClassify(args, reporter, lang = 'en') {
119
- if (args.includes('--help') || args.includes('-h')) {
96
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
120
97
  reporter.stdout(getClassifyHelp(lang));
121
98
  return 0;
122
99
  }
123
100
  const parsed = parseClassifyArgs(args);
124
101
  if (parsed.error) {
125
- const message = parsed.error === 'missing_write_value'
126
- ? t(lang, 'cli.error.missingValue', { option: '--write' })
127
- : t(lang, 'cli.error.unknownOption', { option: parsed.error });
128
- printUsageError(reporter, message, 'mf classify --help', getClassifyHelp(lang), lang);
102
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf classify --help', getClassifyHelp(lang), lang);
129
103
  return 1;
130
104
  }
131
105
  if (parsed.changed && parsed.paths.length > 0) {
@@ -1,6 +1,7 @@
1
1
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
2
  import { getAgentContext, getPromptCacheProfileContext } from '../lib/agent-context.js';
3
3
  import { t } from '../lib/i18n.js';
4
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
5
  import { resolveMustflowRoot } from '../lib/project-root.js';
5
6
  export function getContextHelp(lang = 'en') {
6
7
  return renderHelp({
@@ -27,55 +28,41 @@ export function getContextHelp(lang = 'en') {
27
28
  ],
28
29
  }, lang);
29
30
  }
31
+ const CONTEXT_OPTIONS = [
32
+ { name: '--json', kind: 'boolean' },
33
+ { name: '--cache-profile', kind: 'string' },
34
+ ];
30
35
  const CACHE_PROFILES = new Set(['stable', 'task', 'volatile', 'all']);
31
- function parseCacheProfile(args) {
32
- const index = args.indexOf('--cache-profile');
33
- if (index === -1) {
34
- return { profile: null, error: null };
35
- }
36
- const value = args[index + 1];
37
- if (!value || value.startsWith('-')) {
38
- return { profile: null, error: 'missing' };
36
+ function parseCacheProfile(value) {
37
+ if (value === null) {
38
+ return { profile: null, invalid: false };
39
39
  }
40
40
  if (!CACHE_PROFILES.has(value)) {
41
- return { profile: null, error: value };
41
+ return { profile: null, invalid: true };
42
42
  }
43
- return { profile: value, error: null };
43
+ return { profile: value, invalid: false };
44
44
  }
45
45
  export async function runContext(args, reporter, lang = 'en') {
46
- if (args.includes('--help') || args.includes('-h')) {
46
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
47
47
  reporter.stdout(getContextHelp(lang));
48
48
  return 0;
49
49
  }
50
- const supported = new Set(['--json', '--cache-profile']);
51
- const cacheProfile = parseCacheProfile(args);
52
- const unsupported = args.filter((arg, index) => {
53
- if (arg === '--cache-profile') {
54
- return false;
55
- }
56
- if (index > 0 && args[index - 1] === '--cache-profile') {
57
- return false;
58
- }
59
- return !supported.has(arg);
60
- });
61
- if (unsupported.length > 0) {
62
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf context --help', getContextHelp(lang), lang);
63
- return 1;
64
- }
65
- if (cacheProfile.error === 'missing') {
66
- printUsageError(reporter, t(lang, 'cli.error.missingValue', { option: '--cache-profile' }), 'mf context --help', getContextHelp(lang), lang);
50
+ const parsed = parseCliOptions(args, CONTEXT_OPTIONS);
51
+ if (parsed.error) {
52
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf context --help', getContextHelp(lang), lang);
67
53
  return 1;
68
54
  }
69
- if (cacheProfile.error) {
55
+ const cacheProfile = parseCacheProfile(getParsedCliStringOption(parsed, '--cache-profile'));
56
+ if (cacheProfile.invalid) {
70
57
  printUsageError(reporter, t(lang, 'cli.error.unexpectedValue', { option: '--cache-profile' }), 'mf context --help', getContextHelp(lang), lang);
71
58
  return 1;
72
59
  }
73
- if (cacheProfile.profile && !args.includes('--json')) {
60
+ if (cacheProfile.profile && !hasParsedCliOption(parsed, '--json')) {
74
61
  printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: '--cache-profile' }), 'mf context --help', getContextHelp(lang), lang);
75
62
  return 1;
76
63
  }
77
64
  const mustflowRoot = resolveMustflowRoot();
78
- if (args.includes('--json')) {
65
+ if (hasParsedCliOption(parsed, '--json')) {
79
66
  if (cacheProfile.profile) {
80
67
  reporter.stdout(JSON.stringify(await getPromptCacheProfileContext(mustflowRoot, cacheProfile.profile), null, 2));
81
68
  return 0;
@@ -5,9 +5,15 @@ import { readCommandContract, isRecord } from '../../core/config-loading.js';
5
5
  import { releaseVersioningIsEnabled } from '../../core/version-sources.js';
6
6
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
7
7
  import { t } from '../lib/i18n.js';
8
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
8
9
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
10
  import { readMustflowTomlFile } from '../lib/toml.js';
10
11
  const CONTRACT_LINT_SCHEMA_VERSION = '1';
12
+ const CONTRACT_LINT_OPTIONS = [
13
+ { name: '--coverage', kind: 'boolean' },
14
+ { name: '--suggest', kind: 'boolean' },
15
+ { name: '--json', kind: 'boolean' },
16
+ ];
11
17
  export function getContractLintHelp(lang = 'en') {
12
18
  return renderHelp({
13
19
  usage: 'mf contract-lint [options]',
@@ -82,18 +88,17 @@ function renderContractLintOutput(output, lang) {
82
88
  return lines.join('\n');
83
89
  }
84
90
  export function runContractLint(args, reporter, lang = 'en') {
85
- if (args.includes('--help') || args.includes('-h')) {
91
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
86
92
  reporter.stdout(getContractLintHelp(lang));
87
93
  return 0;
88
94
  }
89
- const supported = new Set(['--coverage', '--suggest', '--json']);
90
- const unsupported = args.filter((arg) => !supported.has(arg));
91
- if (unsupported.length > 0) {
92
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf contract-lint --help', getContractLintHelp(lang), lang);
95
+ const options = parseCliOptions(args, CONTRACT_LINT_OPTIONS);
96
+ if (options.error) {
97
+ printUsageError(reporter, formatCliOptionParseError(options.error, lang), 'mf contract-lint --help', getContractLintHelp(lang), lang);
93
98
  return 1;
94
99
  }
95
- const output = createContractLintOutput(resolveMustflowRoot(), args.includes('--coverage'), args.includes('--suggest'));
96
- if (args.includes('--json')) {
100
+ const output = createContractLintOutput(resolveMustflowRoot(), hasParsedCliOption(options, '--coverage'), hasParsedCliOption(options, '--suggest'));
101
+ if (hasParsedCliOption(options, '--json')) {
97
102
  reporter.stdout(JSON.stringify(output, null, 2));
98
103
  }
99
104
  else {
@@ -4,6 +4,7 @@ import http from 'node:http';
4
4
  import path from 'node:path';
5
5
  import { openUrlInBrowser } from '../lib/browser-open.js';
6
6
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
7
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
7
8
  import { renderDashboardHtml, } from '../lib/dashboard-html.js';
8
9
  import { DashboardExportPathError, writeDashboardExport, } from '../lib/dashboard-export.js';
9
10
  import { DASHBOARD_VERIFICATION_MAX_FILE_MATCHES, createDashboardVerificationSnapshot, } from '../../core/dashboard-verification.js';
@@ -18,6 +19,7 @@ import { markDashboardDocReviewFromPayload, openDashboardMustflowFolder, updateD
18
19
  import { MANIFEST_LOCK_RELATIVE_PATH, inspectManifestLock } from '../lib/manifest-lock.js';
19
20
  import { getLocalIndexDatabasePath, readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
20
21
  import { readPackageMetadata } from '../lib/package-info.js';
22
+ import { createRunPlan } from '../lib/run-plan.js';
21
23
  import { t } from '../lib/i18n.js';
22
24
  import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, } from '../lib/mustflow-read.js';
23
25
  import { resolveMustflowRoot } from '../lib/project-root.js';
@@ -27,6 +29,16 @@ const DEFAULT_DASHBOARD_HOST = '127.0.0.1';
27
29
  const DEFAULT_DASHBOARD_PORT = 0;
28
30
  const MAX_REQUEST_BYTES = 64 * 1024;
29
31
  const LOCAL_DASHBOARD_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
32
+ const DASHBOARD_OPTIONS = [
33
+ { name: '--host', kind: 'string' },
34
+ { name: '--port', kind: 'string' },
35
+ { name: '--open', kind: 'boolean' },
36
+ { name: '--no-open', kind: 'boolean' },
37
+ { name: '--json', kind: 'boolean' },
38
+ { name: '--export', kind: 'string' },
39
+ { name: '--export-json', kind: 'string' },
40
+ { name: '--help', kind: 'boolean', aliases: ['-h'] },
41
+ ];
30
42
  const RELEASE_FILE_PATTERNS = [
31
43
  /^package\.json$/u,
32
44
  /^bun\.lockb?$/u,
@@ -168,112 +180,34 @@ export function getDashboardHelp(lang = 'en') {
168
180
  }, lang);
169
181
  }
170
182
  function parseDashboardOptions(args, lang) {
171
- let host = DEFAULT_DASHBOARD_HOST;
183
+ const parsed = parseCliOptions(args, DASHBOARD_OPTIONS);
184
+ if (parsed.error) {
185
+ return { error: formatCliOptionParseError(parsed.error, lang) };
186
+ }
187
+ const host = getParsedCliStringOption(parsed, '--host') ?? DEFAULT_DASHBOARD_HOST;
172
188
  let port = DEFAULT_DASHBOARD_PORT;
173
- let json = false;
174
- let openBrowser = false;
175
- let exportPath;
176
- let exportFormat;
177
- let serverOptionUsed = false;
178
- for (let index = 0; index < args.length; index += 1) {
179
- const arg = args[index];
180
- if (!arg) {
181
- continue;
182
- }
183
- if (arg === '--json') {
184
- json = true;
185
- openBrowser = false;
186
- serverOptionUsed = true;
187
- continue;
188
- }
189
- if (arg === '--open') {
190
- openBrowser = true;
191
- serverOptionUsed = true;
192
- continue;
193
- }
194
- if (arg === '--no-open') {
195
- openBrowser = false;
196
- serverOptionUsed = true;
197
- continue;
198
- }
199
- if (arg === '--host') {
200
- const value = args[index + 1];
201
- if (!value || value.startsWith('-')) {
202
- return { error: t(lang, 'cli.error.missingValue', { option: '--host' }) };
203
- }
204
- host = value;
205
- serverOptionUsed = true;
206
- index += 1;
207
- continue;
208
- }
209
- if (arg.startsWith('--host=')) {
210
- host = arg.slice('--host='.length);
211
- serverOptionUsed = true;
212
- continue;
213
- }
214
- if (arg === '--port') {
215
- const value = args[index + 1];
216
- if (!value || value.startsWith('-')) {
217
- return { error: t(lang, 'cli.error.missingValue', { option: '--port' }) };
218
- }
219
- const parsedPort = Number(value);
220
- if (!Number.isInteger(parsedPort) || parsedPort < 0 || parsedPort > 65_535) {
221
- return { error: t(lang, 'dashboard.error.invalidPort', { port: value }) };
222
- }
223
- port = parsedPort;
224
- serverOptionUsed = true;
225
- index += 1;
226
- continue;
227
- }
228
- if (arg.startsWith('--port=')) {
229
- const value = arg.slice('--port='.length);
230
- const parsedPort = Number(value);
231
- if (!Number.isInteger(parsedPort) || parsedPort < 0 || parsedPort > 65_535) {
232
- return { error: t(lang, 'dashboard.error.invalidPort', { port: value }) };
233
- }
234
- port = parsedPort;
235
- serverOptionUsed = true;
236
- continue;
189
+ const portValue = getParsedCliStringOption(parsed, '--port');
190
+ if (portValue !== null) {
191
+ const parsedPort = Number(portValue);
192
+ if (!Number.isInteger(parsedPort) || parsedPort < 0 || parsedPort > 65_535) {
193
+ return { error: t(lang, 'dashboard.error.invalidPort', { port: portValue }) };
237
194
  }
238
- if (arg === '--export' || arg === '--export-json') {
239
- const value = args[index + 1];
240
- if (!value || value.startsWith('-')) {
241
- return { error: t(lang, 'cli.error.missingValue', { option: arg }) };
242
- }
243
- if (exportPath) {
244
- return { error: t(lang, 'dashboard.error.conflictingExportModes') };
245
- }
246
- exportPath = value;
247
- exportFormat = arg === '--export-json' ? 'json' : 'html';
248
- index += 1;
249
- continue;
250
- }
251
- if (arg.startsWith('--export=')) {
252
- if (exportPath) {
253
- return { error: t(lang, 'dashboard.error.conflictingExportModes') };
254
- }
255
- exportPath = arg.slice('--export='.length);
256
- exportFormat = 'html';
257
- if (!exportPath) {
258
- return { error: t(lang, 'cli.error.missingValue', { option: '--export' }) };
259
- }
260
- continue;
261
- }
262
- if (arg.startsWith('--export-json=')) {
263
- if (exportPath) {
264
- return { error: t(lang, 'dashboard.error.conflictingExportModes') };
265
- }
266
- exportPath = arg.slice('--export-json='.length);
267
- exportFormat = 'json';
268
- if (!exportPath) {
269
- return { error: t(lang, 'cli.error.missingValue', { option: '--export-json' }) };
270
- }
271
- continue;
272
- }
273
- if (arg.startsWith('-')) {
274
- return { error: t(lang, 'cli.error.unknownOption', { option: arg }) };
275
- }
276
- return { error: t(lang, 'cli.error.unexpectedArgument', { argument: arg }) };
195
+ port = parsedPort;
196
+ }
197
+ const json = hasParsedCliOption(parsed, '--json');
198
+ const lastOpenOption = lastOptionTokenName(args, ['--open', '--no-open']);
199
+ const openBrowser = lastOpenOption === '--open' && !json;
200
+ const serverOptionUsed = hasParsedCliOption(parsed, '--json')
201
+ || hasParsedCliOption(parsed, '--open')
202
+ || hasParsedCliOption(parsed, '--no-open')
203
+ || getParsedCliStringOption(parsed, '--host') !== null
204
+ || portValue !== null;
205
+ const htmlExportPath = getParsedCliStringOption(parsed, '--export');
206
+ const jsonExportPath = getParsedCliStringOption(parsed, '--export-json');
207
+ const exportPath = htmlExportPath ?? jsonExportPath ?? undefined;
208
+ const exportFormat = jsonExportPath !== null ? 'json' : htmlExportPath !== null ? 'html' : undefined;
209
+ if (countOptionTokens(args, ['--export', '--export-json']) > 1) {
210
+ return { error: t(lang, 'dashboard.error.conflictingExportModes') };
277
211
  }
278
212
  if (exportPath && serverOptionUsed) {
279
213
  return { error: t(lang, 'dashboard.error.exportServerOptions') };
@@ -284,11 +218,33 @@ function parseDashboardOptions(args, lang) {
284
218
  if (!LOCAL_DASHBOARD_HOSTS.has(host)) {
285
219
  return { error: t(lang, 'dashboard.error.nonLocalHost', { host }) };
286
220
  }
287
- if (json) {
288
- openBrowser = false;
289
- }
290
221
  return { options: { host, port, json, openBrowser } };
291
222
  }
223
+ function countOptionTokens(args, names) {
224
+ const nameSet = new Set(names);
225
+ let count = 0;
226
+ for (const arg of args) {
227
+ if (nameSet.has(optionTokenName(arg))) {
228
+ count += 1;
229
+ }
230
+ }
231
+ return count;
232
+ }
233
+ function lastOptionTokenName(args, names) {
234
+ const nameSet = new Set(names);
235
+ let lastName = null;
236
+ for (const arg of args) {
237
+ const name = optionTokenName(arg);
238
+ if (nameSet.has(name)) {
239
+ lastName = name;
240
+ }
241
+ }
242
+ return lastName;
243
+ }
244
+ function optionTokenName(arg) {
245
+ const separatorIndex = arg.indexOf('=');
246
+ return separatorIndex === -1 ? arg : arg.slice(0, separatorIndex);
247
+ }
292
248
  function sendJson(response, statusCode, value) {
293
249
  response.writeHead(statusCode, {
294
250
  'cache-control': 'no-store',
@@ -510,12 +466,7 @@ async function renderCommandContractResponse(projectRoot, contract) {
510
466
  const runPolicy = readString(intent, 'run_policy') ?? null;
511
467
  const stdin = readString(intent, 'stdin') ?? null;
512
468
  const timeoutSeconds = readPositiveInteger(intent, 'timeout_seconds') ?? null;
513
- const runnable = status === 'configured' &&
514
- lifecycle === 'oneshot' &&
515
- runPolicy === 'agent_allowed' &&
516
- stdin === 'closed' &&
517
- timeoutSeconds !== null &&
518
- (readStringArray(intent, 'argv') !== undefined || readString(intent, 'cmd') !== undefined);
469
+ const runnable = createRunPlan(projectRoot, contract, name).ok;
519
470
  return [
520
471
  {
521
472
  name,
@@ -753,7 +704,7 @@ function toDashboardUrl(host, port) {
753
704
  return `http://${formattedHost}:${port}/`;
754
705
  }
755
706
  export async function runDashboard(args, reporter, lang = 'en') {
756
- if (args.includes('--help') || args.includes('-h')) {
707
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
757
708
  reporter.stdout(getDashboardHelp(lang));
758
709
  return 0;
759
710
  }
@@ -4,6 +4,7 @@ import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.j
4
4
  import { DOC_REVIEW_LEDGER_RELATIVE_PATH, DOC_REVIEW_STATUSES, REVIEWER_KINDS, addDocReviewEntry, commentDocReviewEntry, isReviewerKind, listDocReviewEntries, markDocReviewEntry, } from '../lib/doc-review-ledger.js';
5
5
  import { ensureInside, readUtf8FileInsideWithoutSymlinks } from '../lib/filesystem.js';
6
6
  import { t } from '../lib/i18n.js';
7
+ import { getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
7
8
  import { resolveMustflowRoot } from '../lib/project-root.js';
8
9
  const LIST_FLAGS = new Set(['--json', '--all']);
9
10
  const LIST_VALUE_OPTIONS = new Set(['--status']);
@@ -50,35 +51,51 @@ export function getDocsHelp(lang = 'en') {
50
51
  }, lang);
51
52
  }
52
53
  function parseOptions(args, valueOptions, flags) {
54
+ const specs = createDocsOptionSpecs(valueOptions, flags);
55
+ const parsed = parseCliOptions(args, specs, { allowPositionals: true });
56
+ const values = parsedDocsValues(parsed, valueOptions);
57
+ const parsedFlags = parsedDocsFlags(parsed, flags);
58
+ if (parsed.error) {
59
+ return {
60
+ values,
61
+ flags: parsedFlags,
62
+ positionals: parsed.positionals,
63
+ error: {
64
+ option: parsed.error.option,
65
+ missingValue: parsed.error.kind === 'missing_value',
66
+ },
67
+ };
68
+ }
69
+ return { values, flags: parsedFlags, positionals: parsed.positionals };
70
+ }
71
+ function createDocsOptionSpecs(valueOptions, flags) {
72
+ const specs = new Map();
73
+ for (const option of valueOptions) {
74
+ specs.set(option, 'string');
75
+ }
76
+ for (const flag of flags) {
77
+ specs.set(flag, 'boolean');
78
+ }
79
+ return [...specs.entries()].map(([name, kind]) => ({ name, kind }));
80
+ }
81
+ function parsedDocsValues(parsed, valueOptions) {
53
82
  const values = new Map();
54
- const parsedFlags = new Set();
55
- const positionals = [];
56
- for (let index = 0; index < args.length; index += 1) {
57
- const arg = args[index];
58
- if (flags.has(arg)) {
59
- parsedFlags.add(arg);
60
- continue;
61
- }
62
- const equalsIndex = arg.indexOf('=');
63
- const optionName = equalsIndex > -1 ? arg.slice(0, equalsIndex) : arg;
64
- if (valueOptions.has(optionName)) {
65
- const inlineValue = equalsIndex > -1 ? arg.slice(equalsIndex + 1) : undefined;
66
- const nextValue = inlineValue ?? args[index + 1];
67
- if (!nextValue || (!inlineValue && nextValue.startsWith('-'))) {
68
- return { values, flags: parsedFlags, positionals, error: { option: optionName, missingValue: true } };
69
- }
70
- values.set(optionName, nextValue);
71
- if (inlineValue === undefined) {
72
- index += 1;
73
- }
74
- continue;
83
+ for (const option of valueOptions) {
84
+ const value = getParsedCliStringOption(parsed, option);
85
+ if (value !== null) {
86
+ values.set(option, value);
75
87
  }
76
- if (arg.startsWith('-')) {
77
- return { values, flags: parsedFlags, positionals, error: { option: arg } };
88
+ }
89
+ return values;
90
+ }
91
+ function parsedDocsFlags(parsed, flags) {
92
+ const parsedFlags = new Set();
93
+ for (const flag of flags) {
94
+ if (hasParsedCliOption(parsed, flag)) {
95
+ parsedFlags.add(flag);
78
96
  }
79
- positionals.push(arg);
80
97
  }
81
- return { values, flags: parsedFlags, positionals };
98
+ return parsedFlags;
82
99
  }
83
100
  function parseStatus(value) {
84
101
  if (value === undefined) {
@@ -352,7 +369,7 @@ function runReviewMark(args, status, reporter, lang) {
352
369
  }
353
370
  }
354
371
  export function runDocs(args, reporter, lang = 'en') {
355
- if (args.includes('--help') || args.includes('-h')) {
372
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
356
373
  reporter.stdout(getDocsHelp(lang));
357
374
  return 0;
358
375
  }
@@ -4,12 +4,17 @@ import { printUsageError, renderHelp } from '../lib/cli-output.js';
4
4
  import { getAgentContext, } from '../lib/agent-context.js';
5
5
  import { t } from '../lib/i18n.js';
6
6
  import { getLocalIndexDatabasePath } from '../lib/local-index.js';
7
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
7
8
  import { resolveMustflowRoot } from '../lib/project-root.js';
8
9
  import { checkMustflowProjectReport } from '../lib/validation.js';
9
10
  import { findCommandEnvInheritanceWarnings } from '../../core/command-contract-validation.js';
10
11
  import { readCommandContract } from '../../core/config-loading.js';
11
12
  import { summarizeSkillRouteAlignment } from '../../core/skill-route-alignment.js';
12
13
  const DOCTOR_SCHEMA_VERSION = '1';
14
+ const DOCTOR_OPTIONS = [
15
+ { name: '--json', kind: 'boolean' },
16
+ { name: '--strict', kind: 'boolean' },
17
+ ];
13
18
  export function getDoctorHelp(lang = 'en') {
14
19
  return renderHelp({
15
20
  usage: 'mf doctor [options]',
@@ -264,18 +269,17 @@ function renderDoctorOutput(output, lang) {
264
269
  return lines.join('\n');
265
270
  }
266
271
  export function runDoctor(args, reporter, lang = 'en') {
267
- if (args.includes('--help') || args.includes('-h')) {
272
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
268
273
  reporter.stdout(getDoctorHelp(lang));
269
274
  return 0;
270
275
  }
271
- const supported = new Set(['--json', '--strict']);
272
- const unsupported = args.filter((arg) => !supported.has(arg));
273
- if (unsupported.length > 0) {
274
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf doctor --help', getDoctorHelp(lang), lang);
276
+ const options = parseCliOptions(args, DOCTOR_OPTIONS);
277
+ if (options.error) {
278
+ printUsageError(reporter, formatCliOptionParseError(options.error, lang), 'mf doctor --help', getDoctorHelp(lang), lang);
275
279
  return 1;
276
280
  }
277
- const output = createDoctorOutput(args.includes('--strict'));
278
- if (args.includes('--json')) {
281
+ const output = createDoctorOutput(hasParsedCliOption(options, '--strict'));
282
+ if (hasParsedCliOption(options, '--json')) {
279
283
  reporter.stdout(JSON.stringify(output, null, 2));
280
284
  return output.ok ? 0 : 1;
281
285
  }