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
@@ -0,0 +1,93 @@
1
+ import { t } from './i18n.js';
2
+ function createSpecMap(specs) {
3
+ const map = new Map();
4
+ for (const spec of specs) {
5
+ map.set(spec.name, spec);
6
+ for (const alias of spec.aliases ?? []) {
7
+ map.set(alias, spec);
8
+ }
9
+ }
10
+ return map;
11
+ }
12
+ function splitOptionToken(token) {
13
+ const separatorIndex = token.indexOf('=');
14
+ if (separatorIndex === -1) {
15
+ return { name: token, inlineValue: null };
16
+ }
17
+ return {
18
+ name: token.slice(0, separatorIndex),
19
+ inlineValue: token.slice(separatorIndex + 1),
20
+ };
21
+ }
22
+ export function hasCliOptionToken(args, name, aliases = []) {
23
+ const tokens = new Set([name, ...aliases]);
24
+ return args.some((arg) => tokens.has(splitOptionToken(arg).name));
25
+ }
26
+ export function parseCliOptions(args, specs, config = {}) {
27
+ const specByToken = createSpecMap(specs);
28
+ const values = new Map();
29
+ const occurrences = [];
30
+ const positionals = [];
31
+ const allowPositionals = config.allowPositionals === true;
32
+ const allowUnknownOptions = config.allowUnknownOptions === true;
33
+ const allowEmptyStringValues = config.allowEmptyStringValues === true;
34
+ for (let index = 0; index < args.length; index += 1) {
35
+ const arg = args[index];
36
+ if (!arg) {
37
+ continue;
38
+ }
39
+ if (!arg.startsWith('-')) {
40
+ if (allowPositionals) {
41
+ positionals.push(arg);
42
+ continue;
43
+ }
44
+ return { values, occurrences, positionals, error: { kind: 'unknown_option', option: arg } };
45
+ }
46
+ const { name, inlineValue } = splitOptionToken(arg);
47
+ const spec = specByToken.get(name);
48
+ if (!spec) {
49
+ if (allowUnknownOptions && allowPositionals) {
50
+ positionals.push(arg);
51
+ continue;
52
+ }
53
+ return { values, occurrences, positionals, error: { kind: 'unknown_option', option: arg } };
54
+ }
55
+ if (spec.kind === 'boolean') {
56
+ if (inlineValue !== null) {
57
+ return { values, occurrences, positionals, error: { kind: 'unknown_option', option: arg } };
58
+ }
59
+ values.set(spec.name, true);
60
+ occurrences.push({ name: spec.name, value: true, token: arg });
61
+ continue;
62
+ }
63
+ if (inlineValue !== null) {
64
+ if (inlineValue.length === 0 && !allowEmptyStringValues) {
65
+ return { values, occurrences, positionals, error: { kind: 'missing_value', option: name } };
66
+ }
67
+ values.set(spec.name, inlineValue);
68
+ occurrences.push({ name: spec.name, value: inlineValue, token: arg });
69
+ continue;
70
+ }
71
+ const nextArg = args[index + 1];
72
+ if (!nextArg || nextArg.startsWith('-')) {
73
+ return { values, occurrences, positionals, error: { kind: 'missing_value', option: name } };
74
+ }
75
+ values.set(spec.name, nextArg);
76
+ occurrences.push({ name: spec.name, value: nextArg, token: arg });
77
+ index += 1;
78
+ }
79
+ return { values, occurrences, positionals, error: null };
80
+ }
81
+ export function hasParsedCliOption(parsed, name) {
82
+ return parsed.values.get(name) === true;
83
+ }
84
+ export function getParsedCliStringOption(parsed, name) {
85
+ const value = parsed.values.get(name);
86
+ return typeof value === 'string' ? value : null;
87
+ }
88
+ export function formatCliOptionParseError(error, lang) {
89
+ if (error.kind === 'missing_value') {
90
+ return t(lang, 'cli.error.missingValue', { option: error.option });
91
+ }
92
+ return t(lang, 'cli.error.unknownOption', { option: error.option });
93
+ }
@@ -215,7 +215,7 @@ function readMustflowConfig(projectRoot) {
215
215
  return {};
216
216
  }
217
217
  }
218
- function getRepoMapConfig(projectRoot) {
218
+ export function getRepoMapConfig(projectRoot) {
219
219
  const parsed = readMustflowConfig(projectRoot);
220
220
  const configuredPriorityPaths = [...getStringArray(parsed.read_order), ...getStringArray(parsed.optional_read_order)];
221
221
  const map = isRecord(parsed.map) ? parsed.map : {};
@@ -503,7 +503,7 @@ function collectNestedRepository(projectRoot, repositoryPath, anchorFiles) {
503
503
  editingPolicies,
504
504
  };
505
505
  }
506
- function discoverNestedRepositories(projectRoot, mapConfig, workspaceConfig) {
506
+ export function discoverNestedRepositories(projectRoot, mapConfig, workspaceConfig) {
507
507
  if (!mapConfig.includeNested || !workspaceConfig.enabled || workspaceConfig.roots.length === 0) {
508
508
  return [];
509
509
  }
@@ -2,10 +2,11 @@ import path from 'node:path';
2
2
  import { isMustflowBinName } from '../../core/command-classification.js';
3
3
  import { resolveSafeProjectCwd } from '../../core/command-cwd.js';
4
4
  import { resolveCommandEnv } from '../../core/command-env.js';
5
+ import { getCommandMaxOutputBytesLimitDetail, readEffectiveCommandCwd, readEffectiveCommandMaxOutputBytes, } from '../../core/command-run-constraints.js';
5
6
  import { evaluateCommandIntentEligibility, } from '../../core/command-intent-eligibility.js';
6
7
  import { inspectActiveRunLocks, } from '../../core/active-run-locks.js';
7
8
  import { isRecord, readPositiveInteger, readString, readStringArray, } from '../../core/config-loading.js';
8
- import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, COMMAND_OUTPUT_LIMIT_SCOPE, MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage, } from '../../core/command-output-limits.js';
9
+ import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, COMMAND_OUTPUT_LIMIT_SCOPE, } from '../../core/command-output-limits.js';
9
10
  import { normalizeSuccessExitCodes } from '../../core/success-exit-codes.js';
10
11
  import { normalizeSafeTestTargetPath, TEST_TARGET_PATH_ERROR } from '../../core/test-target-paths.js';
11
12
  import { evaluateCommandPreconditions, } from '../../core/command-preconditions.js';
@@ -79,31 +80,13 @@ function getRunPlanMode(commandArgv, intent) {
79
80
  }
80
81
  return intent.mode === 'shell' ? 'shell' : null;
81
82
  }
82
- function readEffectiveMaxOutputBytes(contract, intent) {
83
- return readPositiveInteger(intent, 'max_output_bytes') ??
84
- readPositiveInteger(contract.defaults, 'max_output_bytes') ??
85
- DEFAULT_COMMAND_MAX_OUTPUT_BYTES;
86
- }
87
83
  function readEffectiveKillAfterSeconds(contract, intent) {
88
84
  return readPositiveInteger(intent, 'kill_after_seconds') ??
89
85
  readPositiveInteger(contract.defaults, 'kill_after_seconds') ??
90
86
  5;
91
87
  }
92
- function getMaxOutputBytesLimitDetail(contract, intent) {
93
- const intentValue = readPositiveInteger(intent, 'max_output_bytes');
94
- if (intentValue !== undefined) {
95
- return intentValue > MAX_COMMAND_OUTPUT_BYTES ?
96
- commandMaxOutputBytesLimitMessage('[commands.intents.<intent>].max_output_bytes') :
97
- null;
98
- }
99
- const defaultValue = readPositiveInteger(contract.defaults, 'max_output_bytes');
100
- if (defaultValue !== undefined && defaultValue > MAX_COMMAND_OUTPUT_BYTES) {
101
- return commandMaxOutputBytesLimitMessage('[commands.defaults].max_output_bytes');
102
- }
103
- return null;
104
- }
105
88
  function readRunIntentMetadata(contract, intent) {
106
- const configuredCwd = readString(intent, 'cwd') ?? readString(contract.defaults, 'default_cwd') ?? '.';
89
+ const configuredCwd = readEffectiveCommandCwd(contract, intent);
107
90
  const commandArgv = readStringArray(intent, 'argv');
108
91
  const shellCommand = intent.mode === 'shell' ? readString(intent, 'cmd') : undefined;
109
92
  const env = resolveCommandEnv(contract, intent);
@@ -115,7 +98,7 @@ function readRunIntentMetadata(contract, intent) {
115
98
  configuredCwd,
116
99
  timeoutSeconds: readPositiveInteger(intent, 'timeout_seconds') ?? null,
117
100
  killAfterSeconds: readEffectiveKillAfterSeconds(contract, intent),
118
- maxOutputBytes: readEffectiveMaxOutputBytes(contract, intent),
101
+ maxOutputBytes: readEffectiveCommandMaxOutputBytes(contract, intent, DEFAULT_COMMAND_MAX_OUTPUT_BYTES),
119
102
  successExitCodes: getSuccessExitCodes(intent),
120
103
  commandArgv,
121
104
  shellCommand,
@@ -186,7 +169,7 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
186
169
  return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, eligibility.code, eligibility.detail, preconditions);
187
170
  }
188
171
  const metadata = readRunIntentMetadata(contract, rawIntent);
189
- const maxOutputBytesLimitDetail = getMaxOutputBytesLimitDetail(contract, rawIntent);
172
+ const maxOutputBytesLimitDetail = getCommandMaxOutputBytesLimitDetail(contract, rawIntent);
190
173
  if (maxOutputBytesLimitDetail) {
191
174
  return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, 'max_output_bytes_exceeds_limit', maxOutputBytesLimitDetail, preconditions);
192
175
  }
@@ -120,7 +120,7 @@ function escalationIntentsForCandidate(commandContract, candidate) {
120
120
  ...readIntentRelationList(commandContract, candidate.intent, 'escalate_to'),
121
121
  ]);
122
122
  }
123
- function expandCandidatesWithDeclaredFallbacks(commandContract, candidates) {
123
+ function expandCandidatesWithDeclaredFallbacks(commandContract, projectRoot, candidates) {
124
124
  const hasRunnableCandidate = candidates.some((candidate) => candidate.status === 'runnable' && candidate.intent.length > 0);
125
125
  if (hasRunnableCandidate) {
126
126
  return candidates;
@@ -133,7 +133,10 @@ function expandCandidatesWithDeclaredFallbacks(commandContract, candidates) {
133
133
  return [
134
134
  ...candidates,
135
135
  ...fallbackIntents.map((intent) => {
136
- const fallback = classifyVerificationCandidate(intent, commandContract.intents[intent]);
136
+ const fallback = classifyVerificationCandidate(intent, commandContract.intents[intent], {
137
+ commandContract,
138
+ projectRoot,
139
+ });
137
140
  return {
138
141
  ...fallback,
139
142
  detail: fallback.status === 'runnable' ? 'Declared fallback for unavailable verification intent.' : fallback.detail,
@@ -153,7 +156,7 @@ function requirementNeedsDeclaredEscalation(requirement) {
153
156
  ].join('\n').toLowerCase();
154
157
  return DECLARED_ESCALATION_SIGNALS.some((signal) => signalText.includes(signal.toLowerCase()));
155
158
  }
156
- function expandCandidatesWithDeclaredEscalations(commandContract, requirement, candidates) {
159
+ function expandCandidatesWithDeclaredEscalations(commandContract, projectRoot, requirement, candidates) {
157
160
  if (!requirementNeedsDeclaredEscalation(requirement)) {
158
161
  return candidates;
159
162
  }
@@ -165,7 +168,10 @@ function expandCandidatesWithDeclaredEscalations(commandContract, requirement, c
165
168
  return [
166
169
  ...candidates,
167
170
  ...escalationIntents.map((intent) => {
168
- const escalation = classifyVerificationCandidate(intent, commandContract.intents[intent]);
171
+ const escalation = classifyVerificationCandidate(intent, commandContract.intents[intent], {
172
+ commandContract,
173
+ projectRoot,
174
+ });
169
175
  return {
170
176
  ...escalation,
171
177
  detail: escalation.status === 'runnable'
@@ -295,7 +301,7 @@ export function createChangeVerificationReport(classificationReport, commandCont
295
301
  const requirements = classificationReport.summary.validationReasons.map((reason) => createVerificationRequirement(classificationReport, reason));
296
302
  const plans = requirements.map((requirement) => ({
297
303
  requirement,
298
- candidates: expandCandidatesWithDeclaredEscalations(commandContract, requirement, expandCandidatesWithDeclaredFallbacks(commandContract, createVerificationPlan(commandContract, requirement.reason).candidates)),
304
+ candidates: expandCandidatesWithDeclaredEscalations(commandContract, projectRoot, requirement, expandCandidatesWithDeclaredFallbacks(commandContract, projectRoot, createVerificationPlan(commandContract, requirement.reason, projectRoot).candidates)),
299
305
  }));
300
306
  const plansWithProjectTestSelection = plans.map((plan) => {
301
307
  const additionalCandidates = testSelectionPlan.candidates
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { isRecord, readString, readStringArray, } from './config-loading.js';
3
3
  import { resolveSafeProjectCwd } from './command-cwd.js';
4
+ import { readEffectiveCommandCwd } from './command-run-constraints.js';
4
5
  export const COMMAND_EFFECT_MODES = new Set(['read', 'write', 'append', 'replace', 'delete_recreate']);
5
6
  export const COMMAND_EFFECT_TYPES = new Set(['read', 'write']);
6
7
  export const COMMAND_EFFECT_CONCURRENCY = new Set(['shared', 'exclusive']);
@@ -13,9 +14,6 @@ function normalizeRelativePath(rawPath) {
13
14
  function pathLockKey(relativePath) {
14
15
  return `path:${normalizeRelativePath(relativePath)}`;
15
16
  }
16
- function readEffectiveCommandCwd(commandContract, intent) {
17
- return readString(intent, 'cwd') ?? readString(commandContract.defaults, 'default_cwd') ?? '.';
18
- }
19
17
  function validateEffectPath(projectRoot, commandContract, intent, rawPath) {
20
18
  const cwd = resolveSafeProjectCwd(projectRoot, readEffectiveCommandCwd(commandContract, intent));
21
19
  const resolved = path.resolve(cwd, rawPath);
@@ -1,5 +1,19 @@
1
1
  import { isRecord, readString } from './config-loading.js';
2
2
  import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
3
+ export const COMMAND_INTENT_INELIGIBILITY_CODES = [
4
+ 'intent_not_table',
5
+ 'status_not_configured',
6
+ 'lifecycle_not_oneshot',
7
+ 'run_policy_not_agent_allowed',
8
+ 'stdin_not_closed',
9
+ 'agent_shell_requires_allow',
10
+ 'missing_timeout',
11
+ 'missing_command_source',
12
+ 'unsafe_intent_name',
13
+ 'blocked_shell_background_pattern',
14
+ 'blocked_long_running_command_pattern',
15
+ ];
16
+ export const commandIntentIneligibilityCodeContractComplete = true;
3
17
  export function evaluateCommandIntentEligibility(intentName, rawIntent) {
4
18
  if (!commandIntentNameIsSafe(intentName)) {
5
19
  return {
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isRecord, readString, readStringArray, } from './config-loading.js';
4
4
  import { evaluateCommandIntentEligibility } from './command-intent-eligibility.js';
5
+ import { getCommandRunStaticBlocker } from './command-run-constraints.js';
5
6
  export const COMMAND_PRECONDITION_KINDS = new Set(['path_exists', 'artifact_freshness']);
6
7
  const IGNORED_WALK_DIRECTORIES = new Set([
7
8
  '.git',
@@ -47,20 +48,23 @@ function readPreconditionDeclarations(intent) {
47
48
  satisfyIntent: readString(precondition, 'satisfy_intent') ?? null,
48
49
  }));
49
50
  }
50
- function createSatisfyIntentSummary(contract, intentName) {
51
+ function createSatisfyIntentSummary(projectRoot, contract, intentName) {
51
52
  if (!intentName) {
52
53
  return null;
53
54
  }
54
55
  const rawIntent = contract.intents[intentName];
55
56
  const eligibility = evaluateCommandIntentEligibility(intentName, rawIntent);
57
+ const runBlocker = eligibility.ok && isRecord(rawIntent)
58
+ ? getCommandRunStaticBlocker(projectRoot, contract, rawIntent)
59
+ : null;
56
60
  return {
57
61
  intent: intentName,
58
62
  declared: isRecord(rawIntent),
59
- runnable: eligibility.ok,
63
+ runnable: eligibility.ok && !runBlocker,
60
64
  status: isRecord(rawIntent) ? readString(rawIntent, 'status') ?? null : null,
61
65
  lifecycle: isRecord(rawIntent) ? readString(rawIntent, 'lifecycle') ?? null : null,
62
66
  runPolicy: isRecord(rawIntent) ? readString(rawIntent, 'run_policy') ?? null : null,
63
- detail: eligibility.detail,
67
+ detail: eligibility.detail ?? runBlocker?.detail ?? null,
64
68
  };
65
69
  }
66
70
  function escapeRegExp(value) {
@@ -258,7 +262,7 @@ export function evaluateCommandPreconditions(projectRoot, contract, intentName)
258
262
  }
259
263
  const context = { projectFiles: null };
260
264
  return readPreconditionDeclarations(intent).map((declaration) => {
261
- const satisfyIntent = createSatisfyIntentSummary(contract, declaration.satisfyIntent);
265
+ const satisfyIntent = createSatisfyIntentSummary(projectRoot, contract, declaration.satisfyIntent);
262
266
  if (declaration.kind === 'path_exists') {
263
267
  return evaluatePathExists(projectRoot, declaration, satisfyIntent);
264
268
  }
@@ -0,0 +1,43 @@
1
+ import { resolveSafeProjectCwd } from './command-cwd.js';
2
+ import { MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage, } from './command-output-limits.js';
3
+ import { readPositiveInteger, readString, } from './config-loading.js';
4
+ export function readEffectiveCommandCwd(contract, intent) {
5
+ return readString(intent, 'cwd') ?? readString(contract.defaults, 'default_cwd') ?? '.';
6
+ }
7
+ export function readEffectiveCommandMaxOutputBytes(contract, intent, fallback) {
8
+ return readPositiveInteger(intent, 'max_output_bytes') ??
9
+ readPositiveInteger(contract.defaults, 'max_output_bytes') ??
10
+ fallback;
11
+ }
12
+ export function getCommandMaxOutputBytesLimitDetail(contract, intent) {
13
+ const intentValue = readPositiveInteger(intent, 'max_output_bytes');
14
+ if (intentValue !== undefined) {
15
+ return intentValue > MAX_COMMAND_OUTPUT_BYTES ?
16
+ commandMaxOutputBytesLimitMessage('[commands.intents.<intent>].max_output_bytes') :
17
+ null;
18
+ }
19
+ const defaultValue = readPositiveInteger(contract.defaults, 'max_output_bytes');
20
+ if (defaultValue !== undefined && defaultValue > MAX_COMMAND_OUTPUT_BYTES) {
21
+ return commandMaxOutputBytesLimitMessage('[commands.defaults].max_output_bytes');
22
+ }
23
+ return null;
24
+ }
25
+ export function getCommandRunStaticBlocker(projectRoot, contract, intent) {
26
+ const maxOutputBytesLimitDetail = getCommandMaxOutputBytesLimitDetail(contract, intent);
27
+ if (maxOutputBytesLimitDetail) {
28
+ return {
29
+ reason: 'max_output_bytes_exceeds_limit',
30
+ detail: maxOutputBytesLimitDetail,
31
+ };
32
+ }
33
+ try {
34
+ resolveSafeProjectCwd(projectRoot, readEffectiveCommandCwd(contract, intent));
35
+ }
36
+ catch (error) {
37
+ return {
38
+ reason: 'cwd_outside_project',
39
+ detail: error instanceof Error ? error.message : String(error),
40
+ };
41
+ }
42
+ return null;
43
+ }
@@ -79,6 +79,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
79
79
  documented: true,
80
80
  installedCommand: ['mf', 'api', 'locks', '--json'],
81
81
  },
82
+ {
83
+ id: 'api-serve-response',
84
+ schemaFile: 'api-serve-response.schema.json',
85
+ producer: 'mf api serve --stdio',
86
+ packaged: true,
87
+ documented: true,
88
+ },
82
89
  {
83
90
  id: 'run-receipt',
84
91
  schemaFile: 'run-receipt.schema.json',
@@ -109,6 +116,56 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
109
116
  documented: true,
110
117
  installedCommand: ['mf', 'contract-lint', '--json'],
111
118
  },
119
+ {
120
+ id: 'onboard-commands-report',
121
+ schemaFile: 'onboard-commands-report.schema.json',
122
+ producer: 'mf onboard commands --json',
123
+ packaged: true,
124
+ documented: true,
125
+ installedCommand: ['mf', 'onboard', 'commands', '--json'],
126
+ },
127
+ {
128
+ id: 'next-report',
129
+ schemaFile: 'next-report.schema.json',
130
+ producer: 'mf next --json',
131
+ packaged: true,
132
+ documented: true,
133
+ installedCommand: ['mf', 'next', '--json'],
134
+ expectedExitCodes: [0, 1],
135
+ },
136
+ {
137
+ id: 'evidence-report',
138
+ schemaFile: 'evidence-report.schema.json',
139
+ producer: 'mf evidence --changed --json',
140
+ packaged: true,
141
+ documented: true,
142
+ installedCommand: ['mf', 'evidence', '--changed', '--json'],
143
+ expectedExitCodes: [0, 1],
144
+ },
145
+ {
146
+ id: 'workspace-status',
147
+ schemaFile: 'workspace-status.schema.json',
148
+ producer: 'mf workspace status --json',
149
+ packaged: true,
150
+ documented: true,
151
+ installedCommand: ['mf', 'workspace', 'status', '--json'],
152
+ },
153
+ {
154
+ id: 'workspace-command-catalog',
155
+ schemaFile: 'workspace-command-catalog.schema.json',
156
+ producer: 'mf workspace command-catalog --json',
157
+ packaged: true,
158
+ documented: true,
159
+ installedCommand: ['mf', 'workspace', 'command-catalog', '--json'],
160
+ },
161
+ {
162
+ id: 'workspace-verification-plan',
163
+ schemaFile: 'workspace-verification-plan.schema.json',
164
+ producer: 'mf workspace verify --changed --plan-only --json',
165
+ packaged: true,
166
+ documented: true,
167
+ installedCommand: ['mf', 'workspace', 'verify', '--changed', '--plan-only', '--json'],
168
+ },
112
169
  {
113
170
  id: 'dashboard-export',
114
171
  schemaFile: 'dashboard-export.schema.json',
@@ -190,7 +190,10 @@ export function createProjectTestSelectionPlan(projectRoot, classificationReport
190
190
  fallbackIntent: rule.fallbackIntent,
191
191
  testTargets: rule.testTargets,
192
192
  });
193
- const primary = withDetail(classifyVerificationCandidate(rule.intent, commandContract.intents[rule.intent]), `Project-declared test selection rule "${rule.id}".`);
193
+ const primary = withDetail(classifyVerificationCandidate(rule.intent, commandContract.intents[rule.intent], {
194
+ commandContract,
195
+ projectRoot,
196
+ }), `Project-declared test selection rule "${rule.id}".`);
194
197
  const primaryTestTargets = appliedTestTargetsForCandidate(rule, commandContract, primary);
195
198
  const primaryCandidate = { reason, candidate: primary, testTargets: primaryTestTargets };
196
199
  candidates.push(primaryCandidate);
@@ -200,7 +203,10 @@ export function createProjectTestSelectionPlan(projectRoot, classificationReport
200
203
  continue;
201
204
  }
202
205
  selectedReportCandidates.push(toReportCandidate(rule, reason, 'primary', primary, primaryTestTargets));
203
- const fallback = withDetail(classifyVerificationCandidate(rule.fallbackIntent, commandContract.intents[rule.fallbackIntent]), `Fallback for project-declared test selection rule "${rule.id}".`);
206
+ const fallback = withDetail(classifyVerificationCandidate(rule.fallbackIntent, commandContract.intents[rule.fallbackIntent], {
207
+ commandContract,
208
+ projectRoot,
209
+ }), `Fallback for project-declared test selection rule "${rule.id}".`);
204
210
  const fallbackTestTargets = appliedTestTargetsForCandidate(rule, commandContract, fallback);
205
211
  const fallbackCandidate = { reason, candidate: fallback, testTargets: fallbackTestTargets };
206
212
  candidates.push(fallbackCandidate);
@@ -1,9 +1,23 @@
1
1
  import { isRecord, readStringArray, } from './config-loading.js';
2
- import { evaluateCommandIntentEligibility, } from './command-intent-eligibility.js';
2
+ import { getCommandRunStaticBlocker, } from './command-run-constraints.js';
3
+ import { COMMAND_INTENT_INELIGIBILITY_CODES, evaluateCommandIntentEligibility, } from './command-intent-eligibility.js';
4
+ export const VERIFICATION_SKIP_REASONS = [
5
+ 'no_matching_intents',
6
+ ...COMMAND_INTENT_INELIGIBILITY_CODES,
7
+ 'cwd_outside_project',
8
+ 'max_output_bytes_exceeds_limit',
9
+ ];
10
+ export const verificationSkipReasonContractComplete = true;
3
11
  function hasRequiredAfter(intent, reason) {
4
12
  return (readStringArray(intent, 'required_after') ?? []).includes(reason);
5
13
  }
6
- export function classifyVerificationCandidate(intentName, rawIntent) {
14
+ function getRunPlanBlocker(rawIntent, context) {
15
+ if (!context.projectRoot || !context.commandContract) {
16
+ return null;
17
+ }
18
+ return getCommandRunStaticBlocker(context.projectRoot, context.commandContract, rawIntent);
19
+ }
20
+ export function classifyVerificationCandidate(intentName, rawIntent, context = {}) {
7
21
  const eligibility = evaluateCommandIntentEligibility(intentName, rawIntent);
8
22
  if (!eligibility.ok) {
9
23
  return {
@@ -13,6 +27,17 @@ export function classifyVerificationCandidate(intentName, rawIntent) {
13
27
  detail: eligibility.detail,
14
28
  };
15
29
  }
30
+ if (isRecord(rawIntent)) {
31
+ const blocker = getRunPlanBlocker(rawIntent, context);
32
+ if (blocker) {
33
+ return {
34
+ intent: intentName,
35
+ status: 'skipped',
36
+ reason: blocker.reason,
37
+ detail: blocker.detail,
38
+ };
39
+ }
40
+ }
16
41
  return {
17
42
  intent: intentName,
18
43
  status: 'runnable',
@@ -20,10 +45,13 @@ export function classifyVerificationCandidate(intentName, rawIntent) {
20
45
  detail: null,
21
46
  };
22
47
  }
23
- export function createVerificationPlan(contract, reason) {
48
+ export function createVerificationPlan(contract, reason, projectRoot) {
24
49
  const candidates = Object.entries(contract.intents)
25
50
  .filter(([, intent]) => isRecord(intent) && hasRequiredAfter(intent, reason))
26
- .map(([intentName, intent]) => classifyVerificationCandidate(intentName, intent))
51
+ .map(([intentName, intent]) => classifyVerificationCandidate(intentName, intent, {
52
+ commandContract: contract,
53
+ projectRoot,
54
+ }))
27
55
  .sort((left, right) => left.intent.localeCompare(right.intent));
28
56
  return {
29
57
  reason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.23.0",
3
+ "version": "2.25.0",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
package/schemas/README.md CHANGED
@@ -15,6 +15,7 @@ Current schemas:
15
15
  - `diff-risk.schema.json`: output of `mf api diff-risk --changed --json`
16
16
  - `health.schema.json`: output of `mf api health --json`
17
17
  - `locks.schema.json`: output of `mf api locks --json`
18
+ - `api-serve-response.schema.json`: each newline-delimited response from `mf api serve --stdio`
18
19
  - `run-receipt.schema.json`: output of `mf run <intent> --json` and `.mustflow/state/runs/latest.json`,
19
20
  including bounded declared-write drift metadata, a safe latest-run performance summary, and optional
20
21
  structured phase timings and selection summaries
@@ -23,6 +24,21 @@ Current schemas:
23
24
  command execution or automatic dependency execution
24
25
  - `test-selection.schema.json`: parsed optional `.mustflow/config/test-selection.toml`
25
26
  - `contract-lint-report.schema.json`: output of `mf contract-lint --json`
27
+ - `onboard-commands-report.schema.json`: output of `mf onboard commands --json`, containing
28
+ review-only command-intent suggestions that do not grant command authority or write files
29
+ - `next-report.schema.json`: output of `mf next --json`, containing the next safe action,
30
+ changed-file verification gaps, and read-only command recommendations
31
+ - `evidence-report.schema.json`: output of `mf evidence --changed --json`, containing verification
32
+ requirements, latest bounded evidence, receipts, remaining risks, and gaps without running commands
33
+ - `workspace-status.schema.json`: output of `mf workspace status --json`, containing configured
34
+ workspace roots, discovered nested repositories, and per-root command-contract readiness without
35
+ granting command authority
36
+ - `workspace-command-catalog.schema.json`: output of `mf workspace command-catalog --json`,
37
+ containing per-root command intent availability, safe `mf run` entrypoints, and no raw command
38
+ strings
39
+ - `workspace-verification-plan.schema.json`: output of
40
+ `mf workspace verify --changed --plan-only --json`, containing per-root changed-file
41
+ verification plans without running commands or granting parent-to-child command authority
26
42
  - `dashboard-export.schema.json`: bounded static export written by `mf dashboard --export-json <path>`,
27
43
  including output policy, redaction and truncation metadata, the dashboard harness report, and
28
44
  the evidence-based completion verdict, evidence model, and conservative coverage matrix for the
@@ -0,0 +1,89 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://mustflow.github.io/schemas/api-serve-response.schema.json",
4
+ "title": "mustflow api serve response",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "command",
10
+ "transport",
11
+ "id",
12
+ "ok",
13
+ "policy"
14
+ ],
15
+ "properties": {
16
+ "schema_version": { "const": "1" },
17
+ "command": { "const": "api serve" },
18
+ "transport": { "const": "stdio" },
19
+ "id": { "type": ["string", "number", "null"] },
20
+ "ok": { "type": "boolean" },
21
+ "policy": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": [
25
+ "mode",
26
+ "executes_commands",
27
+ "direct_commands_allowed",
28
+ "raw_output_included",
29
+ "hidden_reasoning_included"
30
+ ],
31
+ "properties": {
32
+ "mode": { "const": "read_only" },
33
+ "executes_commands": { "const": false },
34
+ "direct_commands_allowed": { "const": false },
35
+ "raw_output_included": { "const": false },
36
+ "hidden_reasoning_included": { "const": false }
37
+ }
38
+ },
39
+ "result": {
40
+ "anyOf": [
41
+ { "$ref": "workspace-summary.schema.json" },
42
+ { "$ref": "command-catalog.schema.json" },
43
+ { "$ref": "verification-plan.schema.json" },
44
+ { "$ref": "latest-evidence.schema.json" },
45
+ { "$ref": "diff-risk.schema.json" },
46
+ { "$ref": "health.schema.json" },
47
+ { "$ref": "locks.schema.json" }
48
+ ]
49
+ },
50
+ "error": {
51
+ "type": "object",
52
+ "additionalProperties": false,
53
+ "required": ["code", "message"],
54
+ "properties": {
55
+ "code": {
56
+ "enum": [
57
+ "invalid_json",
58
+ "invalid_request",
59
+ "unknown_action",
60
+ "action_requires_changed",
61
+ "action_does_not_accept_changed",
62
+ "report_unavailable"
63
+ ]
64
+ },
65
+ "message": { "type": "string" }
66
+ }
67
+ }
68
+ },
69
+ "oneOf": [
70
+ {
71
+ "properties": {
72
+ "ok": { "const": true }
73
+ },
74
+ "required": ["result"],
75
+ "not": {
76
+ "required": ["error"]
77
+ }
78
+ },
79
+ {
80
+ "properties": {
81
+ "ok": { "const": false }
82
+ },
83
+ "required": ["error"],
84
+ "not": {
85
+ "required": ["result"]
86
+ }
87
+ }
88
+ ]
89
+ }
@@ -255,8 +255,11 @@
255
255
  "missing_timeout",
256
256
  "missing_command_source",
257
257
  "unsafe_intent_name",
258
+ "agent_shell_requires_allow",
258
259
  "blocked_shell_background_pattern",
259
- "blocked_long_running_command_pattern"
260
+ "blocked_long_running_command_pattern",
261
+ "cwd_outside_project",
262
+ "max_output_bytes_exceeds_limit"
260
263
  ]
261
264
  },
262
265
  "verificationCandidate": {
@@ -139,6 +139,7 @@
139
139
  "missing_timeout",
140
140
  "missing_command_source",
141
141
  "unsafe_intent_name",
142
+ "agent_shell_requires_allow",
142
143
  "blocked_shell_background_pattern",
143
144
  "blocked_long_running_command_pattern"
144
145
  ]