mustflow 2.22.4 → 2.22.9

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 (72) hide show
  1. package/README.md +17 -75
  2. package/dist/cli/commands/classify.js +2 -0
  3. package/dist/cli/commands/contract-lint.js +2 -2
  4. package/dist/cli/commands/dashboard.js +23 -75
  5. package/dist/cli/commands/help.js +8 -9
  6. package/dist/cli/commands/impact.js +2 -3
  7. package/dist/cli/commands/init.js +61 -5
  8. package/dist/cli/commands/run/receipt.js +1 -0
  9. package/dist/cli/commands/run.js +14 -1
  10. package/dist/cli/commands/update.js +2 -2
  11. package/dist/cli/commands/verify/evidence-input.js +269 -0
  12. package/dist/cli/commands/verify/input.js +212 -0
  13. package/dist/cli/commands/verify.js +23 -482
  14. package/dist/cli/commands/version-sources.js +2 -3
  15. package/dist/cli/i18n/en.js +5 -0
  16. package/dist/cli/i18n/es.js +5 -0
  17. package/dist/cli/i18n/fr.js +5 -0
  18. package/dist/cli/i18n/hi.js +5 -0
  19. package/dist/cli/i18n/ko.js +5 -0
  20. package/dist/cli/i18n/zh.js +5 -0
  21. package/dist/cli/lib/agent-context.js +6 -11
  22. package/dist/cli/lib/dashboard-export.js +2 -0
  23. package/dist/cli/lib/dashboard-mutations.js +79 -0
  24. package/dist/cli/lib/local-index/command-effect-index.js +25 -0
  25. package/dist/cli/lib/local-index/hashing.js +7 -0
  26. package/dist/cli/lib/local-index/index.js +127 -823
  27. package/dist/cli/lib/local-index/source-index.js +137 -0
  28. package/dist/cli/lib/local-index/verification-evidence.js +451 -0
  29. package/dist/cli/lib/local-index/workflow-documents.js +204 -0
  30. package/dist/cli/lib/mustflow-read.js +41 -0
  31. package/dist/cli/lib/project-root.js +1 -2
  32. package/dist/cli/lib/repo-map.js +65 -16
  33. package/dist/cli/lib/run-root-trust.js +27 -0
  34. package/dist/cli/lib/templates.js +124 -8
  35. package/dist/cli/lib/toml.js +6 -1
  36. package/dist/cli/lib/validation/constants.js +2 -0
  37. package/dist/cli/lib/validation/index.js +291 -22
  38. package/dist/cli/lib/validation/primitives.js +2 -2
  39. package/dist/cli/lib/validation/test-selection.js +2 -2
  40. package/dist/core/bounded-output.js +32 -7
  41. package/dist/core/change-classification-policy.js +47 -0
  42. package/dist/core/change-classification.js +10 -43
  43. package/dist/core/check-issues.js +7 -1
  44. package/dist/core/command-contract-validation.js +28 -4
  45. package/dist/core/command-env.js +1 -1
  46. package/dist/core/config-loading.js +9 -3
  47. package/dist/core/contract-lint.js +8 -3
  48. package/dist/core/correlation-id.js +16 -0
  49. package/dist/core/run-receipt.js +1 -0
  50. package/dist/core/safe-filesystem.js +11 -4
  51. package/dist/core/skill-route-alignment.js +1 -0
  52. package/dist/core/skill-route-explanation.js +9 -3
  53. package/dist/core/test-selection.js +2 -3
  54. package/dist/core/verification-scheduler.js +7 -6
  55. package/dist/core/version-sources.js +2 -3
  56. package/package.json +4 -1
  57. package/schemas/README.md +4 -0
  58. package/schemas/change-verification-report.schema.json +4 -0
  59. package/schemas/classify-report.schema.json +4 -0
  60. package/schemas/commands.schema.json +1 -0
  61. package/schemas/dashboard-export.schema.json +4 -0
  62. package/schemas/latest-run-pointer.schema.json +4 -0
  63. package/schemas/run-receipt.schema.json +4 -0
  64. package/schemas/verify-report.schema.json +4 -0
  65. package/schemas/verify-run-manifest.schema.json +4 -0
  66. package/templates/default/i18n.toml +3 -3
  67. package/templates/default/locales/en/.mustflow/skills/INDEX.md +10 -6
  68. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +25 -2
  69. package/templates/default/locales/en/.mustflow/skills/routes.toml +2 -2
  70. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +9 -1
  71. package/templates/default/locales/en/.mustflow/skills/test-design-guard/SKILL.md +9 -1
  72. package/templates/default/manifest.toml +1 -1
@@ -1,47 +1,21 @@
1
+ import { CHANGE_CLASSIFICATION_RULE_DEFINITIONS, UNKNOWN_SURFACE, listChangeClassificationPolicyRuleDescriptors, } from './change-classification-policy.js';
1
2
  export const PUBLIC_SURFACE_UPDATE_POLICIES = [
2
3
  'update',
3
4
  'update_or_mark_stale',
4
5
  'not_applicable',
5
6
  ];
6
- function surface(kind, category, isPublicSurface, validationReasons, affectedContracts, updatePolicy, driftChecks) {
7
+ function compileRule(definition) {
7
8
  return {
8
- kind,
9
- category,
10
- isPublicSurface,
11
- validationReasons,
12
- affectedContracts,
13
- updatePolicy,
14
- driftChecks,
15
- };
16
- }
17
- const UNKNOWN_CHANGE_REASON = 'unknown_change';
18
- const UNKNOWN_SURFACE = surface('unclassified_path', 'unknown', false, [UNKNOWN_CHANGE_REASON], ['unclassified repository path'], 'not_applicable', ['classification rule coverage']);
19
- function rule(id, match, changeKinds, surfaceContract) {
20
- return {
21
- id,
9
+ id: definition.id,
22
10
  patternKind: 'regexp',
23
- pattern: match.source,
24
- patternFlags: match.flags,
25
- match,
26
- changeKinds,
27
- surface: surfaceContract,
11
+ pattern: definition.match.source,
12
+ patternFlags: definition.match.flags,
13
+ match: definition.match,
14
+ changeKinds: definition.changeKinds,
15
+ surface: definition.surface,
28
16
  };
29
17
  }
30
- const CHANGE_CLASSIFICATION_RULES = [
31
- rule('readme_page', /^README\.md$/u, ['documentation'], surface('readme_page', 'documentation', true, ['docs_change', 'copy_change'], ['public documentation', 'command examples'], 'update', ['link targets', 'command examples', 'package metadata references'])),
32
- rule('docs_site_translation', /^docs-site\/src\/content\/docs\/(?!en\/)[^/]+\//u, ['documentation', 'translation'], surface('docs_site_translation', 'documentation', true, ['docs_change', 'i18n_change'], ['documentation site', 'localized content', 'navigation links'], 'update_or_mark_stale', ['source page parity', 'navigation links', 'localized examples'])),
33
- rule('docs_site_page', /^docs-site\/src\/content\/docs\//u, ['documentation'], surface('docs_site_page', 'documentation', true, ['docs_change'], ['documentation site', 'navigation links', 'localized content'], 'update', ['navigation links', 'localized copies', 'command examples'])),
34
- rule('installed_template_translation', /^templates\/[^/]+\/locales\/[^/]+\//u, ['installed_template', 'translation'], surface('installed_template_translation', 'installed-template', true, ['i18n_change', 'template_version_change'], ['installed template files', 'localized workflow documents', 'template i18n metadata'], 'update_or_mark_stale', ['template i18n metadata', 'localized frontmatter', 'source revision'])),
35
- rule('installed_template', /^templates\/[^/]+\//u, ['installed_template'], surface('installed_template', 'installed-template', true, ['template_version_change', 'packaging_change'], ['installed template files', 'package contents', 'template manifest'], 'update', ['template manifest', 'package inventory', 'localized copies'])),
36
- rule('workflow_root', /^(AGENTS\.md|\.mustflow\/(?:docs|context|skills|config)\/)/u, ['workflow'], surface('workflow_root', 'workflow', true, ['mustflow_docs_change', 'mustflow_config_change'], ['agent workflow contract', 'command contract', 'installed workflow files'], 'update', ['strict workflow validation', 'installed template parity', 'skill route alignment'])),
37
- rule('host_instruction', /^(CLAUDE\.md|GEMINI\.md|\.github\/copilot-instructions\.md|\.cursor\/rules\/[^/]+\.(?:md|mdc))$/u, ['workflow', 'host_instruction'], surface('host_instruction', 'workflow', true, ['mustflow_docs_change'], ['host instruction compatibility', 'agent workflow contract', 'command contract boundary'], 'update_or_mark_stale', ['host instruction conflicts', 'command contract boundary'])),
38
- rule('example', /^examples\//u, ['example'], surface('example', 'example', true, ['docs_change', 'public_api_change'], ['generated examples', 'human-readable examples'], 'update', ['example commands', 'linked docs', 'public behavior claims'])),
39
- rule('schema_contract', /^schemas\//u, ['schema'], surface('schema_contract', 'contract', true, ['public_api_change', 'release_risk'], ['JSON schema', 'machine-readable output contract'], 'update', ['schema tests', 'documented JSON fields', 'package inventory'])),
40
- rule('package_metadata', /^package\.json$/u, ['package_metadata'], surface('package_metadata', 'release', true, ['package_metadata_change', 'release_risk'], ['npm package metadata', 'published package contents'], 'update', ['package metadata tests', 'version source discovery', 'published file inventory'])),
41
- rule('test_fixture', /^tests\/fixtures\//u, ['test_fixture'], surface('test_fixture', 'test', false, ['test_change'], ['regression fixture inputs'], 'not_applicable', ['fixture safety', 'test route coverage'])),
42
- rule('test_contract', /^tests\//u, ['test'], surface('test_contract', 'test', false, ['test_change'], ['test behavior contract'], 'not_applicable', ['related test selection'])),
43
- rule('implementation', /^(src|scripts)\//u, ['implementation'], surface('implementation', 'code', false, ['code_change'], ['runtime behavior when exported through CLI or package output'], 'not_applicable', ['related tests', 'build output'])),
44
- ];
18
+ const CHANGE_CLASSIFICATION_RULES = CHANGE_CLASSIFICATION_RULE_DEFINITIONS.map(compileRule);
45
19
  function uniqueSorted(values) {
46
20
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
47
21
  }
@@ -93,14 +67,7 @@ export function classifyChangePath(relativePath) {
93
67
  };
94
68
  }
95
69
  export function listChangeClassificationRuleDescriptors() {
96
- return CHANGE_CLASSIFICATION_RULES.map((classificationRule) => ({
97
- id: classificationRule.id,
98
- patternKind: classificationRule.patternKind,
99
- pattern: classificationRule.pattern,
100
- patternFlags: classificationRule.patternFlags,
101
- changeKinds: classificationRule.changeKinds,
102
- surface: classificationRule.surface,
103
- }));
70
+ return listChangeClassificationPolicyRuleDescriptors();
104
71
  }
105
72
  export function listChangeClassificationValidationReasons() {
106
73
  return uniqueSorted([
@@ -7,6 +7,7 @@ const CHECK_ISSUE_ID_RULES = [
7
7
  ['mustflow.command_contract.max_output_bytes_exceeds_limit', /^\[commands\.(?:defaults|intents\.[^\]]+)\]\.max_output_bytes must be less than or equal to \d+ per output stream$/u],
8
8
  ['mustflow.command_contract.oneshot_stdin_not_closed', /^Oneshot intent [^\s]+ must set stdin = "closed"$/u],
9
9
  ['mustflow.command_contract.long_running_agent_allowed', /^Long-running intent [^\s]+ must not use run_policy = "agent_allowed"$/u],
10
+ ['mustflow.command_contract.agent_shell_requires_allow', /^Agent-runnable shell intent [^\s]+ must set allow_shell = true$/u],
10
11
  ['mustflow.command_contract.executable_source_missing', /^Configured intent [^\s]+ must define argv or mode = "shell" with cmd$/u],
11
12
  ['mustflow.command_contract.shell_background_pattern', /^Shell intent [^\s]+ contains a blocked long-running or background pattern$/u],
12
13
  ['mustflow.command_contract.long_running_command_pattern', /^Intent [^\s]+ contains a blocked long-running or background command pattern$/u],
@@ -17,7 +18,7 @@ const CHECK_ISSUE_ID_RULES = [
17
18
  ['mustflow.command_contract.effects_invalid', /^(?:Strict: )?(?:\[commands\.(?:resources|intents\.[^\]]+\.effects)[^\]]*\]|Command effect for intent [^\s]+ must define path, paths, or lock)/u],
18
19
  ['mustflow.command_contract.effect_path_escape', /^Strict: Command effect path must stay inside the current root:/u],
19
20
  ['mustflow.command_contract.shared_writes_without_effects', /^Strict warning: configured agent-runnable intents .+ share path:.+ through writes without explicit effects or resource locks$/u],
20
- ['mustflow.command_contract.broad_env_inheritance', /^Strict warning: configured agent-runnable intent [^\s]+ (?:implicitly inherits the host environment|uses env_policy = "inherit")/u],
21
+ ['mustflow.command_contract.broad_env_inheritance', /^Strict(?: warning)?: configured agent-runnable intent [^\s]+ (?:implicitly inherits the host environment|uses env_policy = "inherit")/u],
21
22
  ['mustflow.command_contract.project_local_bin_bare_executable', /^Strict warning: configured agent-runnable intent [^\s]+ uses bare executable "[^"]+" that matches project-local node_modules\/\.bin/u],
22
23
  ['mustflow.prompt_cache.required', /^Strict: \[prompt_cache\] table is required$/u],
23
24
  ['mustflow.prompt_cache.volatile_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include volatile path /u],
@@ -48,6 +49,11 @@ const CHECK_ISSUE_ID_RULES = [
48
49
  ['mustflow.skill.route_metadata_category_mismatch', /^Strict: \.mustflow\/skills\/INDEX\.md route "[^"]+" must appear under the .+ category section from \.mustflow\/skills\/routes\.toml$/u],
49
50
  ['mustflow.skill.route_metadata_unknown_reference', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" references unknown mutually exclusive route "[^"]+"$/u],
50
51
  ['mustflow.skill.route_metadata_asymmetric_exclusion', /^Strict warning: \.mustflow\/skills\/routes\.toml route "[^"]+" lists "[^"]+" as mutually exclusive but the reverse route does not$/u],
52
+ ['mustflow.skill.template_profile_empty_category', /^Strict: template profile "[^"]+" (?:skill index category ".+" has no route rows|route category gate references ".+" without route rows)$/u],
53
+ ['mustflow.skill.template_profile_dead_route', /^Strict: template profile "[^"]+" (?:\.mustflow\/skills\/INDEX\.md route "[^"]+" points to a skill not installed by that profile|\.mustflow\/skills\/routes\.toml route "[^"]+" points to a skill not installed by that profile|skill "[^"]+" is installed but not listed in \.mustflow\/skills\/INDEX\.md)$/u],
54
+ ['mustflow.skill.template_profile_missing_main_route', /^Strict: template profile "[^"]+" skill category ".+" must include at least one primary or authoring route$/u],
55
+ ['mustflow.skill.template_profile_command_intent_drift', /^Strict: template profile "[^"]+" \.mustflow\/skills\/INDEX\.md route "[^"]+" references (?:unknown command intent "[^"]+"|command intent "[^"]+" not declared by the skill frontmatter)$/u],
56
+ ['mustflow.skill.template_profile_metadata_mismatch', /^Strict: template profile "[^"]+" (?:\.mustflow\/skills\/routes\.toml is missing metadata for route "[^"]+"|\.mustflow\/skills\/routes\.toml route "[^"]+" is not listed in \.mustflow\/skills\/INDEX\.md|\.mustflow\/skills\/INDEX\.md route "[^"]+" must appear under the .+ category section from \.mustflow\/skills\/routes\.toml)$/u],
51
57
  ['mustflow.skill.resource_unknown_command_intent', /^Strict: \.mustflow\/skills\/[^/]+\/resources\.toml script [^\s]+ references unknown command intent "[^"]+"$/u],
52
58
  ['mustflow.source_anchor.invalid_format', /^Strict: source anchor .+ has invalid format:/u],
53
59
  ['mustflow.source_anchor.duplicate_id', /^Strict: source anchor id "[^"]+" is duplicated:/u],
@@ -160,6 +160,7 @@ function validateCommandIntent(intentName, intent, issues) {
160
160
  validateAllowedStringField(intent, 'lifecycle', `[commands.intents.${intentName}].lifecycle`, COMMAND_LIFECYCLES, issues);
161
161
  validateAllowedStringField(intent, 'run_policy', `[commands.intents.${intentName}].run_policy`, COMMAND_RUN_POLICIES, issues);
162
162
  validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
163
+ validateBooleanField(intent, 'allow_shell', `[commands.intents.${intentName}].allow_shell`, issues);
163
164
  validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
164
165
  validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
165
166
  validatePositiveIntegerField(intent, 'kill_after_seconds', `[commands.intents.${intentName}].kill_after_seconds`, issues);
@@ -187,6 +188,9 @@ function validateCommandIntent(intentName, intent, issues) {
187
188
  if (lifecycle && LONG_RUNNING_LIFECYCLES.has(lifecycle) && runPolicy === 'agent_allowed') {
188
189
  issues.push(commandContractIssue(`Long-running intent ${intentName} must not use run_policy = "agent_allowed"`));
189
190
  }
191
+ if (intent.mode === 'shell' && runPolicy === 'agent_allowed' && intent.allow_shell !== true) {
192
+ issues.push(commandContractIssue(`Agent-runnable shell intent ${intentName} must set allow_shell = true`));
193
+ }
190
194
  if (!commandIntentHasCommandSource(intent)) {
191
195
  issues.push(commandContractIssue(`Configured intent ${intentName} must define argv or mode = "shell" with cmd`));
192
196
  }
@@ -238,26 +242,46 @@ export function findCommandEnvInheritanceWarnings(commandsToml) {
238
242
  if (envPolicy.policy !== 'inherit') {
239
243
  continue;
240
244
  }
245
+ const reasons = readCommandEnvInheritanceRiskReasons(intent);
241
246
  warnings.push({
242
247
  intentName,
243
248
  source: envPolicy.source,
249
+ severity: reasons.length > 0 ? 'error' : 'warning',
244
250
  network: intent.network === true,
251
+ reasons,
245
252
  });
246
253
  }
247
254
  return warnings;
248
255
  }
256
+ function readCommandEnvInheritanceRiskReasons(intent) {
257
+ const reasons = [];
258
+ if (intent.network === true) {
259
+ reasons.push('network = true');
260
+ }
261
+ if (intent.destructive === true) {
262
+ reasons.push('destructive = true');
263
+ }
264
+ if (intent.mode === 'shell') {
265
+ reasons.push('mode = "shell"');
266
+ }
267
+ if (Array.isArray(intent.writes) && intent.writes.length > 0) {
268
+ reasons.push('declared writes');
269
+ }
270
+ return reasons;
271
+ }
249
272
  function formatCommandEnvInheritanceWarning(warning) {
250
- const networkScope = warning.network ? ' with network = true' : '';
273
+ const reasonScope = warning.reasons.length > 0 ? ` (${warning.reasons.join(', ')})` : '';
251
274
  const migration = 'set env_policy = "minimal" or "allowlist" unless broad host state is required';
252
275
  if (warning.source === 'implicit') {
253
- return `configured agent-runnable intent ${warning.intentName} implicitly inherits the host environment${networkScope}; ${migration}`;
276
+ return `configured agent-runnable intent ${warning.intentName} implicitly inherits the host environment${reasonScope}; ${migration}`;
254
277
  }
255
- return `configured agent-runnable intent ${warning.intentName} uses env_policy = "inherit"${networkScope}; ${migration}`;
278
+ return `configured agent-runnable intent ${warning.intentName} uses env_policy = "inherit"${reasonScope}; ${migration}`;
256
279
  }
257
280
  function validateCommandEnvInheritanceWarnings(commandsToml) {
258
281
  const issues = [];
259
282
  for (const warning of findCommandEnvInheritanceWarnings(commandsToml)) {
260
- issues.push(commandContractWarning(formatCommandEnvInheritanceWarning(warning)));
283
+ const message = formatCommandEnvInheritanceWarning(warning);
284
+ issues.push(warning.severity === 'warning' ? commandContractWarning(message) : commandContractIssue(message));
261
285
  }
262
286
  return issues;
263
287
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { readString, readStringArray } from './config-loading.js';
3
3
  export const COMMAND_ENV_POLICIES = new Set(['inherit', 'minimal', 'allowlist']);
4
- export const DEFAULT_COMMAND_ENV_POLICY = 'inherit';
4
+ export const DEFAULT_COMMAND_ENV_POLICY = 'minimal';
5
5
  const BASE_MINIMAL_ENV_KEYS = [
6
6
  'CI',
7
7
  'COLORTERM',
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { readTomlFile } from './toml.js';
3
+ import { readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
4
+ import { parseTomlText } from './toml.js';
4
5
  export const COMMAND_LIFECYCLES = new Set(['oneshot', 'server', 'watch', 'interactive', 'browser', 'background']);
5
6
  export const LONG_RUNNING_LIFECYCLES = new Set(['server', 'watch', 'interactive', 'browser', 'background']);
6
7
  export const COMMAND_RUN_POLICIES = new Set(['agent_allowed', 'requires_explicit_user_request', 'manual_only']);
@@ -12,8 +13,13 @@ export function isRecord(value) {
12
13
  export function resolveMustflowConfigPath(projectRoot, relativePath) {
13
14
  return path.join(projectRoot, ...relativePath.split('/'));
14
15
  }
16
+ export function readMustflowOwnedTomlFile(projectRoot, relativePath) {
17
+ return parseTomlText(readUtf8FileInsideWithoutSymlinks(projectRoot, resolveMustflowConfigPath(projectRoot, relativePath), {
18
+ maxBytes: 256 * 1024,
19
+ }));
20
+ }
15
21
  export function readMustflowConfig(projectRoot) {
16
- const parsed = readTomlFile(resolveMustflowConfigPath(projectRoot, MUSTFLOW_CONFIG_RELATIVE_PATH));
22
+ const parsed = readMustflowOwnedTomlFile(projectRoot, MUSTFLOW_CONFIG_RELATIVE_PATH);
17
23
  if (!isRecord(parsed)) {
18
24
  throw new Error(`${MUSTFLOW_CONFIG_RELATIVE_PATH} must contain a TOML table`);
19
25
  }
@@ -24,7 +30,7 @@ export function readMustflowConfigIfExists(projectRoot) {
24
30
  return existsSync(configPath) ? readMustflowConfig(projectRoot) : undefined;
25
31
  }
26
32
  export function readCommandContract(projectRoot) {
27
- const parsed = readTomlFile(resolveMustflowConfigPath(projectRoot, COMMANDS_CONFIG_RELATIVE_PATH));
33
+ const parsed = readMustflowOwnedTomlFile(projectRoot, COMMANDS_CONFIG_RELATIVE_PATH);
28
34
  if (!isRecord(parsed)) {
29
35
  throw new Error(`${COMMANDS_CONFIG_RELATIVE_PATH} must contain a TOML table`);
30
36
  }
@@ -6,6 +6,7 @@ import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, comm
6
6
  import { MAX_COMMAND_OUTPUT_BYTES } from './command-output-limits.js';
7
7
  import { commandEffectsConflict, normalizeCommandEffects } from './command-effects.js';
8
8
  import { listChangeClassificationValidationReasons } from './change-classification.js';
9
+ import { readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
9
10
  import { parseSkillIndexRoutes } from './skill-route-alignment.js';
10
11
  import { SUCCESS_EXIT_CODES_CONTRACT_DESCRIPTION, successExitCodesAreValid as successExitCodeValuesAreValid, } from './success-exit-codes.js';
11
12
  const CONTRACT_LINT_SOURCE_FILES = [
@@ -13,6 +14,7 @@ const CONTRACT_LINT_SOURCE_FILES = [
13
14
  '.mustflow/docs/agent-workflow.md',
14
15
  'AGENTS.md',
15
16
  'src/core/change-classification.ts',
17
+ 'src/core/change-classification-policy.ts',
16
18
  ];
17
19
  export const DOCUMENTED_VERIFICATION_REASONS = [
18
20
  'before_publish',
@@ -48,7 +50,10 @@ const RELEASE_SENSITIVE_REASONS = new Set([
48
50
  ]);
49
51
  const COMMANDS_CONFIG_PATH = '.mustflow/config/commands.toml';
50
52
  const SKILL_INDEX_PATH = '.mustflow/skills/INDEX.md';
51
- const CHANGE_CLASSIFICATION_SOURCE_PATH = 'src/core/change-classification.ts';
53
+ const CHANGE_CLASSIFICATION_SOURCE_PATHS = [
54
+ 'src/core/change-classification.ts',
55
+ 'src/core/change-classification-policy.ts',
56
+ ];
52
57
  const AGENT_WORKFLOW_PATH = '.mustflow/docs/agent-workflow.md';
53
58
  const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
54
59
  const MAKEFILE_CANDIDATES = ['Makefile', 'makefile'];
@@ -392,7 +397,7 @@ function readSkillPathsByIntent(projectRoot) {
392
397
  if (!existsSync(skillIndexPath)) {
393
398
  return skillPathsByIntent;
394
399
  }
395
- const routes = parseSkillIndexRoutes(readFileSync(skillIndexPath, 'utf8'));
400
+ const routes = parseSkillIndexRoutes(readUtf8FileInsideWithoutSymlinks(projectRoot, skillIndexPath, { maxBytes: 1024 * 1024 }));
396
401
  for (const route of routes) {
397
402
  for (const intent of route.commandIntents) {
398
403
  skillPathsByIntent.set(intent, [...(skillPathsByIntent.get(intent) ?? []), route.skillPath]);
@@ -412,7 +417,7 @@ function classifyReasonSource(reason, knownClassificationReasons, documentedVeri
412
417
  function buildRelatedDocs(source, relatedSkills) {
413
418
  const docs = [COMMANDS_CONFIG_PATH];
414
419
  if (source === 'classification') {
415
- docs.push(CHANGE_CLASSIFICATION_SOURCE_PATH);
420
+ docs.push(...CHANGE_CLASSIFICATION_SOURCE_PATHS);
416
421
  }
417
422
  if (source === 'documented') {
418
423
  docs.push(AGENT_WORKFLOW_PATH);
@@ -0,0 +1,16 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ export const CORRELATION_ID_PATTERN = '^mf-[a-z][a-z0-9_-]*-[0-9a-f]{16}$';
3
+ const CORRELATION_ID_REGEX = new RegExp(CORRELATION_ID_PATTERN);
4
+ function normalizeCorrelationScope(scope) {
5
+ const normalized = scope
6
+ .toLowerCase()
7
+ .replace(/[^a-z0-9_-]+/g, '_')
8
+ .replace(/^_+|_+$/g, '');
9
+ return /^[a-z][a-z0-9_-]*$/.test(normalized) ? normalized : 'event';
10
+ }
11
+ export function createCorrelationId(scope) {
12
+ return `mf-${normalizeCorrelationScope(scope)}-${randomBytes(8).toString('hex')}`;
13
+ }
14
+ export function isCorrelationId(value) {
15
+ return typeof value === 'string' && CORRELATION_ID_REGEX.test(value);
16
+ }
@@ -225,6 +225,7 @@ export function createRunReceipt(input) {
225
225
  return {
226
226
  schema_version: RUN_RECEIPT_SCHEMA_VERSION,
227
227
  command: 'run',
228
+ correlation_id: input.correlationId,
228
229
  intent: input.intent,
229
230
  status: input.status,
230
231
  timed_out: input.timedOut,
@@ -1,4 +1,4 @@
1
- import { closeSync, constants, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
1
+ import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  const NOFOLLOW_FLAG = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
@@ -103,19 +103,26 @@ export function ensureFileTargetInsideWithoutSymlinks(parentPath, childPath, opt
103
103
  throw error;
104
104
  }
105
105
  }
106
- export function readFileInsideWithoutSymlinks(parentPath, childPath) {
106
+ export function readFileInsideWithoutSymlinks(parentPath, childPath, options = {}) {
107
107
  const absoluteChildPath = path.resolve(childPath);
108
108
  ensureInsideWithoutSymlinks(parentPath, absoluteChildPath);
109
109
  const fileDescriptor = openSync(absoluteChildPath, constants.O_RDONLY | NOFOLLOW_FLAG);
110
110
  try {
111
+ const stats = fstatSync(fileDescriptor);
112
+ if (!stats.isFile()) {
113
+ throw new Error(`Path must be a regular file: ${childPath}`);
114
+ }
115
+ if (options.maxBytes !== undefined && stats.size > options.maxBytes) {
116
+ throw new Error(`File exceeds maximum size ${options.maxBytes} bytes: ${childPath}`);
117
+ }
111
118
  return readFileSync(fileDescriptor);
112
119
  }
113
120
  finally {
114
121
  closeSync(fileDescriptor);
115
122
  }
116
123
  }
117
- export function readUtf8FileInsideWithoutSymlinks(parentPath, childPath) {
118
- return readFileInsideWithoutSymlinks(parentPath, childPath).toString('utf8');
124
+ export function readUtf8FileInsideWithoutSymlinks(parentPath, childPath, options = {}) {
125
+ return readFileInsideWithoutSymlinks(parentPath, childPath, options).toString('utf8');
119
126
  }
120
127
  export function writeFileInsideWithoutSymlinks(parentPath, childPath, content) {
121
128
  const absoluteChildPath = path.resolve(childPath);
@@ -172,6 +172,7 @@ export function isSkillRouteAlignmentIssue(issue) {
172
172
  return (issue.includes('.mustflow/skills/INDEX.md route') ||
173
173
  issue.includes('.mustflow/skills/INDEX.md .mustflow/skills/') ||
174
174
  issue.includes('.mustflow/skills/INDEX.md has duplicate route') ||
175
+ issue.includes('template profile "') ||
175
176
  issue.endsWith(' is not listed in .mustflow/skills/INDEX.md'));
176
177
  }
177
178
  export function summarizeSkillRouteAlignment(issues) {
@@ -1,6 +1,8 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
3
4
  import { parseSkillIndexRoutes } from './skill-route-alignment.js';
5
+ const MUSTFLOW_TEXT_MAX_BYTES = 1024 * 1024;
4
6
  const SKILL_INDEX_PATH = '.mustflow/skills/INDEX.md';
5
7
  const SKILL_ROUTE_SOURCE_FILES = [
6
8
  SKILL_INDEX_PATH,
@@ -84,11 +86,15 @@ function routeToSummary(route, skillContent) {
84
86
  }
85
87
  export function explainSkillRoute(projectRoot, target) {
86
88
  const indexPath = path.join(projectRoot, ...SKILL_INDEX_PATH.split('/'));
87
- const indexContent = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : '';
89
+ const indexContent = existsSync(indexPath)
90
+ ? readUtf8FileInsideWithoutSymlinks(projectRoot, indexPath, { maxBytes: MUSTFLOW_TEXT_MAX_BYTES })
91
+ : '';
88
92
  const routes = parseSkillIndexRoutes(indexContent);
89
93
  for (const route of routes) {
90
94
  const absoluteSkillPath = path.join(projectRoot, ...route.skillPath.split('/'));
91
- const skillContent = existsSync(absoluteSkillPath) ? readFileSync(absoluteSkillPath, 'utf8') : null;
95
+ const skillContent = existsSync(absoluteSkillPath)
96
+ ? readUtf8FileInsideWithoutSymlinks(projectRoot, absoluteSkillPath, { maxBytes: MUSTFLOW_TEXT_MAX_BYTES })
97
+ : null;
92
98
  if (!targetMatchesRoute(target, route, skillContent)) {
93
99
  continue;
94
100
  }
@@ -1,6 +1,5 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { isRecord, readStringArray, resolveMustflowConfigPath, } from './config-loading.js';
3
- import { readTomlFile } from './toml.js';
2
+ import { isRecord, readMustflowOwnedTomlFile, readStringArray, resolveMustflowConfigPath, } from './config-loading.js';
4
3
  import { classifyVerificationCandidate, } from './verification-plan.js';
5
4
  export const TEST_SELECTION_CONFIG_RELATIVE_PATH = '.mustflow/config/test-selection.toml';
6
5
  const STALE_OR_MISSING_RULES_NOTE = 'Project-declared test selection rules did not cover the current changed files; review .mustflow/config/test-selection.toml for stale or missing rules.';
@@ -73,7 +72,7 @@ function readRules(projectRoot) {
73
72
  };
74
73
  }
75
74
  try {
76
- const parsed = readTomlFile(configPath);
75
+ const parsed = readMustflowOwnedTomlFile(projectRoot, TEST_SELECTION_CONFIG_RELATIVE_PATH);
77
76
  if (!isRecord(parsed) || parsed.schema_version !== '1' || !Array.isArray(parsed.rules)) {
78
77
  return {
79
78
  status: 'invalid',
@@ -1,6 +1,7 @@
1
- import { readFileSync } from 'node:fs';
2
1
  import path from 'node:path';
3
2
  import { commandEffectsConflict, normalizeCommandEffects, } from './command-effects.js';
3
+ import { readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
4
+ const MUSTFLOW_JSON_MAX_BYTES = 1024 * 1024;
4
5
  function uniqueSorted(values) {
5
6
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
6
7
  }
@@ -18,9 +19,9 @@ function toScheduleEffect(effect) {
18
19
  function isObject(value) {
19
20
  return !!value && typeof value === 'object';
20
21
  }
21
- function readJsonFile(filePath) {
22
+ function readJsonFile(projectRoot, filePath) {
22
23
  try {
23
- return JSON.parse(readFileSync(filePath, 'utf8'));
24
+ return JSON.parse(readUtf8FileInsideWithoutSymlinks(projectRoot, filePath, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
24
25
  }
25
26
  catch {
26
27
  return null;
@@ -56,7 +57,7 @@ function readVerifyManifestUndeclaredWriteIntents(projectRoot, latest) {
56
57
  if (!manifestPath) {
57
58
  return new Set();
58
59
  }
59
- const manifest = readJsonFile(manifestPath);
60
+ const manifest = readJsonFile(projectRoot, manifestPath);
60
61
  if (!isObject(manifest) || manifest.command !== 'verify' || !Array.isArray(manifest.receipts)) {
61
62
  return new Set();
62
63
  }
@@ -69,7 +70,7 @@ function readVerifyManifestUndeclaredWriteIntents(projectRoot, latest) {
69
70
  if (!receiptPath) {
70
71
  continue;
71
72
  }
72
- const intent = getUndeclaredWriteIntent(readJsonFile(receiptPath));
73
+ const intent = getUndeclaredWriteIntent(readJsonFile(projectRoot, receiptPath));
73
74
  if (intent) {
74
75
  intents.add(intent);
75
76
  }
@@ -78,7 +79,7 @@ function readVerifyManifestUndeclaredWriteIntents(projectRoot, latest) {
78
79
  }
79
80
  function readLatestUndeclaredWriteIntents(projectRoot) {
80
81
  const latestPath = path.join(projectRoot, '.mustflow', 'state', 'runs', 'latest.json');
81
- const parsed = readJsonFile(latestPath);
82
+ const parsed = readJsonFile(projectRoot, latestPath);
82
83
  const directIntent = getUndeclaredWriteIntent(parsed);
83
84
  if (directIntent) {
84
85
  return new Set([directIntent]);
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { isRecord } from './config-loading.js';
4
- import { readTomlFile } from './toml.js';
3
+ import { isRecord, readMustflowOwnedTomlFile } from './config-loading.js';
5
4
  const RELEASE_VERSIONING_ACTIVE_FIELDS = [
6
5
  'impact_check',
7
6
  'suggest_bump',
@@ -155,7 +154,7 @@ function hasGoModuleVersionSource(projectRoot) {
155
154
  }
156
155
  export function readDeclaredVersionSources(projectRoot) {
157
156
  try {
158
- const versioningConfig = readTomlFile(path.join(projectRoot, VERSIONING_CONFIG_PATH));
157
+ const versioningConfig = readMustflowOwnedTomlFile(projectRoot, VERSIONING_CONFIG_PATH);
159
158
  if (!isRecord(versioningConfig) || !Array.isArray(versioningConfig.sources)) {
160
159
  return [];
161
160
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.22.4",
3
+ "version": "2.22.9",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -36,10 +36,13 @@
36
36
  "test:coverage": "bun run build && node scripts/run-cli-tests.mjs coverage",
37
37
  "test:audit": "node scripts/audit-tests.mjs --json",
38
38
  "test:release": "bun run build && node scripts/run-cli-tests.mjs release",
39
+ "test:fast:node": "npm run build && node scripts/run-cli-tests.mjs fast",
40
+ "test:release:node": "npm run build && node scripts/run-cli-tests.mjs release",
39
41
  "test:full": "bun run build && node scripts/run-cli-tests.mjs full-auto",
40
42
  "test:full:auto": "bun run build && node scripts/run-cli-tests.mjs full-auto",
41
43
  "test:full:serial": "bun run build && node scripts/run-cli-tests.mjs full",
42
44
  "check": "bun run check:package && bun run test:full",
45
+ "check:core:node": "node scripts/run-node-core-check.mjs",
43
46
  "check:package": "node -e \"const fs=require('fs'); JSON.parse(fs.readFileSync('package.json','utf8')); console.log('package.json ok')\"",
44
47
  "check:typecheck": "tsc -p tsconfig.json --noEmit",
45
48
  "prepack": "npm run build",
package/schemas/README.md CHANGED
@@ -48,6 +48,10 @@ Current schemas:
48
48
  These schemas define stable, automation-facing fields. Human-readable command
49
49
  output is intentionally excluded.
50
50
 
51
+ Current `classify`, `verify`, `run`, dashboard export, and verify state outputs may include
52
+ `correlation_id` so local artifacts from one work incident can be connected without storing raw
53
+ transcripts, environment values, or hidden reasoning.
54
+
51
55
  The published schema surface is tracked in `src/core/public-json-contracts.ts`.
52
56
  Release tests verify consistency between this manifest, `schemas/*.schema.json`,
53
57
  `npm pack --dry-run --json`, and the installed package’s JSON command output.
@@ -29,6 +29,10 @@
29
29
  "classification_summary": {
30
30
  "$ref": "#/$defs/classificationSummary"
31
31
  },
32
+ "correlation_id": {
33
+ "type": "string",
34
+ "pattern": "^mf-[a-z][a-z0-9_-]*-[0-9a-f]{16}$"
35
+ },
32
36
  "verification_plan_id": {
33
37
  "type": "string",
34
38
  "pattern": "^sha256:[0-9a-f]{64}$"
@@ -16,6 +16,10 @@
16
16
  "properties": {
17
17
  "schema_version": { "const": "1" },
18
18
  "command": { "const": "classify" },
19
+ "correlation_id": {
20
+ "type": "string",
21
+ "pattern": "^mf-[a-z][a-z0-9_-]*-[0-9a-f]{16}$"
22
+ },
19
23
  "mustflow_root": { "type": "string" },
20
24
  "source": { "enum": ["changed", "paths"] },
21
25
  "files": {
@@ -135,6 +135,7 @@
135
135
  "items": { "type": "string" }
136
136
  },
137
137
  "cmd": { "type": "string" },
138
+ "allow_shell": { "type": "boolean" },
138
139
  "cwd": { "type": "string" },
139
140
  "timeout_seconds": { "type": "integer" },
140
141
  "kill_after_seconds": { "type": "integer" },
@@ -20,6 +20,10 @@
20
20
  "properties": {
21
21
  "schema_version": { "const": "1" },
22
22
  "command": { "const": "dashboard export" },
23
+ "correlation_id": {
24
+ "type": "string",
25
+ "pattern": "^mf-[a-z][a-z0-9_-]*-[0-9a-f]{16}$"
26
+ },
23
27
  "format": { "enum": ["html", "json"] },
24
28
  "generated_at": { "type": "string" },
25
29
  "mustflow_root": { "type": "string" },
@@ -23,6 +23,10 @@
23
23
  "schema_version": { "const": "1" },
24
24
  "command": { "const": "verify" },
25
25
  "kind": { "const": "verify_run_summary" },
26
+ "correlation_id": {
27
+ "type": "string",
28
+ "pattern": "^mf-[a-z][a-z0-9_-]*-[0-9a-f]{16}$"
29
+ },
26
30
  "reason": { "type": "string" },
27
31
  "reasons": {
28
32
  "type": "array",
@@ -34,6 +34,10 @@
34
34
  "properties": {
35
35
  "schema_version": { "const": "1" },
36
36
  "command": { "const": "run" },
37
+ "correlation_id": {
38
+ "type": "string",
39
+ "pattern": "^mf-[a-z][a-z0-9_-]*-[0-9a-f]{16}$"
40
+ },
37
41
  "intent": { "type": "string" },
38
42
  "status": { "enum": ["passed", "failed", "timed_out", "start_failed", "output_limit_exceeded"] },
39
43
  "timed_out": { "type": "boolean" },
@@ -23,6 +23,10 @@
23
23
  "properties": {
24
24
  "schema_version": { "const": "1" },
25
25
  "command": { "const": "verify" },
26
+ "correlation_id": {
27
+ "type": "string",
28
+ "pattern": "^mf-[a-z][a-z0-9_-]*-[0-9a-f]{16}$"
29
+ },
26
30
  "mustflow_root": { "type": "string" },
27
31
  "reason": { "type": "string" },
28
32
  "reasons": {
@@ -20,6 +20,10 @@
20
20
  "properties": {
21
21
  "schema_version": { "const": "1" },
22
22
  "command": { "const": "verify" },
23
+ "correlation_id": {
24
+ "type": "string",
25
+ "pattern": "^mf-[a-z][a-z0-9_-]*-[0-9a-f]{16}$"
26
+ },
23
27
  "reason": { "type": "string" },
24
28
  "reasons": {
25
29
  "type": "array",
@@ -74,7 +74,7 @@ translations = {}
74
74
  [documents."skill.architecture-deepening-review"]
75
75
  source = "locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md"
76
76
  source_locale = "en"
77
- revision = 1
77
+ revision = 2
78
78
  translations = {}
79
79
 
80
80
  [documents."skill.behavior-preserving-refactor"]
@@ -325,7 +325,7 @@ translations = {}
325
325
  [documents."skill.security-privacy-review"]
326
326
  source = "locales/en/.mustflow/skills/security-privacy-review/SKILL.md"
327
327
  source_locale = "en"
328
- revision = 16
328
+ revision = 17
329
329
  translations = {}
330
330
 
331
331
  [documents."skill.security-regression-tests"]
@@ -349,7 +349,7 @@ translations = {}
349
349
  [documents."skill.test-design-guard"]
350
350
  source = "locales/en/.mustflow/skills/test-design-guard/SKILL.md"
351
351
  source_locale = "en"
352
- revision = 1
352
+ revision = 2
353
353
  translations = {}
354
354
 
355
355
  [documents."skill.test-maintenance"]