mustflow 2.58.1 → 2.68.6

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 (47) hide show
  1. package/README.md +4 -0
  2. package/dist/cli/commands/context.js +81 -6
  3. package/dist/cli/commands/quality.js +89 -0
  4. package/dist/cli/commands/skill.js +116 -0
  5. package/dist/cli/i18n/en.js +16 -0
  6. package/dist/cli/i18n/es.js +16 -0
  7. package/dist/cli/i18n/fr.js +16 -0
  8. package/dist/cli/i18n/hi.js +16 -0
  9. package/dist/cli/i18n/ko.js +16 -0
  10. package/dist/cli/i18n/zh.js +16 -0
  11. package/dist/cli/index.js +1 -0
  12. package/dist/cli/lib/agent-context.js +981 -8
  13. package/dist/cli/lib/command-registry.js +12 -0
  14. package/dist/cli/lib/local-index/constants.js +4 -5
  15. package/dist/cli/lib/local-index/freshness.js +5 -1
  16. package/dist/cli/lib/local-index/index.js +1 -1
  17. package/dist/cli/lib/repo-map.js +7 -1
  18. package/dist/cli/lib/validation/constants.js +3 -0
  19. package/dist/cli/lib/validation/index.js +41 -2
  20. package/dist/core/check-issues.js +5 -0
  21. package/dist/core/prompt-cache-rendering.js +19 -0
  22. package/dist/core/public-json-contracts.js +35 -0
  23. package/dist/core/quality-gaming.js +304 -0
  24. package/dist/core/skill-route-fixtures.js +173 -0
  25. package/dist/core/skill-route-resolution.js +398 -0
  26. package/dist/core/source-anchors.js +91 -5
  27. package/package.json +1 -1
  28. package/schemas/README.md +9 -1
  29. package/schemas/context-report.schema.json +442 -0
  30. package/schemas/quality-gaming-report.schema.json +96 -0
  31. package/schemas/route-fixture.schema.json +57 -0
  32. package/schemas/skill-route-report.schema.json +242 -0
  33. package/templates/default/common/.mustflow/config/commands.toml +34 -2
  34. package/templates/default/common/.mustflow/config/mustflow.toml +16 -10
  35. package/templates/default/i18n.toml +14 -8
  36. package/templates/default/locales/en/.mustflow/context/PROJECT.md +5 -3
  37. package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +13 -9
  38. package/templates/default/locales/en/.mustflow/skills/INDEX.md +3 -2
  39. package/templates/default/locales/en/.mustflow/skills/llm-token-cost-control-review/SKILL.md +5 -2
  40. package/templates/default/locales/en/.mustflow/skills/quality-gaming-guard/SKILL.md +165 -0
  41. package/templates/default/locales/en/.mustflow/skills/router.toml +67 -0
  42. package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
  43. package/templates/default/locales/en/AGENTS.md +15 -7
  44. package/templates/default/locales/ko/.mustflow/context/PROJECT.md +4 -2
  45. package/templates/default/locales/ko/.mustflow/docs/agent-workflow.md +18 -12
  46. package/templates/default/locales/ko/AGENTS.md +8 -6
  47. package/templates/default/manifest.toml +9 -1
@@ -89,6 +89,12 @@ export const COMMAND_DEFINITIONS = [
89
89
  summaryKey: 'command.lineEndings.summary',
90
90
  loadRunner: async () => (await import('../commands/line-endings.js')).runLineEndings,
91
91
  },
92
+ {
93
+ id: 'quality',
94
+ usage: 'mf quality',
95
+ summaryKey: 'command.quality.summary',
96
+ loadRunner: async () => (await import('../commands/quality.js')).runQuality,
97
+ },
92
98
  {
93
99
  id: 'run',
94
100
  usage: 'mf run',
@@ -137,6 +143,12 @@ export const COMMAND_DEFINITIONS = [
137
143
  summaryKey: 'command.search.summary',
138
144
  loadRunner: async () => (await import('../commands/search.js')).runSearch,
139
145
  },
146
+ {
147
+ id: 'skill',
148
+ usage: 'mf skill',
149
+ summaryKey: 'command.skill.summary',
150
+ loadRunner: async () => (await import('../commands/skill.js')).runSkill,
151
+ },
140
152
  {
141
153
  id: 'dashboard',
142
154
  usage: 'mf dashboard',
@@ -32,14 +32,13 @@ export const MUSTFLOW_RELATIVE_PATH = '.mustflow/config/mustflow.toml';
32
32
  export const INDEX_CONFIG_RELATIVE_PATH = '.mustflow/config/index.toml';
33
33
  export const DEFAULT_PROMPT_CACHE_STABLE_READ = [
34
34
  'AGENTS.md',
35
- '.mustflow/docs/agent-workflow.md',
36
- '.mustflow/config/mustflow.toml',
37
- '.mustflow/config/commands.toml',
38
- '.mustflow/skills/INDEX.md',
35
+ '.mustflow/skills/router.toml',
39
36
  ];
40
37
  export const DEFAULT_PROMPT_CACHE_TASK_SOURCES = [
41
38
  '.mustflow/context/INDEX.md',
42
- 'REPO_MAP.md',
39
+ 'skill_route_candidates',
40
+ 'route_metadata_fallback',
41
+ 'repo_map_navigation',
43
42
  'matching_skill',
44
43
  'relevant_source_files',
45
44
  ];
@@ -5,8 +5,9 @@ import { collectDocuments } from './workflow-documents.js';
5
5
  export function readStoredSchemaVersion(database) {
6
6
  return readMetadataValue(database, 'schema_version');
7
7
  }
8
- export function getStalePaths(projectRoot, database) {
8
+ export function getStalePaths(projectRoot, database, options = {}) {
9
9
  const schemaVersion = readStoredSchemaVersion(database);
10
+ const includeState = options.includeState ?? true;
10
11
  if (schemaVersion !== LOCAL_INDEX_SCHEMA_VERSION) {
11
12
  return ['.mustflow/cache/mustflow.sqlite'];
12
13
  }
@@ -17,6 +18,9 @@ export function getStalePaths(projectRoot, database) {
17
18
  for (const row of indexedRows) {
18
19
  const indexedPath = toSearchString(row.path);
19
20
  const sourceScope = normalizeIndexedFileSourceScope(toSearchString(row.source_scope));
21
+ if (!includeState && sourceScope === 'state') {
22
+ continue;
23
+ }
20
24
  try {
21
25
  const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
22
26
  if (current.contentHash !== toSearchString(row.content_hash)) {
@@ -383,7 +383,7 @@ export async function readLocalIndexPromptContext(projectRoot) {
383
383
  const SQL = await loadSqlJs();
384
384
  database = new SQL.Database(readFileSync(databasePath));
385
385
  const capabilities = readStoredSearchCapabilities(database);
386
- const stalePaths = getStalePaths(projectRoot, database);
386
+ const stalePaths = getStalePaths(projectRoot, database, { includeState: false });
387
387
  if (stalePaths.length > 0) {
388
388
  return createLocalIndexPromptContextStatus(databasePath, 'stale', stalePaths, capabilities);
389
389
  }
@@ -28,6 +28,8 @@ const DEFAULT_PRIORITY_PATHS = [
28
28
  '.mustflow/config/commands.toml',
29
29
  '.mustflow/config/preferences.toml',
30
30
  '.mustflow/context/INDEX.md',
31
+ '.mustflow/skills/router.toml',
32
+ '.mustflow/skills/routes.toml',
31
33
  '.mustflow/skills/INDEX.md',
32
34
  ];
33
35
  const ROOT_OPTIONAL_MARKDOWN_ANCHOR_FILES = [
@@ -81,6 +83,8 @@ const DEFAULT_NESTED_ANCHOR_FILES = [
81
83
  '.mustflow/config/preferences.toml',
82
84
  '.mustflow/context/INDEX.md',
83
85
  '.mustflow/context/PROJECT.md',
86
+ '.mustflow/skills/router.toml',
87
+ '.mustflow/skills/routes.toml',
84
88
  '.mustflow/skills/INDEX.md',
85
89
  ...ROOT_OPTIONAL_MARKDOWN_ANCHOR_FILES,
86
90
  ...MACHINE_CONTRACT_ANCHOR_FILES,
@@ -191,7 +195,9 @@ const EXACT_ANCHOR_DESCRIPTIONS = new Map([
191
195
  ['.mustflow/context/INDEX.md', 'Task-specific project context router. Read only when context is needed.'],
192
196
  ['.mustflow/context/PROJECT.md', 'Project goals, non-goals, terms, and repository-wide promises for agents.'],
193
197
  ['.mustflow/docs/agent-workflow.md', 'Shared workflow policy for agent work.'],
194
- ['.mustflow/skills/INDEX.md', 'Index of available procedural skills.'],
198
+ ['.mustflow/skills/router.toml', 'Stable compact skill-routing kernel for prompt-cache-friendly first-pass selection.'],
199
+ ['.mustflow/skills/routes.toml', 'Full skill-routing metadata for detailed procedure selection.'],
200
+ ['.mustflow/skills/INDEX.md', 'Expanded route table for detailed skill selection and route maintenance.'],
195
201
  ]);
196
202
  function spawnGitLsFiles(command, args, options) {
197
203
  const result = spawnSync(command, [...args], options);
@@ -21,6 +21,8 @@ export const REQUIRED_FILES = [
21
21
  'AGENTS.md',
22
22
  '.mustflow/config/mustflow.toml',
23
23
  '.mustflow/config/commands.toml',
24
+ '.mustflow/skills/router.toml',
25
+ '.mustflow/skills/routes.toml',
24
26
  '.mustflow/skills/INDEX.md',
25
27
  ];
26
28
  export const ALLOWED_MAP_MODES = new Set(['anchors_only']);
@@ -181,6 +183,7 @@ export const SKILL_COMMAND_PERMISSION_CLAIM_PATTERNS = [
181
183
  export const ROUTER_INDEX_PROCEDURE_SECTION_PATTERN = /^##\s+(?:Use When|Do Not Use When|Required Inputs|Preconditions|Allowed Edits|Procedure|Postconditions|Verification|Failure Handling|Output Format|사용 조건|사용하지 않는 경우|필요한 입력|사전 조건|허용 수정 범위|절차|사후 조건|검증|실패 대응|출력 형식)\s*$/imu;
182
184
  export const ROUTER_INDEX_FILES = ['.mustflow/skills/INDEX.md', '.mustflow/context/INDEX.md'];
183
185
  export const SKILL_INDEX_PATH = '.mustflow/skills/INDEX.md';
186
+ export const SKILL_ROUTER_PATH = '.mustflow/skills/router.toml';
184
187
  export const SKILL_ROUTES_METADATA_PATH = '.mustflow/skills/routes.toml';
185
188
  export const SUPPORTED_SKILL_SCHEMA_VERSION = '1';
186
189
  export const SKILL_PACK_ID_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/u;
@@ -17,6 +17,8 @@ import { MUSTFLOW_JSON_MAX_BYTES } from '../mustflow-read.js';
17
17
  import { getContractModelDefinitions, validateCandidateContractModelConfig, } from '../../../core/contract-models.js';
18
18
  import { VERSIONING_CONFIG_PATH, detectVersionSourcePaths, readDeclaredVersionSources, releaseVersioningIsEnabled, } from '../../../core/version-sources.js';
19
19
  import { normalizeTechnologyPreferencesTable, TECHNOLOGY_CONFIG_RELATIVE_PATH, } from '../../../core/technology-preferences.js';
20
+ import { isPromptCacheStableLeafSkillSurface, measurePromptCacheReferenceBlockBytes, } from '../../../core/prompt-cache-rendering.js';
21
+ import { validateSkillRouteFixtures } from '../../../core/skill-route-fixtures.js';
20
22
  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_REPO_MAP_DEGRADED_VALUES, ALLOWED_REPO_MAP_GIT_LS_FILES_STATUSES, 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, SUPPORTED_SKILL_SCHEMA_VERSION, TEST_AUTHORING_BOOLEAN_FIELDS, VERIFICATION_SELECTION_BOOLEAN_FIELDS, VOLATILE_REPO_MAP_PATTERNS } from './constants.js';
21
23
  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';
22
24
  import { isConfiguredCommandIntent, isDeclaredCommandIntent } from './command-intents.js';
@@ -63,6 +65,7 @@ function validateMustflowConfig(mustflowToml, issues) {
63
65
  const stable = validateNestedTable(layers, 'stable', '[prompt_cache.layers.stable]', issues);
64
66
  if (stable) {
65
67
  validatePathArrayField(stable, 'read', '[prompt_cache.layers.stable].read', issues);
68
+ validatePositiveIntegerField(stable, 'target_kb', '[prompt_cache.layers.stable].target_kb', issues);
66
69
  }
67
70
  const task = validateNestedTable(layers, 'task', '[prompt_cache.layers.task]', issues);
68
71
  if (task) {
@@ -511,7 +514,33 @@ function validateManifestLock(projectRoot, issues) {
511
514
  issues.push({ message: issue });
512
515
  }
513
516
  }
514
- function validateStrictPromptCachePolicy(mustflowToml, issues) {
517
+ function validateStrictStablePromptCacheBudget(projectRoot, promptCache, stableRead, issues) {
518
+ if (!isPositiveInteger(promptCache.max_stable_prefix_kb)) {
519
+ return;
520
+ }
521
+ const budgetBytes = Number(promptCache.max_stable_prefix_kb) * 1024;
522
+ let renderedBytes = 0;
523
+ for (const entry of stableRead) {
524
+ if (typeof entry !== 'string' || !isSafeRelativePath(entry)) {
525
+ continue;
526
+ }
527
+ const normalizedPath = toPosixPath(entry);
528
+ const absolutePath = path.join(projectRoot, entry);
529
+ if (!existsSync(absolutePath)) {
530
+ pushStrictIssue(issues, `stable prefix document is missing: ${normalizedPath}`);
531
+ continue;
532
+ }
533
+ const content = readStrictMustflowText(projectRoot, entry, issues);
534
+ if (content === undefined) {
535
+ continue;
536
+ }
537
+ renderedBytes += measurePromptCacheReferenceBlockBytes(entry, content);
538
+ }
539
+ if (renderedBytes > budgetBytes) {
540
+ pushStrictIssue(issues, `stable prefix exceeds [prompt_cache].max_stable_prefix_kb: ${renderedBytes} rendered bytes > ${budgetBytes} budget bytes`);
541
+ }
542
+ }
543
+ function validateStrictPromptCachePolicy(projectRoot, mustflowToml, issues) {
515
544
  if (!mustflowToml || !isRecord(mustflowToml.prompt_cache)) {
516
545
  pushStrictIssue(issues, '[prompt_cache] table is required');
517
546
  return;
@@ -534,7 +563,11 @@ function validateStrictPromptCachePolicy(mustflowToml, issues) {
534
563
  if (typeof entry === 'string' && volatileSources.has(entry)) {
535
564
  pushStrictIssue(issues, `[prompt_cache.layers.stable].read must not include volatile path "${entry}"`);
536
565
  }
566
+ if (typeof entry === 'string' && isPromptCacheStableLeafSkillSurface(entry)) {
567
+ pushStrictIssue(issues, `[prompt_cache.layers.stable].read must not include leaf skill or expanded route surface "${toPosixPath(entry)}"`);
568
+ }
537
569
  }
570
+ validateStrictStablePromptCacheBudget(projectRoot, promptCache, stable.read, issues);
538
571
  if (promptCache.exclude_volatile_state_from_prefix !== true) {
539
572
  pushStrictIssue(issues, '[prompt_cache].exclude_volatile_state_from_prefix should be true');
540
573
  }
@@ -590,6 +623,11 @@ function validateStrictRouterIndexes(projectRoot, issues) {
590
623
  }
591
624
  }
592
625
  }
626
+ function validateStrictSkillRouteFixtures(projectRoot, issues) {
627
+ for (const issue of validateSkillRouteFixtures(projectRoot)) {
628
+ pushStrictIssue(issues, issue.message);
629
+ }
630
+ }
593
631
  function validateSkillIndexRouteShape(content, issues) {
594
632
  for (const line of content.split(/\r?\n/u)) {
595
633
  if (!line.trim().startsWith('|')) {
@@ -1536,7 +1574,7 @@ function validateStrictStorage(projectRoot, limits, issues) {
1536
1574
  }
1537
1575
  function validateStrict(projectRoot, parsed, issues) {
1538
1576
  const retentionLimits = validateStrictRetentionPolicy(parsed.mustflowToml, issues);
1539
- validateStrictPromptCachePolicy(parsed.mustflowToml, issues);
1577
+ validateStrictPromptCachePolicy(projectRoot, parsed.mustflowToml, issues);
1540
1578
  validateStrictRefreshPolicy(parsed.mustflowToml, issues);
1541
1579
  validateStrictHarnessPolicy(parsed.mustflowToml, issues);
1542
1580
  validateStrictCommandDefaults(projectRoot, parsed.commandsToml, issues);
@@ -1548,6 +1586,7 @@ function validateStrict(projectRoot, parsed, issues) {
1548
1586
  validateStrictTemplateVersionSync(projectRoot, parsed.preferencesToml, issues);
1549
1587
  validateStrictManagedMarkdownIdentities(projectRoot, issues);
1550
1588
  validateStrictRouterIndexes(projectRoot, issues);
1589
+ validateStrictSkillRouteFixtures(projectRoot, issues);
1551
1590
  validateStrictSkills(projectRoot, parsed.commandsToml, issues);
1552
1591
  validateStrictTemplateSkillProfiles(issues);
1553
1592
  validateStrictRepoMap(projectRoot, issues);
@@ -30,6 +30,9 @@ const CHECK_ISSUE_ID_RULES = [
30
30
  ['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],
31
31
  ['mustflow.prompt_cache.required', /^Strict: \[prompt_cache\] table is required$/u],
32
32
  ['mustflow.prompt_cache.volatile_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include volatile path /u],
33
+ ['mustflow.prompt_cache.leaf_skill_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include leaf skill or expanded route surface /u],
34
+ ['mustflow.prompt_cache.stable_file_missing', /^Strict: stable prefix document is missing: /u],
35
+ ['mustflow.prompt_cache.stable_prefix_over_budget', /^Strict: stable prefix exceeds \[prompt_cache\]\.max_stable_prefix_kb: \d+ rendered bytes > \d+ budget bytes$/u],
33
36
  ['mustflow.refresh.hash_method_required', /^Strict: \[refresh\]\.default_method should be "hash_check" for cache-friendly refresh$/u],
34
37
  ['mustflow.release.template_version_mismatch', /^Strict: templates\/default\/manifest\.toml version "[^"]+" must match package\.json version "[^"]+" when \[release\.versioning\]\.sync_template_version is true$/u],
35
38
  ['mustflow.release.template_version_ahead', /^Strict: templates\/default\/manifest\.toml version "[^"]+" must not be ahead of package\.json version "[^"]+"$/u],
@@ -57,6 +60,8 @@ const CHECK_ISSUE_ID_RULES = [
57
60
  ['mustflow.skill.route_metadata_category_mismatch', /^Strict: \.mustflow\/skills\/INDEX\.md route "[^"]+" must appear under the .+ category section from \.mustflow\/skills\/routes\.toml$/u],
58
61
  ['mustflow.skill.route_metadata_unknown_reference', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" references unknown mutually exclusive route "[^"]+"$/u],
59
62
  ['mustflow.skill.route_metadata_asymmetric_exclusion', /^Strict warning: \.mustflow\/skills\/routes\.toml route "[^"]+" lists "[^"]+" as mutually exclusive but the reverse route does not$/u],
63
+ ['mustflow.skill.route_fixture_invalid', /^Strict: \.mustflow\/skills\/route-fixtures\.json (?:must|is not valid JSON|cases\[\d+\])/u],
64
+ ['mustflow.skill.route_fixture_mismatch', /^Strict: Skill route fixture "[^"]+" (?:expected|forbids)/u],
60
65
  ['mustflow.skill.template_profile_empty_category', /^Strict: template profile "[^"]+" (?:skill index category ".+" has no route rows|route category gate references ".+" without route rows)$/u],
61
66
  ['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],
62
67
  ['mustflow.skill.template_profile_missing_main_route', /^Strict: template profile "[^"]+" skill category ".+" must include at least one primary or authoring route$/u],
@@ -0,0 +1,19 @@
1
+ function toPromptCachePosixPath(relativePath) {
2
+ return relativePath.replace(/\\/gu, '/');
3
+ }
4
+ export function isPromptCacheStableLeafSkillSurface(relativePath) {
5
+ const normalized = toPromptCachePosixPath(relativePath);
6
+ return normalized === '.mustflow/skills/INDEX.md'
7
+ || normalized === '.mustflow/skills/routes.toml'
8
+ || /^\.mustflow\/skills\/[^/]+\/.+/u.test(normalized);
9
+ }
10
+ export function normalizePromptCacheBlockContent(content) {
11
+ const normalized = content.replace(/\r\n?/gu, '\n').replace(/\n*$/u, '');
12
+ return `${normalized}\n`;
13
+ }
14
+ export function renderPromptCacheReferenceBlock(relativePath, content) {
15
+ return `--- mustflow-cache-block: ${toPromptCachePosixPath(relativePath)} ---\n${normalizePromptCacheBlockContent(content)}`;
16
+ }
17
+ export function measurePromptCacheReferenceBlockBytes(relativePath, content) {
18
+ return Buffer.byteLength(renderPromptCacheReferenceBlock(relativePath, content), 'utf8');
19
+ }
@@ -198,6 +198,41 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
198
198
  installedCommand: ['mf', 'line-endings', 'check', '--json'],
199
199
  expectedExitCodes: [0, 1],
200
200
  },
201
+ {
202
+ id: 'quality-gaming-report',
203
+ schemaFile: 'quality-gaming-report.schema.json',
204
+ producer: 'mf quality check --json',
205
+ packaged: true,
206
+ documented: true,
207
+ installedCommand: ['mf', 'quality', 'check', '--json'],
208
+ expectedExitCodes: [0, 1],
209
+ },
210
+ {
211
+ id: 'skill-route-report',
212
+ schemaFile: 'skill-route-report.schema.json',
213
+ producer: 'mf skill route --json',
214
+ packaged: true,
215
+ documented: true,
216
+ installedCommand: [
217
+ 'mf',
218
+ 'skill',
219
+ 'route',
220
+ '--task',
221
+ 'Change TypeScript CLI output',
222
+ '--path',
223
+ 'src/cli/index.ts',
224
+ '--reason',
225
+ 'code_change',
226
+ '--json',
227
+ ],
228
+ },
229
+ {
230
+ id: 'route-fixture',
231
+ schemaFile: 'route-fixture.schema.json',
232
+ producer: 'parsed .mustflow/skills/route-fixtures.json',
233
+ packaged: true,
234
+ documented: true,
235
+ },
201
236
  {
202
237
  id: 'latest-run-pointer',
203
238
  schemaFile: 'latest-run-pointer.schema.json',
@@ -0,0 +1,304 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createCommandEnv } from './command-env.js';
5
+ import { readFileInsideWithoutSymlinks } from './safe-filesystem.js';
6
+ const LONG_LINE_THRESHOLD = 160;
7
+ const MULTI_STATEMENT_SEMICOLON_THRESHOLD = 1;
8
+ const LARGE_HELPER_LINE_THRESHOLD = 120;
9
+ const TEXT_FILE_PATTERN = /\.(?:cjs|cts|css|go|h|hpp|html|java|js|jsx|json|kt|md|mjs|mts|php|ps1|py|rb|rs|scss|sh|sql|svelte|swift|toml|ts|tsx|txt|vue|xml|yaml|yml)$/iu;
10
+ const CODE_FILE_PATTERN = /\.(?:cjs|cts|go|java|js|jsx|kt|mjs|mts|php|ps1|py|rb|rs|svelte|swift|ts|tsx|vue)$/iu;
11
+ const GENERATED_OR_VENDOR_PATTERN = /(?:^|\/)(?:generated|vendor|third_party)(?:\/|$)/iu;
12
+ const HELPER_CONTAINER_PATTERN = /(?:^|\/)[^/]*(?:helper|helpers|util|utils|manager|common|misc)[^/]*\.(?:cjs|cts|go|java|js|jsx|kt|mjs|mts|php|ps1|py|rb|rs|svelte|swift|ts|tsx|vue)$/iu;
13
+ const SUPPRESSION_PATTERN = /(?:eslint-disable|ts-ignore|ts-expect-error|biome-ignore|rome-ignore|noqa|type:\s*ignore|pragma:\s*no cover|istanbul\s+ignore|c8\s+ignore)/iu;
14
+ const TYPE_ESCAPE_PATTERN = /\b(?:as\s+any|as\s+unknown\s+as|as\s+never|:\s*any\b|!\.|!\)|!\]|!\s*[,;]|\bObject\s+as\s+unknown)\b/u;
15
+ const TEST_BYPASS_PATTERN = /(?:\.skip\b|\.only\b|@Disabled\b|@Ignore\b|\bxfail\b|pytest\.mark\.skip|it\.todo\b|test\.todo\b)/u;
16
+ const PLACEHOLDER_PATTERN = /(?:TODO|FIXME|HACK|not\s+implemented|unimplemented|throw\s+new\s+Error\(["'`](?:not implemented|todo|unsupported)|\bpass\s*$)/iu;
17
+ const THROW_PLACEHOLDER_MESSAGE_PATTERN = /(?:not\s+implemented|unimplemented|todo|unsupported)/iu;
18
+ const EMPTY_CATCH_PATTERN = /\bcatch\s*(?:\([^)]*\))?\s*\{\s*\}/u;
19
+ function toPosixPath(value) {
20
+ return value.split(path.sep).join('/');
21
+ }
22
+ function normalizeGitPath(value) {
23
+ return value.replace(/\\/gu, '/');
24
+ }
25
+ function gitList(projectRoot, args) {
26
+ const result = spawnSync('git', [...args, '-z'], {
27
+ cwd: projectRoot,
28
+ encoding: 'buffer',
29
+ env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
30
+ stdio: ['ignore', 'pipe', 'pipe'],
31
+ timeout: 30_000,
32
+ windowsHide: true,
33
+ });
34
+ if (result.status !== 0) {
35
+ return null;
36
+ }
37
+ return result.stdout
38
+ .toString('utf8')
39
+ .split('\0')
40
+ .map((entry) => normalizeGitPath(entry.trim()))
41
+ .filter((entry) => entry.length > 0)
42
+ .sort((left, right) => left.localeCompare(right));
43
+ }
44
+ function gitText(projectRoot, args) {
45
+ const result = spawnSync('git', args, {
46
+ cwd: projectRoot,
47
+ encoding: 'utf8',
48
+ env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ timeout: 30_000,
51
+ windowsHide: true,
52
+ });
53
+ return result.status === 0 ? result.stdout : null;
54
+ }
55
+ function listGitTrackedFiles(projectRoot) {
56
+ return gitList(projectRoot, ['ls-files']);
57
+ }
58
+ function listGitChangedFiles(projectRoot) {
59
+ const changedLists = [
60
+ gitList(projectRoot, ['diff', '--name-only', '--diff-filter=ACMR']),
61
+ gitList(projectRoot, ['diff', '--cached', '--name-only', '--diff-filter=ACMR']),
62
+ gitList(projectRoot, ['ls-files', '--others', '--exclude-standard']),
63
+ ];
64
+ if (changedLists.some((list) => list === null)) {
65
+ return null;
66
+ }
67
+ return [...new Set(changedLists.flatMap((list) => list ?? []))].sort((left, right) => left.localeCompare(right));
68
+ }
69
+ function isTextCandidate(relativePath) {
70
+ return TEXT_FILE_PATTERN.test(relativePath) && !relativePath.startsWith('.mustflow/state/');
71
+ }
72
+ function isCodeCandidate(relativePath) {
73
+ return CODE_FILE_PATTERN.test(relativePath);
74
+ }
75
+ function isMeaningfulCodeLine(text) {
76
+ const trimmed = text.trim();
77
+ return trimmed.length > 0 && !trimmed.startsWith('//') && !trimmed.startsWith('#') && !trimmed.startsWith('*');
78
+ }
79
+ function stripQuotedText(text) {
80
+ let stripped = '';
81
+ let quote = null;
82
+ let escaped = false;
83
+ for (const char of text) {
84
+ if (quote) {
85
+ stripped += ' ';
86
+ if (escaped) {
87
+ escaped = false;
88
+ continue;
89
+ }
90
+ if (char === '\\') {
91
+ escaped = true;
92
+ continue;
93
+ }
94
+ if (char === quote) {
95
+ quote = null;
96
+ }
97
+ continue;
98
+ }
99
+ if (char === '"' || char === "'" || char === '`') {
100
+ quote = char;
101
+ stripped += ' ';
102
+ continue;
103
+ }
104
+ stripped += char;
105
+ }
106
+ return stripped;
107
+ }
108
+ function stripRegexLiterals(text) {
109
+ const patternDeclaration = /^(\s*(?:const\s+[A-Z0-9_]+\s*=\s*)?)\/.*\/[a-z]*;?\s*$/u;
110
+ if (patternDeclaration.test(text)) {
111
+ return text.replace(/\/.*\/[a-z]*;?/u, 'REGEX;');
112
+ }
113
+ return text.replace(/([=(:,\[]\s*)\/.*\/[a-z]*/giu, '$1 REGEX');
114
+ }
115
+ function codeSurfaceForLine(text) {
116
+ const literalStripped = stripQuotedText(stripRegexLiterals(text));
117
+ return literalStripped.split('//', 1)[0] ?? literalStripped;
118
+ }
119
+ function isCommentOnlyLine(text) {
120
+ const trimmed = text.trim();
121
+ return trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*');
122
+ }
123
+ function countSemicolonsOutsideComment(text) {
124
+ return [...codeSurfaceForLine(text)].filter((char) => char === ';').length;
125
+ }
126
+ function readTextLines(projectRoot, relativePath, issues) {
127
+ const absolutePath = path.join(projectRoot, ...relativePath.split('/'));
128
+ if (!existsSync(absolutePath)) {
129
+ return [];
130
+ }
131
+ try {
132
+ const buffer = readFileInsideWithoutSymlinks(projectRoot, absolutePath);
133
+ if (buffer.includes(0)) {
134
+ return [];
135
+ }
136
+ return buffer
137
+ .toString('utf8')
138
+ .split(/\r?\n/u)
139
+ .map((text, index) => ({ line: index + 1, text }));
140
+ }
141
+ catch (error) {
142
+ issues.push(error instanceof Error ? error.message : String(error));
143
+ return [];
144
+ }
145
+ }
146
+ function isGitTracked(projectRoot, relativePath) {
147
+ const result = spawnSync('git', ['ls-files', '--error-unmatch', '--', relativePath], {
148
+ cwd: projectRoot,
149
+ encoding: 'utf8',
150
+ env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
151
+ stdio: ['ignore', 'pipe', 'pipe'],
152
+ timeout: 30_000,
153
+ windowsHide: true,
154
+ });
155
+ return result.status === 0;
156
+ }
157
+ function parseAddedLinesFromDiff(diffText) {
158
+ const addedLines = [];
159
+ let nextLine = 0;
160
+ for (const rawLine of diffText.split(/\r?\n/u)) {
161
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/u.exec(rawLine);
162
+ if (hunk) {
163
+ nextLine = Number.parseInt(hunk[1], 10);
164
+ continue;
165
+ }
166
+ if (nextLine === 0) {
167
+ continue;
168
+ }
169
+ if (rawLine.startsWith('+++')) {
170
+ continue;
171
+ }
172
+ if (rawLine.startsWith('+')) {
173
+ addedLines.push({ line: nextLine, text: rawLine.slice(1) });
174
+ nextLine += 1;
175
+ continue;
176
+ }
177
+ if (rawLine.startsWith('-')) {
178
+ continue;
179
+ }
180
+ nextLine += 1;
181
+ }
182
+ return addedLines;
183
+ }
184
+ function readChangedLineCandidates(projectRoot, relativePath, issues) {
185
+ if (!isGitTracked(projectRoot, relativePath)) {
186
+ return readTextLines(projectRoot, relativePath, issues);
187
+ }
188
+ const diffText = gitText(projectRoot, ['diff', 'HEAD', '--no-ext-diff', '--unified=0', '--', relativePath]);
189
+ if (diffText === null) {
190
+ return readTextLines(projectRoot, relativePath, issues);
191
+ }
192
+ return parseAddedLinesFromDiff(diffText);
193
+ }
194
+ function addRisk(risks, code, severity, relativePath, line, detail, metric) {
195
+ risks.push({
196
+ code,
197
+ severity,
198
+ path: toPosixPath(relativePath),
199
+ line,
200
+ detail,
201
+ metric,
202
+ });
203
+ }
204
+ function inspectLine(relativePath, candidate, risks) {
205
+ const text = candidate.text;
206
+ const trimmed = text.trim();
207
+ const isCode = isCodeCandidate(relativePath);
208
+ const codeSurface = codeSurfaceForLine(text);
209
+ const markerSurface = isCommentOnlyLine(text) ? trimmed : codeSurface;
210
+ if (isCode && codeSurface.length > LONG_LINE_THRESHOLD) {
211
+ addRisk(risks, 'line_stuffing_added', 'high', relativePath, candidate.line, 'Added line exceeds the anti-stuffing length threshold.', {
212
+ name: 'line_length',
213
+ value: codeSurface.length,
214
+ threshold: LONG_LINE_THRESHOLD,
215
+ });
216
+ }
217
+ if (isCode &&
218
+ isMeaningfulCodeLine(text) &&
219
+ countSemicolonsOutsideComment(text) > MULTI_STATEMENT_SEMICOLON_THRESHOLD) {
220
+ addRisk(risks, 'multiple_statements_per_line_added', 'high', relativePath, candidate.line, 'Added code line appears to contain multiple statements.', {
221
+ name: 'semicolons',
222
+ value: countSemicolonsOutsideComment(text),
223
+ threshold: MULTI_STATEMENT_SEMICOLON_THRESHOLD,
224
+ });
225
+ }
226
+ if (isCode && SUPPRESSION_PATTERN.test(markerSurface)) {
227
+ addRisk(risks, 'suppression_added', 'critical', relativePath, candidate.line, 'Added line contains a validation suppression marker.', null);
228
+ }
229
+ if (isCode && TYPE_ESCAPE_PATTERN.test(codeSurface)) {
230
+ addRisk(risks, 'type_escape_added', 'high', relativePath, candidate.line, 'Added line contains a broad type escape or non-null assertion pattern.', null);
231
+ }
232
+ if (isCode && TEST_BYPASS_PATTERN.test(codeSurface)) {
233
+ addRisk(risks, 'test_bypass_marker_added', 'critical', relativePath, candidate.line, 'Added line contains a test bypass marker.', null);
234
+ }
235
+ if (isCode &&
236
+ (PLACEHOLDER_PATTERN.test(markerSurface) ||
237
+ (/\bthrow\s+new\s+Error\s*\(/u.test(codeSurface) && THROW_PLACEHOLDER_MESSAGE_PATTERN.test(text)))) {
238
+ addRisk(risks, 'placeholder_added', 'medium', relativePath, candidate.line, 'Added line looks like a placeholder or unfinished implementation.', null);
239
+ }
240
+ if (isCode && EMPTY_CATCH_PATTERN.test(codeSurface)) {
241
+ addRisk(risks, 'empty_catch_swallow_added', 'high', relativePath, candidate.line, 'Added line contains an empty catch block that swallows failures.', null);
242
+ }
243
+ if (GENERATED_OR_VENDOR_PATTERN.test(relativePath) && isCode && isMeaningfulCodeLine(text)) {
244
+ addRisk(risks, 'generated_or_vendor_logic_added', 'high', relativePath, candidate.line, 'Added executable-looking logic under generated, vendor, or third_party paths.', null);
245
+ }
246
+ }
247
+ function inspectFile(projectRoot, relativePath, scope, issues) {
248
+ const candidates = scope === 'tracked'
249
+ ? readTextLines(projectRoot, relativePath, issues)
250
+ : readChangedLineCandidates(projectRoot, relativePath, issues);
251
+ const risks = [];
252
+ for (const candidate of candidates) {
253
+ inspectLine(relativePath, candidate, risks);
254
+ }
255
+ if (scope === 'tracked' && HELPER_CONTAINER_PATTERN.test(relativePath) && candidates.length > LARGE_HELPER_LINE_THRESHOLD) {
256
+ addRisk(risks, 'large_helper_container_added', 'medium', relativePath, null, 'Helper, util, manager, common, or misc container exceeds the size threshold.', {
257
+ name: 'file_lines',
258
+ value: candidates.length,
259
+ threshold: LARGE_HELPER_LINE_THRESHOLD,
260
+ });
261
+ }
262
+ if (risks.length === 0) {
263
+ return null;
264
+ }
265
+ return {
266
+ path: toPosixPath(relativePath),
267
+ checked_lines: candidates.length,
268
+ risk_count: risks.length,
269
+ risks,
270
+ };
271
+ }
272
+ export function inspectQualityGaming(projectRoot, options = {}) {
273
+ const root = path.resolve(projectRoot);
274
+ const scope = options.scope ?? 'changed';
275
+ const candidateFiles = scope === 'tracked' ? listGitTrackedFiles(root) : listGitChangedFiles(root);
276
+ const issues = [];
277
+ const riskyFiles = [];
278
+ if (!candidateFiles) {
279
+ issues.push('Git files could not be listed. Run this command inside a Git working tree.');
280
+ }
281
+ for (const relativePath of candidateFiles ?? []) {
282
+ if (!isTextCandidate(relativePath)) {
283
+ continue;
284
+ }
285
+ const fileReport = inspectFile(root, relativePath, scope, issues);
286
+ if (fileReport) {
287
+ riskyFiles.push(fileReport);
288
+ }
289
+ }
290
+ const riskCount = riskyFiles.reduce((sum, file) => sum + file.risk_count, 0);
291
+ return {
292
+ schema_version: '1',
293
+ command: 'quality',
294
+ mode: 'check',
295
+ scope,
296
+ ok: issues.length === 0 && riskCount === 0,
297
+ mustflow_root: root,
298
+ git_tracked: candidateFiles !== null,
299
+ checked_files: candidateFiles?.filter(isTextCandidate).length ?? 0,
300
+ risk_count: riskCount,
301
+ risky_files: riskyFiles,
302
+ issues,
303
+ };
304
+ }