mustflow 1.31.0 → 2.16.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 (66) hide show
  1. package/README.md +23 -9
  2. package/dist/cli/commands/classify.js +61 -6
  3. package/dist/cli/commands/contract-lint.js +13 -4
  4. package/dist/cli/commands/dashboard.js +77 -2
  5. package/dist/cli/commands/explain-verify.js +11 -1
  6. package/dist/cli/commands/index.js +14 -0
  7. package/dist/cli/commands/run.js +4 -1
  8. package/dist/cli/commands/verify.js +986 -43
  9. package/dist/cli/i18n/en.js +61 -10
  10. package/dist/cli/i18n/es.js +61 -10
  11. package/dist/cli/i18n/fr.js +61 -10
  12. package/dist/cli/i18n/hi.js +61 -10
  13. package/dist/cli/i18n/ko.js +61 -10
  14. package/dist/cli/i18n/zh.js +61 -10
  15. package/dist/cli/lib/dashboard-export.js +62 -12
  16. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  17. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  18. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  19. package/dist/cli/lib/dashboard-html/template.js +134 -0
  20. package/dist/cli/lib/dashboard-html/types.js +1 -0
  21. package/dist/cli/lib/dashboard-html.js +1 -1907
  22. package/dist/cli/lib/dashboard-locale.js +37 -0
  23. package/dist/cli/lib/local-index/constants.js +48 -0
  24. package/dist/cli/lib/local-index/index.js +2951 -0
  25. package/dist/cli/lib/local-index/sql.js +15 -0
  26. package/dist/cli/lib/local-index/types.js +1 -0
  27. package/dist/cli/lib/local-index.js +1 -1911
  28. package/dist/cli/lib/run-plan.js +76 -1
  29. package/dist/cli/lib/templates.js +18 -1
  30. package/dist/cli/lib/validation/command-intents.js +11 -0
  31. package/dist/cli/lib/validation/constants.js +238 -0
  32. package/dist/cli/lib/validation/index.js +1384 -0
  33. package/dist/cli/lib/validation/primitives.js +198 -0
  34. package/dist/cli/lib/validation/test-selection.js +95 -0
  35. package/dist/cli/lib/validation/types.js +1 -0
  36. package/dist/cli/lib/validation.js +1 -1770
  37. package/dist/core/check-issues.js +6 -0
  38. package/dist/core/completion-verdict.js +341 -0
  39. package/dist/core/contract-lint.js +221 -6
  40. package/dist/core/external-evidence.js +9 -0
  41. package/dist/core/public-json-contracts.js +21 -0
  42. package/dist/core/repeated-failure.js +179 -0
  43. package/dist/core/repro-evidence.js +134 -0
  44. package/dist/core/scope-risk.js +64 -0
  45. package/dist/core/skill-route-alignment.js +20 -0
  46. package/dist/core/source-anchor-status.js +4 -1
  47. package/dist/core/test-selection.js +3 -0
  48. package/dist/core/validation-ratchet.js +196 -0
  49. package/dist/core/verification-evidence.js +249 -0
  50. package/examples/README.md +12 -4
  51. package/package.json +3 -3
  52. package/schemas/README.md +13 -3
  53. package/schemas/change-verification-report.schema.json +16 -2
  54. package/schemas/commands.schema.json +4 -0
  55. package/schemas/contract-lint-report.schema.json +29 -0
  56. package/schemas/dashboard-export.schema.json +310 -0
  57. package/schemas/explain-report.schema.json +173 -1
  58. package/schemas/latest-run-pointer.schema.json +601 -0
  59. package/schemas/run-receipt.schema.json +4 -0
  60. package/schemas/test-selection.schema.json +81 -0
  61. package/schemas/verify-report.schema.json +578 -1
  62. package/schemas/verify-run-manifest.schema.json +627 -0
  63. package/templates/default/i18n.toml +1 -1
  64. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  65. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  66. package/templates/default/manifest.toml +29 -2
@@ -0,0 +1,1384 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { isRecord } from '../command-contract.js';
4
+ import { validateCommandContractConfig, validateCommandContractStrictDefaults, } from '../../../core/command-contract-validation.js';
5
+ import { ALLOWED_RETENTION_ON_LIMIT, ALLOWED_RETENTION_STORES, DEFAULT_RETENTION_LIMITS, readNestedRetentionTable, readRetentionTable, resolveRetentionLimits, } from '../../../core/retention-policy.js';
6
+ import { formatManagedMarkdownLabel, getManagedMarkdownExpectation, } from '../../../core/authority-resolution.js';
7
+ import { SKILL_INDEX_ROUTE_COLUMN_COUNT, SKILL_INDEX_ROUTE_COLUMNS, SKILL_INDEX_SKILL_PATH_COLUMN_INDEX, findSkillRouteConflictWarnings, findSkillIndexRoutePathColumn, parseSkillIndexRoutes, readBacktickValues, } from '../../../core/skill-route-alignment.js';
8
+ import { validateTemplateVersionSync } from '../../../core/release-version-validation.js';
9
+ import { validateSourceAnchorsInProject } from '../../../core/source-anchor-validation.js';
10
+ import { listFilesRecursive, toPosixPath } from '../filesystem.js';
11
+ import { readGitChangedFiles } from '../git-changes.js';
12
+ import { inspectManifestLock } from '../manifest-lock.js';
13
+ import { generateRepoMap } from '../repo-map.js';
14
+ import { readTomlFile } from '../toml.js';
15
+ import { getContractModelDefinitions, validateCandidateContractModelConfig, } from '../../../core/contract-models.js';
16
+ import { VERSIONING_CONFIG_PATH, detectVersionSourcePaths, readDeclaredVersionSources, releaseVersioningIsEnabled, } from '../../../core/version-sources.js';
17
+ import { ALLOWED_APPROVAL_ACTIONS, ALLOWED_APPROVAL_GATES, ALLOWED_BUDGET_LIMIT_ACTIONS, ALLOWED_CAPABILITY_STATES, ALLOWED_COMMIT_MESSAGE_STYLES, ALLOWED_COMPACTION_CATEGORIES, ALLOWED_COMPACTION_LONG_LIMIT_ACTIONS, ALLOWED_COMPACTION_RAW_LIMIT_ACTIONS, ALLOWED_COMPACTION_STATE_STORES, ALLOWED_COMPACTION_STRATEGIES, ALLOWED_CONTEXT_AUTHORITIES, ALLOWED_CONTEXT_DOCUMENT_AUTHORITIES, ALLOWED_CONTEXT_READ_POLICIES, ALLOWED_HANDOFF_MODES, ALLOWED_HARNESS_FRESH_CONTEXT_MODES, ALLOWED_HARNESS_MODES, ALLOWED_HARNESS_PHASES, ALLOWED_ISOLATION_PREFERENCES, ALLOWED_MAP_MODES, ALLOWED_MAP_PRIVACY_LEVELS, ALLOWED_PROJECT_PROFILES, ALLOWED_PROMPT_CACHE_STABLE_PREFIX_POLICIES, ALLOWED_PROMPT_CACHE_STRATEGIES, ALLOWED_PROMPT_CACHE_TASK_READ_POLICIES, ALLOWED_REFRESH_CHECKPOINTS, ALLOWED_REFRESH_METHODS, ALLOWED_REFRESH_MODES, ALLOWED_REFRESH_STATE_STORES, ALLOWED_SKILL_RESOURCE_TYPES, ALLOWED_SKILL_ROUTE_CATEGORIES, ALLOWED_SKILL_ROUTE_PROFILES, ALLOWED_SKILL_ROUTE_TYPES, ALLOWED_STALE_TEST_ACTIONS, ALLOWED_TEST_AUTHORING_POLICIES, ALLOWED_TEST_DELETION_REASONS, ALLOWED_TESTING_POLICIES, ALLOWED_TRANSLATION_POLICIES, ALLOWED_VERIFICATION_SELECTION_STRATEGIES, ALLOWED_VERSION_SOURCE_AUTHORITIES, ALLOWED_VERSION_SOURCE_KINDS, CAPABILITY_BOOLEAN_FIELDS, CAPABILITY_STATE_FIELDS, CONTEXT_AUTHORITY_DRIFT_PATTERNS, DESIGN_TOKEN_DEFINITION_PATTERNS, FORBIDDEN_RELEASE_VERSIONING_CONTRACT_FIELDS, FORBIDDEN_TEST_DELETION_REASONS, FORBIDDEN_VERIFICATION_SELECTION_AUTHORITY_FIELDS, LOCAL_ABSOLUTE_PATH_PATTERNS, LOCAL_TASK_STATE_ROOTS, RAW_COMMAND_FENCE_PATTERN, RELEASE_VERSIONING_BOOLEAN_FIELDS, REPO_MAP_DOC_ID, REPO_MAP_GENERATOR, REPO_MAP_LIFECYCLE, REPO_MAP_PRIVACY_MODE, REPO_MAP_RELATIVE_ROOT, REPO_MAP_REMOTE_OR_BRANCH_PATTERNS, REPO_MAP_SOURCE_FINGERPRINT_PATTERN, REPO_MAP_SOURCE_POLICY, REQUIRED_AGENT_LOOP_PHASES, REQUIRED_SKILL_SCRIPT_RUN_POLICY, REQUIRED_SKILL_SECTION_IDS, ROUTER_INDEX_FILES, ROUTER_INDEX_PROCEDURE_SECTION_PATTERN, SECRET_LIKE_CONTEXT_PATTERNS, SKILL_COMMAND_PERMISSION_CLAIM_PATTERNS, SKILL_INDEX_PATH, SKILL_PACK_ID_PATTERN, SKILL_RESOURCE_MANIFEST, SKILL_RESOURCE_ROOTS, SKILL_RESOURCE_TYPE_BY_ROOT, SKILL_ROUTE_CATEGORY_LABELS, SKILL_ROUTES_METADATA_PATH, SKILL_SECTION_MARKER_PATTERN, SUPPORTED_SKILL_SCHEMA_VERSION, TEST_AUTHORING_BOOLEAN_FIELDS, VERIFICATION_SELECTION_BOOLEAN_FIELDS, VOLATILE_REPO_MAP_PATTERNS } from './constants.js';
18
+ import { hasOwn, isPositiveInteger, isSafeRelativePath, pushStrictIssue, pushStrictWarning, validateAllowedStringField, validateBooleanField, validateExactStringArrayField, validateNestedTable, validatePathArrayField, validatePathField, validatePositiveIntegerField, validateRequiredFiles, validateRequiredPathField, validateRequiredStringField, validateStringArrayField, validateStringArrayMembers, validateStringField, validateTable, validateToml, validateWorkspaceRoots } from './primitives.js';
19
+ import { isConfiguredCommandIntent, isDeclaredCommandIntent } from './command-intents.js';
20
+ import { validateStrictTestSelectionConfig } from './test-selection.js';
21
+ export { describeCheckIssues, getCheckIssueId, } from '../../../core/check-issues.js';
22
+ function validateMustflowConfig(mustflowToml, issues) {
23
+ if (!mustflowToml) {
24
+ return;
25
+ }
26
+ const map = validateTable(mustflowToml, 'map', issues);
27
+ if (map) {
28
+ validatePathField(map, 'output', '[map].output', issues);
29
+ validateAllowedStringField(map, 'mode', '[map].mode', ALLOWED_MAP_MODES, issues);
30
+ validateAllowedStringField(map, 'privacy', '[map].privacy', ALLOWED_MAP_PRIVACY_LEVELS, issues);
31
+ validateBooleanField(map, 'include_nested', '[map].include_nested', issues);
32
+ validatePathArrayField(map, 'anchor_files', '[map].anchor_files', issues);
33
+ }
34
+ const context = validateTable(mustflowToml, 'context', issues);
35
+ if (context) {
36
+ validateBooleanField(context, 'enabled', '[context].enabled', issues);
37
+ validatePathField(context, 'root', '[context].root', issues);
38
+ validatePathField(context, 'index', '[context].index', issues);
39
+ validatePathArrayField(context, 'default_files', '[context].default_files', issues);
40
+ validateAllowedStringField(context, 'read_policy', '[context].read_policy', ALLOWED_CONTEXT_READ_POLICIES, issues);
41
+ validateAllowedStringField(context, 'authority', '[context].authority', ALLOWED_CONTEXT_AUTHORITIES, issues);
42
+ validatePathArrayField(context, 'external_anchors', '[context].external_anchors', issues);
43
+ }
44
+ const promptCache = validateTable(mustflowToml, 'prompt_cache', issues);
45
+ if (promptCache) {
46
+ validateBooleanField(promptCache, 'enabled', '[prompt_cache].enabled', issues);
47
+ validateAllowedStringField(promptCache, 'strategy', '[prompt_cache].strategy', ALLOWED_PROMPT_CACHE_STRATEGIES, issues);
48
+ validateAllowedStringField(promptCache, 'stable_prefix_policy', '[prompt_cache].stable_prefix_policy', ALLOWED_PROMPT_CACHE_STABLE_PREFIX_POLICIES, issues);
49
+ validateBooleanField(promptCache, 'prefer_references_when_unchanged', '[prompt_cache].prefer_references_when_unchanged', issues);
50
+ validateBooleanField(promptCache, 'exclude_volatile_state_from_prefix', '[prompt_cache].exclude_volatile_state_from_prefix', issues);
51
+ validateBooleanField(promptCache, 'include_content_hashes', '[prompt_cache].include_content_hashes', issues);
52
+ validatePositiveIntegerField(promptCache, 'max_stable_prefix_kb', '[prompt_cache].max_stable_prefix_kb', issues);
53
+ validatePositiveIntegerField(promptCache, 'max_task_context_kb', '[prompt_cache].max_task_context_kb', issues);
54
+ validatePositiveIntegerField(promptCache, 'max_volatile_suffix_kb', '[prompt_cache].max_volatile_suffix_kb', issues);
55
+ const layers = validateNestedTable(promptCache, 'layers', '[prompt_cache.layers]', issues);
56
+ if (layers) {
57
+ const stable = validateNestedTable(layers, 'stable', '[prompt_cache.layers.stable]', issues);
58
+ if (stable) {
59
+ validatePathArrayField(stable, 'read', '[prompt_cache.layers.stable].read', issues);
60
+ }
61
+ const task = validateNestedTable(layers, 'task', '[prompt_cache.layers.task]', issues);
62
+ if (task) {
63
+ validateAllowedStringField(task, 'read_policy', '[prompt_cache.layers.task].read_policy', ALLOWED_PROMPT_CACHE_TASK_READ_POLICIES, issues);
64
+ validateStringArrayField(task, 'sources', '[prompt_cache.layers.task].sources', issues);
65
+ }
66
+ const volatile = validateNestedTable(layers, 'volatile', '[prompt_cache.layers.volatile]', issues);
67
+ if (volatile) {
68
+ validateStringArrayField(volatile, 'sources', '[prompt_cache.layers.volatile].sources', issues);
69
+ validateBooleanField(volatile, 'never_place_before_stable_prefix', '[prompt_cache.layers.volatile].never_place_before_stable_prefix', issues);
70
+ }
71
+ }
72
+ }
73
+ const workspace = validateTable(mustflowToml, 'workspace', issues);
74
+ if (workspace) {
75
+ validateBooleanField(workspace, 'enabled', '[workspace].enabled', issues);
76
+ const roots = validateWorkspaceRoots(workspace, issues);
77
+ validatePositiveIntegerField(workspace, 'max_depth', '[workspace].max_depth', issues);
78
+ validatePositiveIntegerField(workspace, 'max_repositories', '[workspace].max_repositories', issues);
79
+ validateBooleanField(workspace, 'follow_symlinks', '[workspace].follow_symlinks', issues);
80
+ validateBooleanField(workspace, 'stop_at_repository_root', '[workspace].stop_at_repository_root', issues);
81
+ if (workspace.enabled === true && roots?.length === 0) {
82
+ issues.push({ message: '[workspace].enabled requires at least one [workspace].roots entry' });
83
+ }
84
+ }
85
+ const capabilities = validateTable(mustflowToml, 'capabilities', issues);
86
+ if (capabilities) {
87
+ for (const field of CAPABILITY_BOOLEAN_FIELDS) {
88
+ validateBooleanField(capabilities, field, `[capabilities].${field}`, issues);
89
+ }
90
+ for (const field of CAPABILITY_STATE_FIELDS) {
91
+ validateAllowedStringField(capabilities, field, `[capabilities].${field}`, ALLOWED_CAPABILITY_STATES, issues);
92
+ }
93
+ validateStringArrayField(capabilities, 'adapters', '[capabilities].adapters', issues);
94
+ }
95
+ const agentLoop = validateTable(mustflowToml, 'agent_loop', issues);
96
+ if (agentLoop) {
97
+ validateExactStringArrayField(agentLoop, 'phases', '[agent_loop].phases', REQUIRED_AGENT_LOOP_PHASES, issues);
98
+ }
99
+ const harness = validateTable(mustflowToml, 'harness', issues);
100
+ if (harness) {
101
+ validateAllowedStringField(harness, 'mode', '[harness].mode', ALLOWED_HARNESS_MODES, issues);
102
+ validateBooleanField(harness, 'fresh_context_preferred', '[harness].fresh_context_preferred', issues);
103
+ validateAllowedStringField(harness, 'fresh_context_mode', '[harness].fresh_context_mode', ALLOWED_HARNESS_FRESH_CONTEXT_MODES, issues);
104
+ const phases = validateNestedTable(harness, 'phases', '[harness.phases]', issues);
105
+ if (phases) {
106
+ validateStringArrayMembers(phases, 'enabled', '[harness.phases].enabled', ALLOWED_HARNESS_PHASES, 'phase', issues);
107
+ }
108
+ }
109
+ const refresh = validateTable(mustflowToml, 'refresh', issues);
110
+ if (refresh) {
111
+ validateBooleanField(refresh, 'enabled', '[refresh].enabled', issues);
112
+ validateAllowedStringField(refresh, 'mode', '[refresh].mode', ALLOWED_REFRESH_MODES, issues);
113
+ validateAllowedStringField(refresh, 'default_method', '[refresh].default_method', ALLOWED_REFRESH_METHODS, issues);
114
+ validateBooleanField(refresh, 'reread_when_hash_changed', '[refresh].reread_when_hash_changed', issues);
115
+ validateBooleanField(refresh, 'reuse_cached_prefix_when_unchanged', '[refresh].reuse_cached_prefix_when_unchanged', issues);
116
+ validateStringArrayMembers(refresh, 'required_at', '[refresh].required_at', ALLOWED_REFRESH_CHECKPOINTS, 'checkpoint', issues);
117
+ validatePositiveIntegerField(refresh, 'turn_threshold', '[refresh].turn_threshold', issues);
118
+ validatePositiveIntegerField(refresh, 'tool_call_threshold', '[refresh].tool_call_threshold', issues);
119
+ validatePositiveIntegerField(refresh, 'output_bytes_threshold', '[refresh].output_bytes_threshold', issues);
120
+ validateAllowedStringField(refresh, 'state_store', '[refresh].state_store', ALLOWED_REFRESH_STATE_STORES, issues);
121
+ const levels = validateNestedTable(refresh, 'levels', '[refresh.levels]', issues);
122
+ if (levels) {
123
+ for (const [levelName, level] of Object.entries(levels)) {
124
+ if (!isRecord(level)) {
125
+ issues.push({ message: `[refresh.levels.${levelName}] must be a TOML table` });
126
+ continue;
127
+ }
128
+ validateAllowedStringField(level, 'method', `[refresh.levels.${levelName}].method`, ALLOWED_REFRESH_METHODS, issues);
129
+ validatePathArrayField(level, 'read', `[refresh.levels.${levelName}].read`, issues);
130
+ }
131
+ }
132
+ }
133
+ const compaction = validateTable(mustflowToml, 'compaction', issues);
134
+ if (compaction) {
135
+ validateBooleanField(compaction, 'enabled', '[compaction].enabled', issues);
136
+ validateAllowedStringField(compaction, 'strategy', '[compaction].strategy', ALLOWED_COMPACTION_STRATEGIES, issues);
137
+ validateAllowedStringField(compaction, 'state_store', '[compaction].state_store', ALLOWED_COMPACTION_STATE_STORES, issues);
138
+ const recent = validateNestedTable(compaction, 'recent', '[compaction.recent]', issues);
139
+ if (recent) {
140
+ validatePositiveIntegerField(recent, 'keep_turns', '[compaction.recent].keep_turns', issues);
141
+ validatePositiveIntegerField(recent, 'max_total_bytes', '[compaction.recent].max_total_bytes', issues);
142
+ validateBooleanField(recent, 'store_raw', '[compaction.recent].store_raw', issues);
143
+ }
144
+ const mid = validateNestedTable(compaction, 'mid', '[compaction.mid]', issues);
145
+ if (mid) {
146
+ validatePositiveIntegerField(mid, 'trigger_turns', '[compaction.mid].trigger_turns', issues);
147
+ validatePositiveIntegerField(mid, 'target_items', '[compaction.mid].target_items', issues);
148
+ validatePositiveIntegerField(mid, 'target_max_words_per_item', '[compaction.mid].target_max_words_per_item', issues);
149
+ validateStringArrayMembers(mid, 'include_categories', '[compaction.mid].include_categories', ALLOWED_COMPACTION_CATEGORIES, 'category', issues);
150
+ }
151
+ const long = validateNestedTable(compaction, 'long', '[compaction.long]', issues);
152
+ if (long) {
153
+ validatePositiveIntegerField(long, 'promote_after_mid_items', '[compaction.long].promote_after_mid_items', issues);
154
+ validatePositiveIntegerField(long, 'target_items', '[compaction.long].target_items', issues);
155
+ validatePositiveIntegerField(long, 'max_items', '[compaction.long].max_items', issues);
156
+ validateAllowedStringField(long, 'on_limit', '[compaction.long].on_limit', ALLOWED_COMPACTION_LONG_LIMIT_ACTIONS, issues);
157
+ }
158
+ const rawRetention = validateNestedTable(compaction, 'raw_retention', '[compaction.raw_retention]', issues);
159
+ if (rawRetention) {
160
+ validatePositiveIntegerField(rawRetention, 'max_age_days', '[compaction.raw_retention].max_age_days', issues);
161
+ validatePositiveIntegerField(rawRetention, 'max_total_mb', '[compaction.raw_retention].max_total_mb', issues);
162
+ validateAllowedStringField(rawRetention, 'on_limit', '[compaction.raw_retention].on_limit', ALLOWED_COMPACTION_RAW_LIMIT_ACTIONS, issues);
163
+ }
164
+ const rules = validateNestedTable(compaction, 'rules', '[compaction.rules]', issues);
165
+ if (rules) {
166
+ validateBooleanField(rules, 'require_source_refs', '[compaction.rules].require_source_refs', issues);
167
+ validateBooleanField(rules, 'summaries_are_derived', '[compaction.rules].summaries_are_derived', issues);
168
+ validateBooleanField(rules, 'current_files_override_summaries', '[compaction.rules].current_files_override_summaries', issues);
169
+ validateBooleanField(rules, 'never_store_secrets', '[compaction.rules].never_store_secrets', issues);
170
+ validateBooleanField(rules, 'scrub_absolute_user_paths', '[compaction.rules].scrub_absolute_user_paths', issues);
171
+ validateBooleanField(rules, 'do_not_store_hidden_chain_of_thought', '[compaction.rules].do_not_store_hidden_chain_of_thought', issues);
172
+ }
173
+ }
174
+ const verification = validateTable(mustflowToml, 'verification', issues);
175
+ if (verification) {
176
+ validatePathField(verification, 'command_source', '[verification].command_source', issues);
177
+ validateBooleanField(verification, 'require_configured_intents', '[verification].require_configured_intents', issues);
178
+ validateBooleanField(verification, 'allow_inferred_commands', '[verification].allow_inferred_commands', issues);
179
+ validateBooleanField(verification, 'require_command_lifecycle', '[verification].require_command_lifecycle', issues);
180
+ validateBooleanField(verification, 'require_timeout_for_oneshot', '[verification].require_timeout_for_oneshot', issues);
181
+ }
182
+ const testing = validateTable(mustflowToml, 'testing', issues);
183
+ if (testing) {
184
+ validateAllowedStringField(testing, 'policy', '[testing].policy', ALLOWED_TESTING_POLICIES, issues);
185
+ validateBooleanField(testing, 'prefer_update_existing_tests', '[testing].prefer_update_existing_tests', issues);
186
+ validateBooleanField(testing, 'require_existing_test_search', '[testing].require_existing_test_search', issues);
187
+ validateBooleanField(testing, 'require_test_change_report', '[testing].require_test_change_report', issues);
188
+ validateBooleanField(testing, 'forbid_validation_weakening', '[testing].forbid_validation_weakening', issues);
189
+ validateStringArrayMembers(testing, 'allow_test_deletion_when', '[testing].allow_test_deletion_when', ALLOWED_TEST_DELETION_REASONS, 'reason', issues);
190
+ validateStringArrayMembers(testing, 'forbid_test_deletion_when', '[testing].forbid_test_deletion_when', FORBIDDEN_TEST_DELETION_REASONS, 'reason', issues);
191
+ validateAllowedStringField(testing, 'stale_test_action', '[testing].stale_test_action', ALLOWED_STALE_TEST_ACTIONS, issues);
192
+ }
193
+ const handoff = validateTable(mustflowToml, 'handoff', issues);
194
+ if (handoff) {
195
+ validateBooleanField(handoff, 'enabled', '[handoff].enabled', issues);
196
+ validateAllowedStringField(handoff, 'mode', '[handoff].mode', ALLOWED_HANDOFF_MODES, issues);
197
+ }
198
+ const budget = validateTable(mustflowToml, 'budget', issues);
199
+ if (budget) {
200
+ validateBooleanField(budget, 'enabled', '[budget].enabled', issues);
201
+ validatePositiveIntegerField(budget, 'max_iterations', '[budget].max_iterations', issues);
202
+ validatePositiveIntegerField(budget, 'max_wall_clock_minutes', '[budget].max_wall_clock_minutes', issues);
203
+ validatePositiveIntegerField(budget, 'max_command_runs', '[budget].max_command_runs', issues);
204
+ validatePositiveIntegerField(budget, 'max_total_output_mb', '[budget].max_total_output_mb', issues);
205
+ validatePositiveIntegerField(budget, 'max_failures_per_intent', '[budget].max_failures_per_intent', issues);
206
+ validateAllowedStringField(budget, 'on_limit', '[budget].on_limit', ALLOWED_BUDGET_LIMIT_ACTIONS, issues);
207
+ }
208
+ const approval = validateTable(mustflowToml, 'approval', issues);
209
+ if (approval) {
210
+ validateStringArrayMembers(approval, 'required_for', '[approval].required_for', ALLOWED_APPROVAL_GATES, 'approval gate', issues);
211
+ validateAllowedStringField(approval, 'on_required', '[approval].on_required', ALLOWED_APPROVAL_ACTIONS, issues);
212
+ }
213
+ const isolation = validateTable(mustflowToml, 'isolation', issues);
214
+ if (isolation) {
215
+ validateAllowedStringField(isolation, 'preferred', '[isolation].preferred', ALLOWED_ISOLATION_PREFERENCES, issues);
216
+ validateBooleanField(isolation, 'required_for_long_running', '[isolation].required_for_long_running', issues);
217
+ validateBooleanField(isolation, 'allow_dirty_main_worktree', '[isolation].allow_dirty_main_worktree', issues);
218
+ }
219
+ const retention = validateTable(mustflowToml, 'retention', issues);
220
+ if (retention) {
221
+ validateBooleanField(retention, 'enabled', '[retention].enabled', issues);
222
+ const rawEvents = validateNestedTable(retention, 'raw_events', '[retention.raw_events]', issues);
223
+ if (rawEvents) {
224
+ validateAllowedStringField(rawEvents, 'store', '[retention.raw_events].store', ALLOWED_RETENTION_STORES, issues);
225
+ validatePositiveIntegerField(rawEvents, 'max_file_mb', '[retention.raw_events].max_file_mb', issues);
226
+ validatePositiveIntegerField(rawEvents, 'max_total_mb', '[retention.raw_events].max_total_mb', issues);
227
+ validatePositiveIntegerField(rawEvents, 'max_age_days', '[retention.raw_events].max_age_days', issues);
228
+ validateAllowedStringField(rawEvents, 'on_limit', '[retention.raw_events].on_limit', ALLOWED_RETENTION_ON_LIMIT, issues);
229
+ }
230
+ const runReceipts = validateNestedTable(retention, 'run_receipts', '[retention.run_receipts]', issues);
231
+ if (runReceipts) {
232
+ validateAllowedStringField(runReceipts, 'store', '[retention.run_receipts].store', ALLOWED_RETENTION_STORES, issues);
233
+ validatePositiveIntegerField(runReceipts, 'max_file_kb', '[retention.run_receipts].max_file_kb', issues);
234
+ validatePositiveIntegerField(runReceipts, 'max_items', '[retention.run_receipts].max_items', issues);
235
+ validatePositiveIntegerField(runReceipts, 'max_total_mb', '[retention.run_receipts].max_total_mb', issues);
236
+ validatePositiveIntegerField(runReceipts, 'keep_stdout_tail_bytes', '[retention.run_receipts].keep_stdout_tail_bytes', issues);
237
+ validatePositiveIntegerField(runReceipts, 'keep_stderr_tail_bytes', '[retention.run_receipts].keep_stderr_tail_bytes', issues);
238
+ }
239
+ const knowledge = validateNestedTable(retention, 'knowledge', '[retention.knowledge]', issues);
240
+ if (knowledge) {
241
+ validateBooleanField(knowledge, 'enabled', '[retention.knowledge].enabled', issues);
242
+ validateAllowedStringField(knowledge, 'store', '[retention.knowledge].store', ALLOWED_RETENTION_STORES, issues);
243
+ validatePositiveIntegerField(knowledge, 'max_file_kb', '[retention.knowledge].max_file_kb', issues);
244
+ validatePositiveIntegerField(knowledge, 'max_total_mb', '[retention.knowledge].max_total_mb', issues);
245
+ validateBooleanField(knowledge, 'require_source_refs', '[retention.knowledge].require_source_refs', issues);
246
+ validateBooleanField(knowledge, 'require_review_status', '[retention.knowledge].require_review_status', issues);
247
+ }
248
+ const context = validateNestedTable(retention, 'context', '[retention.context]', issues);
249
+ if (context) {
250
+ validatePositiveIntegerField(context, 'max_file_kb', '[retention.context].max_file_kb', issues);
251
+ }
252
+ const handoffs = validateNestedTable(retention, 'handoffs', '[retention.handoffs]', issues);
253
+ if (handoffs) {
254
+ validateAllowedStringField(handoffs, 'store', '[retention.handoffs].store', ALLOWED_RETENTION_STORES, issues);
255
+ validatePositiveIntegerField(handoffs, 'max_file_kb', '[retention.handoffs].max_file_kb', issues);
256
+ validatePositiveIntegerField(handoffs, 'max_total_mb', '[retention.handoffs].max_total_mb', issues);
257
+ validateBooleanField(handoffs, 'require_source_refs', '[retention.handoffs].require_source_refs', issues);
258
+ }
259
+ const repoMap = validateNestedTable(retention, 'repo_map', '[retention.repo_map]', issues);
260
+ if (repoMap) {
261
+ validatePositiveIntegerField(repoMap, 'max_file_kb', '[retention.repo_map].max_file_kb', issues);
262
+ validateBooleanField(repoMap, 'fail_if_larger', '[retention.repo_map].fail_if_larger', issues);
263
+ }
264
+ }
265
+ }
266
+ function validatePreferencesStringFields(table, tableName, keys, issues) {
267
+ for (const key of keys) {
268
+ validateStringField(table, key, `[preferences.${tableName}].${key}`, issues);
269
+ }
270
+ }
271
+ function validatePreferenceModeFallback(table, key, label, issues) {
272
+ if (!hasOwn(table, key)) {
273
+ return;
274
+ }
275
+ const value = table[key];
276
+ if (typeof value === 'string' && value.trim().length > 0) {
277
+ return;
278
+ }
279
+ if (!isRecord(value)) {
280
+ issues.push({ message: `${label} must be a string or TOML table` });
281
+ return;
282
+ }
283
+ validateStringField(value, 'mode', `${label}.mode`, issues);
284
+ validateStringField(value, 'fallback', `${label}.fallback`, issues);
285
+ validateStringField(value, 'rule', `${label}.rule`, issues);
286
+ }
287
+ function validatePreferencesConfig(preferencesToml, issues) {
288
+ if (!preferencesToml) {
289
+ return;
290
+ }
291
+ validateStringField(preferencesToml, 'schema_version', '[preferences].schema_version', issues);
292
+ const project = validateTable(preferencesToml, 'project', issues);
293
+ if (project) {
294
+ validatePreferencesStringFields(project, 'project', ['convention_mode'], issues);
295
+ validateAllowedStringField(project, 'profile', '[preferences.project].profile', ALLOWED_PROJECT_PROFILES, issues);
296
+ }
297
+ const language = validateTable(preferencesToml, 'language', issues);
298
+ if (language) {
299
+ validatePreferencesStringFields(language, 'language', ['agent_response', 'docs'], issues);
300
+ for (const key of ['code_comments', 'logs', 'user_facing_text', 'commit_messages']) {
301
+ validatePreferenceModeFallback(language, key, `[preferences.language.${key}]`, issues);
302
+ }
303
+ const memory = validateNestedTable(language, 'memory', '[preferences.language.memory]', issues);
304
+ if (memory) {
305
+ validateStringField(memory, 'summary', '[preferences.language.memory].summary', issues);
306
+ validateStringField(memory, 'fallback', '[preferences.language.memory].fallback', issues);
307
+ validateBooleanField(memory, 'preserve_code', '[preferences.language.memory].preserve_code', issues);
308
+ validateBooleanField(memory, 'preserve_paths', '[preferences.language.memory].preserve_paths', issues);
309
+ validateBooleanField(memory, 'preserve_error_output', '[preferences.language.memory].preserve_error_output', issues);
310
+ }
311
+ }
312
+ const formatting = validateTable(preferencesToml, 'formatting', issues);
313
+ if (formatting) {
314
+ validatePreferencesStringFields(formatting, 'formatting', [
315
+ 'indentation',
316
+ 'indentation_when_missing',
317
+ 'line_endings',
318
+ 'line_endings_when_missing',
319
+ 'quote_style',
320
+ 'trailing_whitespace',
321
+ ], issues);
322
+ }
323
+ const codeStyle = validateTable(preferencesToml, 'code_style', issues);
324
+ if (codeStyle) {
325
+ validatePreferencesStringFields(codeStyle, 'code_style', ['naming', 'comments', 'public_api_docs'], issues);
326
+ validateBooleanField(codeStyle, 'avoid_drive_by_refactors', '[preferences.code_style].avoid_drive_by_refactors', issues);
327
+ }
328
+ const refactoring = validateTable(preferencesToml, 'refactoring', issues);
329
+ if (refactoring) {
330
+ const hotspots = validateNestedTable(refactoring, 'hotspots', '[preferences.refactoring.hotspots]', issues);
331
+ if (hotspots) {
332
+ for (const field of [
333
+ 'large_file_candidate_kb',
334
+ 'history_days',
335
+ 'primary_candidate_limit',
336
+ 'structure_candidate_limit',
337
+ 'full_file_candidate_limit',
338
+ ]) {
339
+ validatePositiveIntegerField(hotspots, field, `[preferences.refactoring.hotspots].${field}`, issues);
340
+ }
341
+ }
342
+ }
343
+ const git = validateTable(preferencesToml, 'git', issues);
344
+ if (git) {
345
+ validatePreferencesStringFields(git, 'git', ['commit_message_style', 'commit_message_language'], issues);
346
+ validateBooleanField(git, 'auto_stage', '[preferences.git].auto_stage', issues);
347
+ validateBooleanField(git, 'auto_commit', '[preferences.git].auto_commit', issues);
348
+ validateBooleanField(git, 'auto_push', '[preferences.git].auto_push', issues);
349
+ const commitMessage = validateNestedTable(git, 'commit_message', '[preferences.git.commit_message]', issues);
350
+ if (commitMessage) {
351
+ validatePreferencesStringFields(commitMessage, 'git.commit_message', ['suggest', 'style', 'language', 'language_when_missing', 'scope', 'include_body'], issues);
352
+ validateAllowedStringField(commitMessage, 'style', '[preferences.git.commit_message].style', ALLOWED_COMMIT_MESSAGE_STYLES, issues);
353
+ validatePositiveIntegerField(commitMessage, 'max_suggestions', '[preferences.git.commit_message].max_suggestions', issues);
354
+ validateBooleanField(commitMessage, 'split_when_multiple_concerns', '[preferences.git.commit_message].split_when_multiple_concerns', issues);
355
+ validateBooleanField(commitMessage, 'avoid_sensitive_details', '[preferences.git.commit_message].avoid_sensitive_details', issues);
356
+ }
357
+ }
358
+ const reporting = validateTable(preferencesToml, 'reporting', issues);
359
+ if (reporting) {
360
+ const commitSuggestion = validateNestedTable(reporting, 'commit_suggestion', '[preferences.reporting.commit_suggestion]', issues);
361
+ if (commitSuggestion) {
362
+ validateBooleanField(commitSuggestion, 'enabled', '[preferences.reporting.commit_suggestion].enabled', issues);
363
+ validatePreferencesStringFields(commitSuggestion, 'reporting.commit_suggestion', ['when', 'source'], issues);
364
+ }
365
+ }
366
+ const release = validateTable(preferencesToml, 'release', issues);
367
+ if (release) {
368
+ const versioning = validateNestedTable(release, 'versioning', '[preferences.release.versioning]', issues);
369
+ if (versioning) {
370
+ for (const field of RELEASE_VERSIONING_BOOLEAN_FIELDS) {
371
+ validateBooleanField(versioning, field, `[preferences.release.versioning].${field}`, issues);
372
+ }
373
+ }
374
+ }
375
+ const verification = validateTable(preferencesToml, 'verification', issues);
376
+ if (verification) {
377
+ const selection = validateNestedTable(verification, 'selection', '[preferences.verification.selection]', issues);
378
+ if (selection) {
379
+ validateAllowedStringField(selection, 'strategy', '[preferences.verification.selection].strategy', ALLOWED_VERIFICATION_SELECTION_STRATEGIES, issues);
380
+ for (const field of VERIFICATION_SELECTION_BOOLEAN_FIELDS) {
381
+ validateBooleanField(selection, field, `[preferences.verification.selection].${field}`, issues);
382
+ }
383
+ }
384
+ }
385
+ const testing = validateTable(preferencesToml, 'testing', issues);
386
+ if (testing) {
387
+ const authoring = validateNestedTable(testing, 'authoring', '[preferences.testing.authoring]', issues);
388
+ if (authoring) {
389
+ validateAllowedStringField(authoring, 'new_test_policy', '[preferences.testing.authoring].new_test_policy', ALLOWED_TEST_AUTHORING_POLICIES, issues);
390
+ for (const field of TEST_AUTHORING_BOOLEAN_FIELDS) {
391
+ validateBooleanField(authoring, field, `[preferences.testing.authoring].${field}`, issues);
392
+ }
393
+ }
394
+ }
395
+ const docs = validateTable(preferencesToml, 'docs', issues);
396
+ if (docs) {
397
+ validateStringArrayField(docs, 'update_when', '[preferences.docs].update_when', issues);
398
+ validateStringField(docs, 'tone', '[preferences.docs].tone', issues);
399
+ }
400
+ const logging = validateTable(preferencesToml, 'logging', issues);
401
+ if (logging) {
402
+ validateStringField(logging, 'style', '[preferences.logging].style', issues);
403
+ validateBooleanField(logging, 'include_sensitive_data', '[preferences.logging].include_sensitive_data', issues);
404
+ validateStringField(logging, 'language', '[preferences.logging].language', issues);
405
+ }
406
+ const productI18n = validateTable(preferencesToml, 'product_i18n', issues);
407
+ if (productI18n) {
408
+ validateBooleanField(productI18n, 'enabled', '[preferences.product_i18n].enabled', issues);
409
+ validateStringField(productI18n, 'source_locale', '[preferences.product_i18n].source_locale', issues);
410
+ validateStringField(productI18n, 'fallback_locale', '[preferences.product_i18n].fallback_locale', issues);
411
+ validateStringField(productI18n, 'locale_tag_format', '[preferences.product_i18n].locale_tag_format', issues);
412
+ validateStringField(productI18n, 'user_facing_text_policy', '[preferences.product_i18n].user_facing_text_policy', issues);
413
+ validateStringField(productI18n, 'hardcoded_user_facing_strings', '[preferences.product_i18n].hardcoded_user_facing_strings', issues);
414
+ validateAllowedStringField(productI18n, 'translation_policy', '[preferences.product_i18n].translation_policy', ALLOWED_TRANSLATION_POLICIES, issues);
415
+ validateStringArrayField(productI18n, 'target_locales', '[preferences.product_i18n].target_locales', issues);
416
+ validateStringArrayField(productI18n, 'do_not_translate', '[preferences.product_i18n].do_not_translate', issues);
417
+ }
418
+ }
419
+ function validateVersioningConfig(versioningToml, issues) {
420
+ if (!versioningToml) {
421
+ return;
422
+ }
423
+ validateRequiredStringField(versioningToml, 'schema_version', `[${VERSIONING_CONFIG_PATH}].schema_version`, issues);
424
+ if (!hasOwn(versioningToml, 'sources')) {
425
+ issues.push({ message: `${VERSIONING_CONFIG_PATH} must define [[sources]]` });
426
+ return;
427
+ }
428
+ const sources = versioningToml.sources;
429
+ if (!Array.isArray(sources) || sources.length === 0 || !sources.every(isRecord)) {
430
+ issues.push({ message: `${VERSIONING_CONFIG_PATH} sources must be a non-empty array of TOML tables` });
431
+ return;
432
+ }
433
+ for (const [index, source] of sources.entries()) {
434
+ const label = `${VERSIONING_CONFIG_PATH} sources[${index}]`;
435
+ validateRequiredPathField(source, 'path', `${label}.path`, issues);
436
+ validateRequiredStringField(source, 'kind', `${label}.kind`, issues);
437
+ validateAllowedStringField(source, 'kind', `${label}.kind`, ALLOWED_VERSION_SOURCE_KINDS, issues);
438
+ validateRequiredStringField(source, 'authority', `${label}.authority`, issues);
439
+ validateAllowedStringField(source, 'authority', `${label}.authority`, ALLOWED_VERSION_SOURCE_AUTHORITIES, issues);
440
+ validateStringField(source, 'description', `${label}.description`, issues);
441
+ }
442
+ }
443
+ /**
444
+ * mf:anchor cli.validation.command-intents.dispatch
445
+ * purpose: Delegate command intent contract checks to the shared core validator.
446
+ * search: commands.toml, command contract validation, check command
447
+ * invariant: CLI check preserves command-contract messages while shared policy lives in core.
448
+ * risk: config, security
449
+ */
450
+ function validateCommandIntents(commandsToml, issues) {
451
+ issues.push(...validateCommandContractConfig(commandsToml));
452
+ }
453
+ function validateSkills(projectRoot, issues) {
454
+ const skillsRoot = path.join(projectRoot, '.mustflow', 'skills');
455
+ const skillFiles = listFilesRecursive(skillsRoot).filter((relativePath) => relativePath.endsWith('/SKILL.md'));
456
+ for (const relativePath of skillFiles) {
457
+ const absolutePath = path.join(skillsRoot, relativePath);
458
+ const content = readFileSync(absolutePath, 'utf8');
459
+ const sectionIds = readSkillSectionIds(content);
460
+ const missingSectionIds = REQUIRED_SKILL_SECTION_IDS.filter((sectionId) => !sectionIds.has(sectionId));
461
+ if (missingSectionIds.length > 0) {
462
+ issues.push({
463
+ message: `Missing required skill section ids in .mustflow/skills/${toPosixPath(relativePath)}: ${missingSectionIds.join(', ')}`,
464
+ });
465
+ }
466
+ }
467
+ }
468
+ function readSkillSectionIds(content) {
469
+ return new Set([...content.matchAll(SKILL_SECTION_MARKER_PATTERN)].map((match) => match[1]));
470
+ }
471
+ function parseSimpleFrontmatter(content) {
472
+ if (!content.startsWith('---')) {
473
+ return {};
474
+ }
475
+ const end = content.indexOf('\n---', 3);
476
+ if (end === -1) {
477
+ return {};
478
+ }
479
+ const frontmatter = {};
480
+ for (const line of content.slice(3, end).split(/\r?\n/)) {
481
+ const separatorIndex = line.indexOf(':');
482
+ if (separatorIndex === -1) {
483
+ continue;
484
+ }
485
+ const key = line.slice(0, separatorIndex).trim();
486
+ const value = line.slice(separatorIndex + 1).trim().replace(/^["']|["']$/g, '');
487
+ if (key.length > 0 && value.length > 0) {
488
+ frontmatter[key] = value;
489
+ }
490
+ }
491
+ return frontmatter;
492
+ }
493
+ function readFrontmatterLines(content) {
494
+ if (!content.startsWith('---')) {
495
+ return [];
496
+ }
497
+ const end = content.indexOf('\n---', 3);
498
+ if (end === -1) {
499
+ return [];
500
+ }
501
+ return content
502
+ .slice(3, end)
503
+ .split(/\n/u)
504
+ .map((line) => line.replace(/\r$/u, ''));
505
+ }
506
+ function stripScalarMarkers(value) {
507
+ return value.trim().replace(/^["'`]|["'`]$/g, '').trim();
508
+ }
509
+ function readFrontmatterList(content, key) {
510
+ const lines = readFrontmatterLines(content);
511
+ const values = [];
512
+ let keyIndent;
513
+ for (const line of lines) {
514
+ const keyMatch = line.match(new RegExp(`^(\\s*)${key}:\\s*$`, 'u'));
515
+ if (keyIndent === undefined) {
516
+ if (keyMatch) {
517
+ keyIndent = keyMatch[1].length;
518
+ }
519
+ continue;
520
+ }
521
+ if (line.trim().length === 0) {
522
+ continue;
523
+ }
524
+ const lineIndent = line.match(/^\s*/u)?.[0].length ?? 0;
525
+ const itemMatch = line.match(/^\s*-\s+(.+)$/u);
526
+ if (lineIndent <= keyIndent && !itemMatch) {
527
+ break;
528
+ }
529
+ if (itemMatch) {
530
+ const value = stripScalarMarkers(itemMatch[1]);
531
+ if (value.length > 0) {
532
+ values.push(value);
533
+ }
534
+ }
535
+ }
536
+ return values;
537
+ }
538
+ function validateContextDocuments(projectRoot, issues) {
539
+ const contextRoot = path.join(projectRoot, '.mustflow', 'context');
540
+ const contextFiles = listFilesRecursive(contextRoot).filter((relativePath) => relativePath.endsWith('.md'));
541
+ for (const relativePath of contextFiles) {
542
+ const normalizedPath = `.mustflow/context/${toPosixPath(relativePath)}`;
543
+ const content = readFileSync(path.join(contextRoot, relativePath), 'utf8');
544
+ const frontmatter = parseSimpleFrontmatter(content);
545
+ if (frontmatter.kind !== 'mustflow-context') {
546
+ issues.push({ message: `${normalizedPath} frontmatter kind must be "mustflow-context"` });
547
+ }
548
+ if (!frontmatter.name) {
549
+ issues.push({ message: `${normalizedPath} frontmatter name is required` });
550
+ }
551
+ if (!frontmatter.authority || !ALLOWED_CONTEXT_DOCUMENT_AUTHORITIES.has(frontmatter.authority)) {
552
+ issues.push({
553
+ message: `${normalizedPath} frontmatter authority must be "contextual", "derived", or "external"`,
554
+ });
555
+ }
556
+ }
557
+ }
558
+ function validateManifestLock(projectRoot, issues) {
559
+ for (const issue of inspectManifestLock(projectRoot).issues) {
560
+ issues.push({ message: issue });
561
+ }
562
+ }
563
+ function validateStrictPromptCachePolicy(mustflowToml, issues) {
564
+ if (!mustflowToml || !isRecord(mustflowToml.prompt_cache)) {
565
+ pushStrictIssue(issues, '[prompt_cache] table is required');
566
+ return;
567
+ }
568
+ const promptCache = mustflowToml.prompt_cache;
569
+ if (!isRecord(promptCache.layers)) {
570
+ pushStrictIssue(issues, '[prompt_cache.layers] table is required');
571
+ return;
572
+ }
573
+ const stable = promptCache.layers.stable;
574
+ const volatile = promptCache.layers.volatile;
575
+ if (!isRecord(stable) || !Array.isArray(stable.read)) {
576
+ pushStrictIssue(issues, '[prompt_cache.layers.stable].read is required');
577
+ return;
578
+ }
579
+ const volatileSources = isRecord(volatile) && Array.isArray(volatile.sources)
580
+ ? new Set(volatile.sources.filter((source) => typeof source === 'string'))
581
+ : new Set();
582
+ for (const entry of stable.read) {
583
+ if (typeof entry === 'string' && volatileSources.has(entry)) {
584
+ pushStrictIssue(issues, `[prompt_cache.layers.stable].read must not include volatile path "${entry}"`);
585
+ }
586
+ }
587
+ if (promptCache.exclude_volatile_state_from_prefix !== true) {
588
+ pushStrictIssue(issues, '[prompt_cache].exclude_volatile_state_from_prefix should be true');
589
+ }
590
+ }
591
+ function validateStrictVersionSources(projectRoot, preferencesToml, versioningToml, issues) {
592
+ if (versioningToml) {
593
+ for (const source of readDeclaredVersionSources(projectRoot)) {
594
+ if (!existsSync(path.join(projectRoot, source.path))) {
595
+ pushStrictIssue(issues, `${VERSIONING_CONFIG_PATH} source "${source.path}" does not exist`);
596
+ }
597
+ }
598
+ }
599
+ if (!releaseVersioningIsEnabled(preferencesToml)) {
600
+ return;
601
+ }
602
+ if (detectVersionSourcePaths(projectRoot).length > 0) {
603
+ return;
604
+ }
605
+ pushStrictIssue(issues, '[release.versioning] is enabled but no version source was detected; add .mustflow/config/versioning.toml or a package/template version source');
606
+ }
607
+ function validateStrictTemplateVersionSync(projectRoot, preferencesToml, issues) {
608
+ const changedPaths = existsSync(path.join(projectRoot, '.git')) ? readGitChangedFiles(projectRoot) : undefined;
609
+ for (const issue of validateTemplateVersionSync(projectRoot, preferencesToml, changedPaths)) {
610
+ if (issue.severity === 'warning') {
611
+ pushStrictWarning(issues, issue.message);
612
+ continue;
613
+ }
614
+ pushStrictIssue(issues, issue.message);
615
+ }
616
+ }
617
+ function validateStrictCommandDefaults(projectRoot, commandsToml, issues) {
618
+ for (const issue of validateCommandContractStrictDefaults(projectRoot, commandsToml)) {
619
+ if (issue.severity === 'warning') {
620
+ pushStrictWarning(issues, issue.message);
621
+ continue;
622
+ }
623
+ pushStrictIssue(issues, issue.message);
624
+ }
625
+ }
626
+ function validateStrictRouterIndexes(projectRoot, issues) {
627
+ for (const relativePath of ROUTER_INDEX_FILES) {
628
+ const filePath = path.join(projectRoot, relativePath);
629
+ if (!existsSync(filePath)) {
630
+ continue;
631
+ }
632
+ const content = readFileSync(filePath, 'utf8');
633
+ if (ROUTER_INDEX_PROCEDURE_SECTION_PATTERN.test(content)) {
634
+ pushStrictIssue(issues, `${relativePath} must stay a routing index and must not embed skill procedure sections`);
635
+ }
636
+ }
637
+ }
638
+ function validateSkillIndexRouteShape(content, issues) {
639
+ for (const line of content.split(/\r?\n/u)) {
640
+ if (!line.trim().startsWith('|')) {
641
+ continue;
642
+ }
643
+ const cells = line
644
+ .trim()
645
+ .replace(/^\|/u, '')
646
+ .replace(/\|$/u, '')
647
+ .split('|')
648
+ .map((cell) => cell.trim());
649
+ if (cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/u.test(cell))) {
650
+ continue;
651
+ }
652
+ const skillPathColumn = findSkillIndexRoutePathColumn(cells);
653
+ if (skillPathColumn < 0) {
654
+ continue;
655
+ }
656
+ const [skillPath] = readBacktickValues(cells[skillPathColumn]);
657
+ if (cells.length !== SKILL_INDEX_ROUTE_COLUMN_COUNT || skillPathColumn !== SKILL_INDEX_SKILL_PATH_COLUMN_INDEX) {
658
+ pushStrictIssue(issues, `${SKILL_INDEX_PATH} route table rows must use columns: ${SKILL_INDEX_ROUTE_COLUMNS}`);
659
+ continue;
660
+ }
661
+ for (const columnIndex of [0, 2, 3, 4, 5, 6]) {
662
+ if (!cells[columnIndex]?.trim()) {
663
+ pushStrictIssue(issues, `${SKILL_INDEX_PATH} route ${skillPath} has an empty route column`);
664
+ break;
665
+ }
666
+ }
667
+ }
668
+ }
669
+ function skillRouteName(skillPath) {
670
+ return /^\.mustflow\/skills\/([^/]+)\/SKILL\.md$/u.exec(skillPath)?.[1];
671
+ }
672
+ function readOptionalStringArray(value, label, issues) {
673
+ if (value === undefined) {
674
+ return [];
675
+ }
676
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || entry.trim().length === 0)) {
677
+ pushStrictIssue(issues, `${label} must be a string array`);
678
+ return [];
679
+ }
680
+ return value.map((entry) => entry.trim());
681
+ }
682
+ function validateSkillRouteMetadataTable(skillName, route, issues) {
683
+ const label = `${SKILL_ROUTES_METADATA_PATH} routes.${skillName}`;
684
+ const rawCategory = typeof route.category === 'string' ? route.category : undefined;
685
+ const category = rawCategory && ALLOWED_SKILL_ROUTE_CATEGORIES.has(rawCategory)
686
+ ? rawCategory
687
+ : undefined;
688
+ const routeType = typeof route.route_type === 'string' ? route.route_type : undefined;
689
+ const profiles = readOptionalStringArray(route.profiles, `${label}.profiles`, issues);
690
+ const appliesToReasons = readOptionalStringArray(route.applies_to_reasons, `${label}.applies_to_reasons`, issues);
691
+ const mutuallyExclusiveWith = readOptionalStringArray(route.mutually_exclusive_with, `${label}.mutually_exclusive_with`, issues);
692
+ if (!category) {
693
+ pushStrictIssue(issues, `${label}.category must be one of ${[...ALLOWED_SKILL_ROUTE_CATEGORIES].join(', ')}`);
694
+ }
695
+ if (!routeType || !ALLOWED_SKILL_ROUTE_TYPES.has(routeType)) {
696
+ pushStrictIssue(issues, `${label}.route_type must be one of ${[...ALLOWED_SKILL_ROUTE_TYPES].join(', ')}`);
697
+ }
698
+ if (!isPositiveInteger(route.priority)) {
699
+ pushStrictIssue(issues, `${label}.priority must be a positive integer`);
700
+ }
701
+ for (const profile of profiles) {
702
+ if (!ALLOWED_SKILL_ROUTE_PROFILES.has(profile)) {
703
+ pushStrictIssue(issues, `${label}.profiles references unknown profile "${profile}"`);
704
+ }
705
+ }
706
+ for (const reason of appliesToReasons) {
707
+ if (!/^[a-z][a-z0-9_]*$/u.test(reason)) {
708
+ pushStrictIssue(issues, `${label}.applies_to_reasons entry "${reason}" must use snake_case`);
709
+ }
710
+ }
711
+ return {
712
+ skillName,
713
+ category,
714
+ routeType,
715
+ priority: route.priority,
716
+ mutuallyExclusiveWith,
717
+ };
718
+ }
719
+ function readSkillRouteMetadata(projectRoot, issues) {
720
+ const metadataPath = path.join(projectRoot, ...SKILL_ROUTES_METADATA_PATH.split('/'));
721
+ if (!existsSync(metadataPath)) {
722
+ return undefined;
723
+ }
724
+ let parsed;
725
+ try {
726
+ parsed = readTomlFile(metadataPath);
727
+ }
728
+ catch (error) {
729
+ const message = error instanceof Error ? error.message : String(error);
730
+ pushStrictIssue(issues, `Invalid TOML in ${SKILL_ROUTES_METADATA_PATH}: ${message}`);
731
+ return new Map();
732
+ }
733
+ if (!isRecord(parsed)) {
734
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} must contain a TOML table`);
735
+ return new Map();
736
+ }
737
+ if (parsed.schema_version !== '1') {
738
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} schema_version must be "1"`);
739
+ }
740
+ if (!isRecord(parsed.routes)) {
741
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} must define a [routes] table`);
742
+ return new Map();
743
+ }
744
+ const metadata = new Map();
745
+ for (const [skillName, route] of Object.entries(parsed.routes)) {
746
+ if (!/^[a-z][a-z0-9-]*$/u.test(skillName)) {
747
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route key "${skillName}" must be a skill folder name`);
748
+ continue;
749
+ }
750
+ if (!isRecord(route)) {
751
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} routes.${skillName} must be a TOML table`);
752
+ continue;
753
+ }
754
+ metadata.set(skillName, validateSkillRouteMetadataTable(skillName, route, issues));
755
+ }
756
+ return metadata;
757
+ }
758
+ function validateSkillRouteMetadataAlignment(metadata, routeSkillNames, expectedSkillNames, issues) {
759
+ for (const skillName of routeSkillNames) {
760
+ if (!metadata.has(skillName)) {
761
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} is missing metadata for route "${skillName}"`);
762
+ }
763
+ }
764
+ for (const [skillName, route] of metadata.entries()) {
765
+ if (!routeSkillNames.has(skillName)) {
766
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" is not listed in ${SKILL_INDEX_PATH}`);
767
+ }
768
+ if (!expectedSkillNames.has(skillName)) {
769
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" points to a missing skill document`);
770
+ }
771
+ for (const otherSkillName of route.mutuallyExclusiveWith) {
772
+ if (otherSkillName === skillName) {
773
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" cannot be mutually exclusive with itself`);
774
+ }
775
+ else if (!metadata.has(otherSkillName)) {
776
+ pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" references unknown mutually exclusive route "${otherSkillName}"`);
777
+ }
778
+ else if (!metadata.get(otherSkillName)?.mutuallyExclusiveWith.includes(skillName)) {
779
+ pushStrictWarning(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" lists "${otherSkillName}" as mutually exclusive but the reverse route does not`);
780
+ }
781
+ }
782
+ }
783
+ }
784
+ function validateSkillIndexRoutes(projectRoot, commandsToml, skillFiles, issues) {
785
+ const skillIndexPath = path.join(projectRoot, SKILL_INDEX_PATH);
786
+ if (!existsSync(skillIndexPath)) {
787
+ return;
788
+ }
789
+ const skillIndexContent = readFileSync(skillIndexPath, 'utf8');
790
+ validateSkillIndexRouteShape(skillIndexContent, issues);
791
+ const skillRoutes = parseSkillIndexRoutes(skillIndexContent);
792
+ const routedSkillPaths = new Set();
793
+ const expectedSkillPaths = new Set(skillFiles.map((relativePath) => `.mustflow/skills/${relativePath}`));
794
+ const expectedSkillNames = new Set(skillFiles
795
+ .map((relativePath) => toPosixPath(relativePath).split('/')[0])
796
+ .filter((value) => Boolean(value)));
797
+ const routedSkillNames = new Set();
798
+ const seenSkillPaths = new Set();
799
+ const routeMetadata = readSkillRouteMetadata(projectRoot, issues);
800
+ for (const warning of findSkillRouteConflictWarnings(skillRoutes)) {
801
+ pushStrictWarning(issues, `${SKILL_INDEX_PATH} ${warning}`);
802
+ }
803
+ for (const route of skillRoutes) {
804
+ if (!route.skillPath.startsWith('.mustflow/skills/') || !route.skillPath.endsWith('/SKILL.md')) {
805
+ pushStrictIssue(issues, `${SKILL_INDEX_PATH} route "${route.skillPath}" must point to .mustflow/skills/<name>/SKILL.md`);
806
+ continue;
807
+ }
808
+ if (seenSkillPaths.has(route.skillPath)) {
809
+ pushStrictIssue(issues, `${SKILL_INDEX_PATH} has duplicate route for ${route.skillPath}`);
810
+ }
811
+ seenSkillPaths.add(route.skillPath);
812
+ routedSkillPaths.add(route.skillPath);
813
+ const routeSkillName = skillRouteName(route.skillPath);
814
+ if (routeSkillName) {
815
+ routedSkillNames.add(routeSkillName);
816
+ }
817
+ const metadata = routeSkillName ? routeMetadata?.get(routeSkillName) : undefined;
818
+ if (metadata?.category && route.category !== metadata.category) {
819
+ pushStrictIssue(issues, `${SKILL_INDEX_PATH} route "${routeSkillName}" must appear under the ${SKILL_ROUTE_CATEGORY_LABELS[metadata.category]} category section from ${SKILL_ROUTES_METADATA_PATH}`);
820
+ }
821
+ const absoluteSkillPath = path.join(projectRoot, ...route.skillPath.split('/'));
822
+ if (!existsSync(absoluteSkillPath)) {
823
+ pushStrictIssue(issues, `${SKILL_INDEX_PATH} route ${route.skillPath} points to a missing skill document`);
824
+ continue;
825
+ }
826
+ const skillCommandIntents = new Set(readFrontmatterList(readFileSync(absoluteSkillPath, 'utf8'), 'command_intents'));
827
+ for (const intentName of route.commandIntents) {
828
+ if (!isDeclaredCommandIntent(commandsToml, intentName)) {
829
+ pushStrictIssue(issues, `${SKILL_INDEX_PATH} route ${route.skillPath} references unknown command intent "${intentName}"`);
830
+ }
831
+ if (!skillCommandIntents.has(intentName)) {
832
+ pushStrictIssue(issues, `${SKILL_INDEX_PATH} route ${route.skillPath} references command intent "${intentName}" not declared by the skill frontmatter`);
833
+ }
834
+ }
835
+ }
836
+ for (const skillPath of expectedSkillPaths) {
837
+ if (!routedSkillPaths.has(skillPath)) {
838
+ pushStrictIssue(issues, `${skillPath} is not listed in ${SKILL_INDEX_PATH}`);
839
+ }
840
+ }
841
+ if (routeMetadata) {
842
+ validateSkillRouteMetadataAlignment(routeMetadata, routedSkillNames, expectedSkillNames, issues);
843
+ }
844
+ }
845
+ function listManagedMarkdownDocuments(projectRoot) {
846
+ const documents = [];
847
+ for (const relativePath of ['AGENTS.md', '.mustflow/skills/INDEX.md']) {
848
+ if (existsSync(path.join(projectRoot, relativePath))) {
849
+ documents.push(relativePath);
850
+ }
851
+ }
852
+ for (const root of ['.mustflow/docs', '.mustflow/context']) {
853
+ const rootPath = path.join(projectRoot, root);
854
+ for (const relativePath of listFilesRecursive(rootPath)) {
855
+ if (relativePath.endsWith('.md')) {
856
+ documents.push(`${root}/${toPosixPath(relativePath)}`);
857
+ }
858
+ }
859
+ }
860
+ const skillsRoot = path.join(projectRoot, '.mustflow', 'skills');
861
+ for (const relativePath of listFilesRecursive(skillsRoot)) {
862
+ if (relativePath.endsWith('/SKILL.md')) {
863
+ documents.push(`.mustflow/skills/${toPosixPath(relativePath)}`);
864
+ }
865
+ }
866
+ return [...new Set(documents)].sort();
867
+ }
868
+ function validateStrictManagedMarkdownIdentities(projectRoot, issues) {
869
+ for (const relativePath of listManagedMarkdownDocuments(projectRoot)) {
870
+ const expectation = getManagedMarkdownExpectation(relativePath);
871
+ if (!expectation) {
872
+ continue;
873
+ }
874
+ const content = readFileSync(path.join(projectRoot, relativePath), 'utf8');
875
+ const frontmatter = parseSimpleFrontmatter(content);
876
+ const documentLabel = formatManagedMarkdownLabel(relativePath, expectation);
877
+ if (frontmatter.mustflow_doc !== expectation.docId) {
878
+ pushStrictIssue(issues, `${documentLabel} frontmatter mustflow_doc must be "${expectation.docId}"`);
879
+ }
880
+ if (!frontmatter.locale) {
881
+ pushStrictIssue(issues, `${documentLabel} frontmatter locale is required`);
882
+ }
883
+ if (frontmatter.canonical !== 'true' && frontmatter.canonical !== 'false') {
884
+ pushStrictIssue(issues, `${documentLabel} frontmatter canonical must be true or false`);
885
+ }
886
+ if (!/^[1-9]\d*$/u.test(frontmatter.revision ?? '')) {
887
+ pushStrictIssue(issues, `${documentLabel} frontmatter revision must be a positive integer`);
888
+ }
889
+ if (frontmatter.authority !== expectation.authority) {
890
+ pushStrictIssue(issues, `${documentLabel} frontmatter authority must be "${expectation.authority}"`);
891
+ }
892
+ if (frontmatter.lifecycle !== expectation.lifecycle) {
893
+ pushStrictIssue(issues, `${documentLabel} frontmatter lifecycle must be "${expectation.lifecycle}"`);
894
+ }
895
+ }
896
+ }
897
+ function validateStrictRetentionPolicy(mustflowToml, issues) {
898
+ const retention = readRetentionTable(mustflowToml);
899
+ if (!retention) {
900
+ pushStrictIssue(issues, '[retention] table is required');
901
+ return DEFAULT_RETENTION_LIMITS;
902
+ }
903
+ for (const tableName of ['raw_events', 'run_receipts', 'knowledge', 'context', 'repo_map']) {
904
+ if (!readNestedRetentionTable(retention, tableName)) {
905
+ pushStrictIssue(issues, `[retention.${tableName}] table is required`);
906
+ }
907
+ }
908
+ return resolveRetentionLimits(mustflowToml);
909
+ }
910
+ function validateStrictRefreshPolicy(mustflowToml, issues) {
911
+ if (!mustflowToml || !isRecord(mustflowToml.refresh)) {
912
+ pushStrictIssue(issues, '[refresh] table is required');
913
+ return;
914
+ }
915
+ const refresh = mustflowToml.refresh;
916
+ if (refresh.default_method !== 'hash_check') {
917
+ pushStrictIssue(issues, '[refresh].default_method should be "hash_check" for cache-friendly refresh');
918
+ }
919
+ if (!Array.isArray(refresh.required_at) || !refresh.required_at.includes('before_command_run')) {
920
+ pushStrictIssue(issues, '[refresh].required_at should include "before_command_run"');
921
+ }
922
+ if (!isRecord(refresh.levels)) {
923
+ pushStrictIssue(issues, '[refresh.levels] table is required');
924
+ return;
925
+ }
926
+ for (const levelName of ['light', 'command', 'skill', 'full']) {
927
+ if (!isRecord(refresh.levels[levelName])) {
928
+ pushStrictIssue(issues, `[refresh.levels.${levelName}] table is required`);
929
+ continue;
930
+ }
931
+ const read = refresh.levels[levelName].read;
932
+ if (!Array.isArray(read) || read.length === 0) {
933
+ pushStrictIssue(issues, `[refresh.levels.${levelName}].read is required`);
934
+ }
935
+ }
936
+ }
937
+ function validateStrictHarnessPolicy(mustflowToml, issues) {
938
+ for (const tableName of ['harness', 'budget', 'approval', 'isolation', 'compaction']) {
939
+ if (!mustflowToml || !isRecord(mustflowToml[tableName])) {
940
+ pushStrictIssue(issues, `[${tableName}] table is required`);
941
+ }
942
+ }
943
+ }
944
+ function validateStrictVerificationSelectionAuthority(preferencesToml, issues) {
945
+ if (!preferencesToml || !isRecord(preferencesToml.verification)) {
946
+ return;
947
+ }
948
+ const selection = preferencesToml.verification.selection;
949
+ if (!isRecord(selection)) {
950
+ return;
951
+ }
952
+ for (const field of FORBIDDEN_VERIFICATION_SELECTION_AUTHORITY_FIELDS) {
953
+ if (hasOwn(selection, field)) {
954
+ pushStrictIssue(issues, `[preferences.verification.selection].${field} cannot define command authority; use .mustflow/config/commands.toml`);
955
+ }
956
+ }
957
+ }
958
+ function validateStrictCandidateContractModelConfigs(projectRoot, issues) {
959
+ for (const model of getContractModelDefinitions()) {
960
+ const configPath = path.join(projectRoot, ...model.filePath.split('/'));
961
+ if (!existsSync(configPath)) {
962
+ continue;
963
+ }
964
+ let parsed;
965
+ try {
966
+ parsed = readTomlFile(configPath);
967
+ }
968
+ catch (error) {
969
+ const message = error instanceof Error ? error.message : String(error);
970
+ pushStrictIssue(issues, `Invalid TOML in ${model.filePath}: ${message}`);
971
+ continue;
972
+ }
973
+ for (const issue of validateCandidateContractModelConfig(model, parsed)) {
974
+ pushStrictIssue(issues, issue.message);
975
+ }
976
+ }
977
+ }
978
+ function validateStrictReleaseVersioningAuthority(preferencesToml, issues) {
979
+ if (!preferencesToml || !isRecord(preferencesToml.release)) {
980
+ return;
981
+ }
982
+ const versioning = preferencesToml.release.versioning;
983
+ if (!isRecord(versioning)) {
984
+ return;
985
+ }
986
+ for (const field of FORBIDDEN_RELEASE_VERSIONING_CONTRACT_FIELDS) {
987
+ if (hasOwn(versioning, field)) {
988
+ pushStrictIssue(issues, `[preferences.release.versioning].${field} cannot define version sources or release authority; use .mustflow/config/versioning.toml or .mustflow/config/commands.toml`);
989
+ }
990
+ }
991
+ }
992
+ function fileSizeBytes(filePath) {
993
+ return statSync(filePath).size;
994
+ }
995
+ function exceedsKiBLimit(filePath, maxFileKb) {
996
+ return fileSizeBytes(filePath) > maxFileKb * 1024;
997
+ }
998
+ function formatStorageLimitMessage(relativePath, field, filePath, maxFileKb) {
999
+ const actualKiB = Math.ceil(fileSizeBytes(filePath) / 1024);
1000
+ return `${relativePath} exceeds ${field} (${actualKiB} KiB > ${maxFileKb} KiB)`;
1001
+ }
1002
+ function normalizeResourcePath(relativePath) {
1003
+ return relativePath.replace(/\\/g, '/');
1004
+ }
1005
+ function listSkillDirectories(skillsRoot) {
1006
+ const skillNames = new Set();
1007
+ for (const relativePath of listFilesRecursive(skillsRoot)) {
1008
+ const normalizedPath = normalizeResourcePath(relativePath);
1009
+ const [skillName] = normalizedPath.split('/');
1010
+ if (!skillName || !normalizedPath.includes('/')) {
1011
+ continue;
1012
+ }
1013
+ skillNames.add(skillName);
1014
+ }
1015
+ return [...skillNames].sort();
1016
+ }
1017
+ function readSkillResourceManifest(manifestPath, manifestLabel, issues) {
1018
+ try {
1019
+ const parsed = readTomlFile(manifestPath);
1020
+ if (!isRecord(parsed)) {
1021
+ pushStrictIssue(issues, `${manifestLabel} must contain a TOML table`);
1022
+ return undefined;
1023
+ }
1024
+ return parsed;
1025
+ }
1026
+ catch (error) {
1027
+ const message = error instanceof Error ? error.message : String(error);
1028
+ pushStrictIssue(issues, `${manifestLabel} is not valid TOML: ${message}`);
1029
+ return undefined;
1030
+ }
1031
+ }
1032
+ function validateSkillScriptResource(resource, manifestLabel, resourcePath, commandsToml, issues) {
1033
+ if (resource.run_policy !== REQUIRED_SKILL_SCRIPT_RUN_POLICY) {
1034
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} must use run_policy = "${REQUIRED_SKILL_SCRIPT_RUN_POLICY}"`);
1035
+ }
1036
+ if (typeof resource.command_intent !== 'string' || resource.command_intent.trim().length === 0) {
1037
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} must define command_intent`);
1038
+ }
1039
+ else if (!isDeclaredCommandIntent(commandsToml, resource.command_intent)) {
1040
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} references unknown command intent "${resource.command_intent}"`);
1041
+ }
1042
+ else if (!isConfiguredCommandIntent(commandsToml, resource.command_intent)) {
1043
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} references command intent "${resource.command_intent}" that is not configured`);
1044
+ }
1045
+ if (hasOwn(resource, 'network') && typeof resource.network !== 'boolean') {
1046
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} network must be a boolean`);
1047
+ }
1048
+ else if (resource.network === true) {
1049
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} cannot set network = true`);
1050
+ }
1051
+ if (hasOwn(resource, 'destructive') && typeof resource.destructive !== 'boolean') {
1052
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} destructive must be a boolean`);
1053
+ }
1054
+ else if (resource.destructive === true) {
1055
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} cannot set destructive = true`);
1056
+ }
1057
+ if (!hasOwn(resource, 'writes')) {
1058
+ return;
1059
+ }
1060
+ const writes = resource.writes;
1061
+ if (!Array.isArray(writes) || writes.some((entry) => typeof entry !== 'string')) {
1062
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} writes must be a string array`);
1063
+ return;
1064
+ }
1065
+ if (writes.some((entry) => !isSafeRelativePath(entry))) {
1066
+ pushStrictIssue(issues, `${manifestLabel} script ${resourcePath} writes entries must stay inside the skill folder`);
1067
+ }
1068
+ }
1069
+ function validateSkillResourceTable(skillDir, manifestLabel, resourcePath, resource, commandsToml, issues) {
1070
+ if (!isSafeRelativePath(resourcePath)) {
1071
+ pushStrictIssue(issues, `${manifestLabel} resource path "${resourcePath}" must be a safe relative path`);
1072
+ return undefined;
1073
+ }
1074
+ const normalizedPath = normalizeResourcePath(resourcePath);
1075
+ const [rootName] = normalizedPath.split('/');
1076
+ if (!rootName || !SKILL_RESOURCE_ROOTS.has(rootName)) {
1077
+ pushStrictIssue(issues, `${manifestLabel} resource ${normalizedPath} must live under references/, assets/, or scripts/`);
1078
+ return undefined;
1079
+ }
1080
+ if (!existsSync(path.join(skillDir, normalizedPath))) {
1081
+ pushStrictIssue(issues, `${manifestLabel} references missing resource ${normalizedPath}`);
1082
+ }
1083
+ if (!isRecord(resource)) {
1084
+ pushStrictIssue(issues, `${manifestLabel} resource ${normalizedPath} must be a TOML table`);
1085
+ return normalizedPath;
1086
+ }
1087
+ if (typeof resource.type !== 'string' || !ALLOWED_SKILL_RESOURCE_TYPES.has(resource.type)) {
1088
+ pushStrictIssue(issues, `${manifestLabel} resource ${normalizedPath} must set type to "reference", "asset", or "script"`);
1089
+ }
1090
+ else if (resource.type !== SKILL_RESOURCE_TYPE_BY_ROOT[rootName]) {
1091
+ pushStrictIssue(issues, `${manifestLabel} resource ${normalizedPath} type must match its folder`);
1092
+ }
1093
+ if (typeof resource.purpose !== 'string' || resource.purpose.trim().length === 0) {
1094
+ pushStrictIssue(issues, `${manifestLabel} resource ${normalizedPath} must define purpose`);
1095
+ }
1096
+ if (rootName === 'scripts' || resource.type === 'script') {
1097
+ validateSkillScriptResource(resource, manifestLabel, normalizedPath, commandsToml, issues);
1098
+ }
1099
+ return normalizedPath;
1100
+ }
1101
+ function validateSkillResourceManifest(skillDir, manifestLabel, commandsToml, issues) {
1102
+ const declaredResources = new Set();
1103
+ const manifestPath = path.join(skillDir, SKILL_RESOURCE_MANIFEST);
1104
+ if (!existsSync(manifestPath)) {
1105
+ return declaredResources;
1106
+ }
1107
+ const manifest = readSkillResourceManifest(manifestPath, manifestLabel, issues);
1108
+ if (!manifest) {
1109
+ return declaredResources;
1110
+ }
1111
+ if (manifest.schema_version !== '1') {
1112
+ pushStrictIssue(issues, `${manifestLabel} schema_version must be "1"`);
1113
+ }
1114
+ if (!isRecord(manifest.resources)) {
1115
+ pushStrictIssue(issues, `${manifestLabel} must define a [resources] table`);
1116
+ return declaredResources;
1117
+ }
1118
+ for (const [resourcePath, resource] of Object.entries(manifest.resources)) {
1119
+ const normalizedPath = validateSkillResourceTable(skillDir, manifestLabel, resourcePath, resource, commandsToml, issues);
1120
+ if (normalizedPath) {
1121
+ declaredResources.add(normalizedPath);
1122
+ }
1123
+ }
1124
+ return declaredResources;
1125
+ }
1126
+ function validateDeclaredSkillScripts(skillDir, skillName, declaredResources, issues) {
1127
+ const scriptsDir = path.join(skillDir, 'scripts');
1128
+ for (const relativePath of listFilesRecursive(scriptsDir)) {
1129
+ const scriptPath = `scripts/${normalizeResourcePath(relativePath)}`;
1130
+ if (!declaredResources.has(scriptPath)) {
1131
+ pushStrictIssue(issues, `.mustflow/skills/${skillName}/${scriptPath} is not declared in ${SKILL_RESOURCE_MANIFEST}`);
1132
+ }
1133
+ }
1134
+ }
1135
+ function validateSkillCommandIntentReferences(skillLabel, content, commandsToml, issues) {
1136
+ if (!commandsToml || !isRecord(commandsToml.intents)) {
1137
+ return;
1138
+ }
1139
+ for (const intentName of readFrontmatterList(content, 'command_intents')) {
1140
+ if (!isDeclaredCommandIntent(commandsToml, intentName)) {
1141
+ pushStrictIssue(issues, `${skillLabel} metadata.command_intents references unknown command intent "${intentName}"`);
1142
+ }
1143
+ }
1144
+ }
1145
+ function validateSkillCommandPermissionClaims(skillLabel, content, issues) {
1146
+ if (SKILL_COMMAND_PERMISSION_CLAIM_PATTERNS.some((pattern) => pattern.test(content))) {
1147
+ pushStrictIssue(issues, `${skillLabel} claims command execution permission; keep permissions in .mustflow/config/commands.toml`);
1148
+ }
1149
+ }
1150
+ function validateSkillPackageIdentity(skillLabel, skillName, frontmatter, issues) {
1151
+ const packId = frontmatter.pack_id;
1152
+ const skillId = frontmatter.skill_id;
1153
+ if (!packId || !SKILL_PACK_ID_PATTERN.test(packId)) {
1154
+ pushStrictIssue(issues, `${skillLabel} metadata.pack_id must be a dotted package identifier`);
1155
+ }
1156
+ if (!skillId) {
1157
+ pushStrictIssue(issues, `${skillLabel} metadata.skill_id is required`);
1158
+ return;
1159
+ }
1160
+ if (packId && SKILL_PACK_ID_PATTERN.test(packId) && skillId !== `${packId}.${skillName}`) {
1161
+ pushStrictIssue(issues, `${skillLabel} metadata.skill_id must be "${packId}.${skillName}"`);
1162
+ }
1163
+ }
1164
+ function validateStrictSkills(projectRoot, commandsToml, issues) {
1165
+ const skillsRoot = path.join(projectRoot, '.mustflow', 'skills');
1166
+ const skillFiles = listFilesRecursive(skillsRoot).filter((relativePath) => relativePath.endsWith('/SKILL.md'));
1167
+ const skillDirectories = listSkillDirectories(skillsRoot);
1168
+ validateSkillIndexRoutes(projectRoot, commandsToml, skillFiles, issues);
1169
+ for (const skillName of skillDirectories) {
1170
+ const skillDir = path.join(skillsRoot, skillName);
1171
+ if (!existsSync(path.join(skillDir, 'SKILL.md'))) {
1172
+ pushStrictIssue(issues, `.mustflow/skills/${skillName} is a skill folder without SKILL.md`);
1173
+ }
1174
+ const manifestLabel = `.mustflow/skills/${skillName}/${SKILL_RESOURCE_MANIFEST}`;
1175
+ const declaredResources = validateSkillResourceManifest(skillDir, manifestLabel, commandsToml, issues);
1176
+ validateDeclaredSkillScripts(skillDir, skillName, declaredResources, issues);
1177
+ }
1178
+ for (const relativePath of skillFiles) {
1179
+ const absolutePath = path.join(skillsRoot, relativePath);
1180
+ const content = readFileSync(absolutePath, 'utf8');
1181
+ const normalizedRelativePath = toPosixPath(relativePath);
1182
+ const skillName = normalizedRelativePath.split('/')[0] ?? '';
1183
+ const skillLabel = `.mustflow/skills/${normalizedRelativePath}`;
1184
+ const frontmatter = parseSimpleFrontmatter(content);
1185
+ if (frontmatter.mustflow_schema !== SUPPORTED_SKILL_SCHEMA_VERSION) {
1186
+ pushStrictIssue(issues, `${skillLabel} metadata.mustflow_schema must be "${SUPPORTED_SKILL_SCHEMA_VERSION}"`);
1187
+ }
1188
+ if (frontmatter.mustflow_kind !== 'procedure') {
1189
+ pushStrictIssue(issues, `${skillLabel} metadata.mustflow_kind must be "procedure"`);
1190
+ }
1191
+ if (frontmatter.name !== skillName) {
1192
+ pushStrictIssue(issues, `${skillLabel} frontmatter name must match skill folder "${skillName}"`);
1193
+ }
1194
+ validateSkillPackageIdentity(skillLabel, skillName, frontmatter, issues);
1195
+ validateSkillCommandIntentReferences(skillLabel, content, commandsToml, issues);
1196
+ validateSkillCommandPermissionClaims(skillLabel, content, issues);
1197
+ if (RAW_COMMAND_FENCE_PATTERN.test(content)) {
1198
+ pushStrictIssue(issues, `${skillLabel} contains a raw shell command block; reference command intents instead`);
1199
+ }
1200
+ RAW_COMMAND_FENCE_PATTERN.lastIndex = 0;
1201
+ }
1202
+ }
1203
+ function validateStrictRepoMap(projectRoot, issues) {
1204
+ const repoMapPath = path.join(projectRoot, 'REPO_MAP.md');
1205
+ if (!existsSync(repoMapPath)) {
1206
+ return;
1207
+ }
1208
+ const content = readFileSync(repoMapPath, 'utf8');
1209
+ const frontmatter = parseSimpleFrontmatter(content);
1210
+ if (frontmatter.mustflow_doc !== REPO_MAP_DOC_ID) {
1211
+ pushStrictIssue(issues, `REPO_MAP.md frontmatter mustflow_doc must be "${REPO_MAP_DOC_ID}"`);
1212
+ }
1213
+ if (frontmatter.lifecycle !== REPO_MAP_LIFECYCLE) {
1214
+ pushStrictIssue(issues, `REPO_MAP.md frontmatter lifecycle must be "${REPO_MAP_LIFECYCLE}"`);
1215
+ }
1216
+ if (frontmatter.generated_by !== REPO_MAP_GENERATOR) {
1217
+ pushStrictIssue(issues, `REPO_MAP.md frontmatter generated_by must be "${REPO_MAP_GENERATOR}"`);
1218
+ }
1219
+ if (frontmatter.relative_root !== REPO_MAP_RELATIVE_ROOT) {
1220
+ pushStrictIssue(issues, `REPO_MAP.md frontmatter relative_root must be "${REPO_MAP_RELATIVE_ROOT}"`);
1221
+ }
1222
+ if (frontmatter.source_policy !== REPO_MAP_SOURCE_POLICY) {
1223
+ pushStrictIssue(issues, `REPO_MAP.md frontmatter source_policy must be "${REPO_MAP_SOURCE_POLICY}"`);
1224
+ }
1225
+ if (frontmatter.privacy_mode !== REPO_MAP_PRIVACY_MODE) {
1226
+ pushStrictIssue(issues, `REPO_MAP.md frontmatter privacy_mode must be "${REPO_MAP_PRIVACY_MODE}"`);
1227
+ }
1228
+ if (!/^[1-9]\d*$/u.test(frontmatter.anchor_count ?? '')) {
1229
+ pushStrictIssue(issues, 'REPO_MAP.md frontmatter anchor_count must be a positive integer');
1230
+ }
1231
+ if (!REPO_MAP_SOURCE_FINGERPRINT_PATTERN.test(frontmatter.source_fingerprint ?? '')) {
1232
+ pushStrictIssue(issues, 'REPO_MAP.md frontmatter source_fingerprint must be sha256:<64 lowercase hex characters>');
1233
+ }
1234
+ else {
1235
+ const currentSourceFingerprint = frontmatter.source_fingerprint;
1236
+ const expectedSourceFingerprint = parseSimpleFrontmatter(generateRepoMap(projectRoot)).source_fingerprint;
1237
+ if (expectedSourceFingerprint && currentSourceFingerprint !== expectedSourceFingerprint) {
1238
+ pushStrictIssue(issues, 'REPO_MAP.md source_fingerprint is stale; regenerate with mf map --write');
1239
+ }
1240
+ }
1241
+ if (VOLATILE_REPO_MAP_PATTERNS.some((pattern) => pattern.test(content))) {
1242
+ pushStrictIssue(issues, 'REPO_MAP.md contains volatile generated metadata');
1243
+ }
1244
+ if (REPO_MAP_REMOTE_OR_BRANCH_PATTERNS.some((pattern) => pattern.test(content))) {
1245
+ pushStrictIssue(issues, 'REPO_MAP.md contains remote URL or branch metadata');
1246
+ }
1247
+ }
1248
+ function validateStrictContextDocuments(projectRoot, limits, issues) {
1249
+ const contextRoot = path.join(projectRoot, '.mustflow', 'context');
1250
+ const contextFiles = listFilesRecursive(contextRoot).filter((relativePath) => relativePath.endsWith('.md'));
1251
+ const hasDesignAnchor = existsSync(path.join(projectRoot, 'DESIGN.md'));
1252
+ for (const relativePath of contextFiles) {
1253
+ const normalizedPath = `.mustflow/context/${toPosixPath(relativePath)}`;
1254
+ const absolutePath = path.join(contextRoot, relativePath);
1255
+ const content = readFileSync(absolutePath, 'utf8');
1256
+ if (exceedsKiBLimit(absolutePath, limits.contextMaxFileKb)) {
1257
+ pushStrictIssue(issues, formatStorageLimitMessage(normalizedPath, '[retention.context].max_file_kb', absolutePath, limits.contextMaxFileKb));
1258
+ }
1259
+ if (LOCAL_ABSOLUTE_PATH_PATTERNS.some((pattern) => pattern.test(content))) {
1260
+ pushStrictIssue(issues, `${normalizedPath} contains a local absolute path; keep machine-local paths out of context files`);
1261
+ }
1262
+ if (SECRET_LIKE_CONTEXT_PATTERNS.some((pattern) => pattern.test(content))) {
1263
+ pushStrictIssue(issues, `${normalizedPath} contains secret-like key/value text; keep secrets out of context files`);
1264
+ }
1265
+ if (hasDesignAnchor && DESIGN_TOKEN_DEFINITION_PATTERNS.some((pattern) => pattern.test(content))) {
1266
+ pushStrictIssue(issues, `${normalizedPath} duplicates design-token definitions while DESIGN.md exists`);
1267
+ }
1268
+ if (CONTEXT_AUTHORITY_DRIFT_PATTERNS.some((pattern) => pattern.test(content))) {
1269
+ pushStrictIssue(issues, `${normalizedPath} declares command policy or file-edit prohibitions; keep execution rules in AGENTS.md or .mustflow/config/commands.toml`);
1270
+ }
1271
+ }
1272
+ }
1273
+ /**
1274
+ * mf:anchor cli.validation.source-anchors
1275
+ * purpose: Validate structured source anchors as navigation metadata with no command authority.
1276
+ * search: mf:anchor, duplicate id, forbidden instruction, risk tag, anchor density
1277
+ * invariant: Source anchors stay navigation-only and cannot carry command or policy instructions.
1278
+ * risk: config, security
1279
+ */
1280
+ function validateStrictSourceAnchors(projectRoot, issues) {
1281
+ for (const issue of validateSourceAnchorsInProject(projectRoot)) {
1282
+ if (issue.severity === 'warning') {
1283
+ pushStrictWarning(issues, issue.message);
1284
+ continue;
1285
+ }
1286
+ pushStrictIssue(issues, issue.message);
1287
+ }
1288
+ }
1289
+ function validateStrictRunReceipt(projectRoot, issues) {
1290
+ const latestRunPath = path.join(projectRoot, '.mustflow', 'state', 'runs', 'latest.json');
1291
+ if (!existsSync(latestRunPath)) {
1292
+ return;
1293
+ }
1294
+ try {
1295
+ const parsed = JSON.parse(readFileSync(latestRunPath, 'utf8'));
1296
+ if (!isRecord(parsed)) {
1297
+ pushStrictIssue(issues, '.mustflow/state/runs/latest.json must contain a JSON object');
1298
+ }
1299
+ }
1300
+ catch (error) {
1301
+ const message = error instanceof Error ? error.message : String(error);
1302
+ pushStrictIssue(issues, `.mustflow/state/runs/latest.json is not valid JSON: ${message}`);
1303
+ }
1304
+ }
1305
+ function validateStrictStorage(projectRoot, limits, issues) {
1306
+ const repoMapPath = path.join(projectRoot, 'REPO_MAP.md');
1307
+ if (existsSync(repoMapPath) && limits.repoMapFailIfLarger && exceedsKiBLimit(repoMapPath, limits.repoMapMaxFileKb)) {
1308
+ pushStrictIssue(issues, formatStorageLimitMessage('REPO_MAP.md', '[retention.repo_map].max_file_kb', repoMapPath, limits.repoMapMaxFileKb));
1309
+ }
1310
+ const latestRunPath = path.join(projectRoot, '.mustflow', 'state', 'runs', 'latest.json');
1311
+ if (existsSync(latestRunPath) && exceedsKiBLimit(latestRunPath, limits.runReceiptMaxFileKb)) {
1312
+ pushStrictIssue(issues, formatStorageLimitMessage('.mustflow/state/runs/latest.json', '[retention.run_receipts].max_file_kb', latestRunPath, limits.runReceiptMaxFileKb));
1313
+ }
1314
+ const knowledgeRoot = path.join(projectRoot, '.mustflow', 'knowledge');
1315
+ const knowledgeFiles = listFilesRecursive(knowledgeRoot);
1316
+ for (const relativePath of knowledgeFiles) {
1317
+ const absolutePath = path.join(knowledgeRoot, relativePath);
1318
+ if (exceedsKiBLimit(absolutePath, limits.knowledgeMaxFileKb)) {
1319
+ pushStrictIssue(issues, formatStorageLimitMessage(`.mustflow/knowledge/${toPosixPath(relativePath)}`, '[retention.knowledge].max_file_kb', absolutePath, limits.knowledgeMaxFileKb));
1320
+ }
1321
+ }
1322
+ const mustflowRoot = path.join(projectRoot, '.mustflow');
1323
+ const mustflowFiles = listFilesRecursive(mustflowRoot);
1324
+ for (const relativePath of mustflowFiles) {
1325
+ const normalizedPath = toPosixPath(relativePath);
1326
+ const [topLevelDirectory] = normalizedPath.split('/');
1327
+ if (relativePath.toLowerCase().endsWith('.jsonl')) {
1328
+ pushStrictIssue(issues, `.mustflow/${normalizedPath} is a raw JSONL file under .mustflow`);
1329
+ }
1330
+ if (topLevelDirectory && LOCAL_TASK_STATE_ROOTS.has(topLevelDirectory)) {
1331
+ pushStrictIssue(issues, `.mustflow/${normalizedPath} is per-task local state; keep plans and worklogs under ignored local state`);
1332
+ }
1333
+ }
1334
+ }
1335
+ function validateStrict(projectRoot, parsed, issues) {
1336
+ const retentionLimits = validateStrictRetentionPolicy(parsed.mustflowToml, issues);
1337
+ validateStrictPromptCachePolicy(parsed.mustflowToml, issues);
1338
+ validateStrictRefreshPolicy(parsed.mustflowToml, issues);
1339
+ validateStrictHarnessPolicy(parsed.mustflowToml, issues);
1340
+ validateStrictCommandDefaults(projectRoot, parsed.commandsToml, issues);
1341
+ validateStrictReleaseVersioningAuthority(parsed.preferencesToml, issues);
1342
+ validateStrictVerificationSelectionAuthority(parsed.preferencesToml, issues);
1343
+ validateStrictCandidateContractModelConfigs(projectRoot, issues);
1344
+ validateStrictTestSelectionConfig(projectRoot, parsed.commandsToml, issues);
1345
+ validateStrictVersionSources(projectRoot, parsed.preferencesToml, parsed.versioningToml, issues);
1346
+ validateStrictTemplateVersionSync(projectRoot, parsed.preferencesToml, issues);
1347
+ validateStrictManagedMarkdownIdentities(projectRoot, issues);
1348
+ validateStrictRouterIndexes(projectRoot, issues);
1349
+ validateStrictSkills(projectRoot, parsed.commandsToml, issues);
1350
+ validateStrictRepoMap(projectRoot, issues);
1351
+ validateStrictContextDocuments(projectRoot, retentionLimits, issues);
1352
+ validateStrictSourceAnchors(projectRoot, issues);
1353
+ validateStrictRunReceipt(projectRoot, issues);
1354
+ validateStrictStorage(projectRoot, retentionLimits, issues);
1355
+ }
1356
+ function collectCheckIssues(projectRoot, options = {}) {
1357
+ const issues = [];
1358
+ validateRequiredFiles(projectRoot, issues);
1359
+ const parsed = validateToml(projectRoot, issues);
1360
+ validateMustflowConfig(parsed.mustflowToml, issues);
1361
+ validatePreferencesConfig(parsed.preferencesToml, issues);
1362
+ validateVersioningConfig(parsed.versioningToml, issues);
1363
+ validateCommandIntents(parsed.commandsToml, issues);
1364
+ validateSkills(projectRoot, issues);
1365
+ validateContextDocuments(projectRoot, issues);
1366
+ validateManifestLock(projectRoot, issues);
1367
+ if (options.strict) {
1368
+ validateStrict(projectRoot, parsed, issues);
1369
+ }
1370
+ return issues;
1371
+ }
1372
+ export function checkMustflowProjectReport(projectRoot, options = {}) {
1373
+ const issues = collectCheckIssues(projectRoot, options);
1374
+ const errors = issues.filter((issue) => issue.severity !== 'warning').map((issue) => issue.message);
1375
+ const warnings = issues.filter((issue) => issue.severity === 'warning').map((issue) => issue.message);
1376
+ return {
1377
+ issues: errors,
1378
+ warnings,
1379
+ allMessages: [...errors, ...warnings],
1380
+ };
1381
+ }
1382
+ export function checkMustflowProject(projectRoot, options = {}) {
1383
+ return checkMustflowProjectReport(projectRoot, options).issues;
1384
+ }