mustflow 2.59.0 → 2.69.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.
- package/README.md +1 -0
- package/dist/cli/commands/context.js +81 -6
- package/dist/cli/commands/skill.js +116 -0
- package/dist/cli/i18n/en.js +3 -1
- package/dist/cli/i18n/es.js +3 -1
- package/dist/cli/i18n/fr.js +3 -1
- package/dist/cli/i18n/hi.js +3 -1
- package/dist/cli/i18n/ko.js +3 -1
- package/dist/cli/i18n/zh.js +3 -1
- package/dist/cli/lib/agent-context.js +981 -8
- package/dist/cli/lib/command-registry.js +6 -0
- package/dist/cli/lib/local-index/constants.js +4 -5
- package/dist/cli/lib/local-index/freshness.js +5 -1
- package/dist/cli/lib/local-index/index.js +1 -1
- package/dist/cli/lib/repo-map.js +7 -1
- package/dist/cli/lib/validation/constants.js +3 -0
- package/dist/cli/lib/validation/index.js +41 -2
- package/dist/core/check-issues.js +5 -0
- package/dist/core/prompt-cache-rendering.js +19 -0
- package/dist/core/public-json-contracts.js +26 -0
- package/dist/core/quality-gaming.js +4 -0
- package/dist/core/skill-route-fixtures.js +173 -0
- package/dist/core/skill-route-resolution.js +398 -0
- package/dist/core/source-anchors.js +91 -5
- package/package.json +1 -1
- package/schemas/README.md +7 -2
- package/schemas/context-report.schema.json +442 -0
- package/schemas/quality-gaming-report.schema.json +1 -0
- package/schemas/route-fixture.schema.json +57 -0
- package/schemas/skill-route-report.schema.json +242 -0
- package/templates/default/common/.mustflow/config/commands.toml +18 -2
- package/templates/default/common/.mustflow/config/mustflow.toml +16 -10
- package/templates/default/i18n.toml +21 -9
- package/templates/default/locales/en/.mustflow/context/PROJECT.md +5 -3
- package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +13 -9
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +4 -2
- package/templates/default/locales/en/.mustflow/skills/llm-token-cost-control-review/SKILL.md +5 -2
- package/templates/default/locales/en/.mustflow/skills/quality-gaming-guard/SKILL.md +8 -6
- package/templates/default/locales/en/.mustflow/skills/router.toml +67 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +12 -0
- package/templates/default/locales/en/.mustflow/skills/skill-refresh/SKILL.md +234 -0
- package/templates/default/locales/en/.mustflow/skills/task-instruction-authoring/SKILL.md +239 -0
- package/templates/default/locales/en/AGENTS.md +15 -7
- package/templates/default/locales/ko/.mustflow/context/PROJECT.md +4 -2
- package/templates/default/locales/ko/.mustflow/docs/agent-workflow.md +18 -12
- package/templates/default/locales/ko/AGENTS.md +8 -6
- package/templates/default/manifest.toml +12 -1
|
@@ -143,6 +143,12 @@ export const COMMAND_DEFINITIONS = [
|
|
|
143
143
|
summaryKey: 'command.search.summary',
|
|
144
144
|
loadRunner: async () => (await import('../commands/search.js')).runSearch,
|
|
145
145
|
},
|
|
146
|
+
{
|
|
147
|
+
id: 'skill',
|
|
148
|
+
usage: 'mf skill',
|
|
149
|
+
summaryKey: 'command.skill.summary',
|
|
150
|
+
loadRunner: async () => (await import('../commands/skill.js')).runSkill,
|
|
151
|
+
},
|
|
146
152
|
{
|
|
147
153
|
id: 'dashboard',
|
|
148
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/
|
|
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
|
-
'
|
|
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
|
}
|
package/dist/cli/lib/repo-map.js
CHANGED
|
@@ -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/
|
|
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
|
|
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
|
+
}
|
|
@@ -207,6 +207,32 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
207
207
|
installedCommand: ['mf', 'quality', 'check', '--json'],
|
|
208
208
|
expectedExitCodes: [0, 1],
|
|
209
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
|
+
},
|
|
210
236
|
{
|
|
211
237
|
id: 'latest-run-pointer',
|
|
212
238
|
schemaFile: 'latest-run-pointer.schema.json',
|
|
@@ -15,6 +15,7 @@ const TYPE_ESCAPE_PATTERN = /\b(?:as\s+any|as\s+unknown\s+as|as\s+never|:\s*any\
|
|
|
15
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
16
|
const PLACEHOLDER_PATTERN = /(?:TODO|FIXME|HACK|not\s+implemented|unimplemented|throw\s+new\s+Error\(["'`](?:not implemented|todo|unsupported)|\bpass\s*$)/iu;
|
|
17
17
|
const THROW_PLACEHOLDER_MESSAGE_PATTERN = /(?:not\s+implemented|unimplemented|todo|unsupported)/iu;
|
|
18
|
+
const EMPTY_CATCH_PATTERN = /\bcatch\s*(?:\([^)]*\))?\s*\{\s*\}/u;
|
|
18
19
|
function toPosixPath(value) {
|
|
19
20
|
return value.split(path.sep).join('/');
|
|
20
21
|
}
|
|
@@ -236,6 +237,9 @@ function inspectLine(relativePath, candidate, risks) {
|
|
|
236
237
|
(/\bthrow\s+new\s+Error\s*\(/u.test(codeSurface) && THROW_PLACEHOLDER_MESSAGE_PATTERN.test(text)))) {
|
|
237
238
|
addRisk(risks, 'placeholder_added', 'medium', relativePath, candidate.line, 'Added line looks like a placeholder or unfinished implementation.', null);
|
|
238
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
|
+
}
|
|
239
243
|
if (GENERATED_OR_VENDOR_PATTERN.test(relativePath) && isCode && isMeaningfulCodeLine(text)) {
|
|
240
244
|
addRisk(risks, 'generated_or_vendor_logic_added', 'high', relativePath, candidate.line, 'Added executable-looking logic under generated, vendor, or third_party paths.', null);
|
|
241
245
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isRecord } from './config-loading.js';
|
|
4
|
+
import { readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
5
|
+
import { resolveSkillRoutes } from './skill-route-resolution.js';
|
|
6
|
+
export const SKILL_ROUTE_FIXTURES_PATH = '.mustflow/skills/route-fixtures.json';
|
|
7
|
+
const FIXTURE_MAX_BYTES = 256 * 1024;
|
|
8
|
+
function readStringArray(value, options = {}) {
|
|
9
|
+
if (!Array.isArray(value) ||
|
|
10
|
+
(!options.allowEmpty && value.length === 0) ||
|
|
11
|
+
!value.every((entry) => typeof entry === 'string' && entry.trim().length > 0)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return value.map((entry) => entry.trim());
|
|
15
|
+
}
|
|
16
|
+
function readOptionalStringArray(value, pointer, issues) {
|
|
17
|
+
if (value === undefined) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const entries = readStringArray(value);
|
|
21
|
+
if (!entries) {
|
|
22
|
+
issues.push({ kind: 'invalid', message: `${pointer} must be a non-empty string array when present` });
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return entries;
|
|
26
|
+
}
|
|
27
|
+
function readOptionalString(value) {
|
|
28
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
29
|
+
}
|
|
30
|
+
function readOptionalMaxCandidates(value) {
|
|
31
|
+
return Number.isInteger(value) && Number(value) > 0 && Number(value) <= 10 ? Number(value) : undefined;
|
|
32
|
+
}
|
|
33
|
+
function parseFixtureCase(value, index, issues) {
|
|
34
|
+
const pointer = `${SKILL_ROUTE_FIXTURES_PATH} cases[${index}]`;
|
|
35
|
+
if (!isRecord(value)) {
|
|
36
|
+
issues.push({ kind: 'invalid', message: `${pointer} must be an object` });
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const id = readOptionalString(value.id);
|
|
40
|
+
if (!id) {
|
|
41
|
+
issues.push({ kind: 'invalid', message: `${pointer}.id must be a non-empty string` });
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const paths = readStringArray(value.paths);
|
|
45
|
+
if (!paths) {
|
|
46
|
+
issues.push({ kind: 'invalid', message: `${pointer}.paths must be a non-empty string array` });
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const reasons = readStringArray(value.reasons);
|
|
50
|
+
if (!reasons) {
|
|
51
|
+
issues.push({ kind: 'invalid', message: `${pointer}.reasons must be a non-empty string array` });
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
let task;
|
|
55
|
+
if (value.task === undefined || value.task === null) {
|
|
56
|
+
task = null;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const taskValue = readOptionalString(value.task);
|
|
60
|
+
if (!taskValue) {
|
|
61
|
+
issues.push({ kind: 'invalid', message: `${pointer}.task must be a non-empty string, null, or omitted` });
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
task = taskValue;
|
|
65
|
+
}
|
|
66
|
+
const maxCandidates = value.max_candidates === undefined ? undefined : readOptionalMaxCandidates(value.max_candidates);
|
|
67
|
+
if (value.max_candidates !== undefined && maxCandidates === undefined) {
|
|
68
|
+
issues.push({ kind: 'invalid', message: `${pointer}.max_candidates must be an integer from 1 through 10` });
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const requiredCandidates = readOptionalStringArray(value.required_candidates, `${pointer}.required_candidates`, issues);
|
|
72
|
+
const requiredAdjuncts = readOptionalStringArray(value.required_adjuncts, `${pointer}.required_adjuncts`, issues);
|
|
73
|
+
const forbiddenCandidates = readOptionalStringArray(value.forbidden_candidates, `${pointer}.forbidden_candidates`, issues);
|
|
74
|
+
if (!requiredCandidates || !requiredAdjuncts || !forbiddenCandidates) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const fixture = {
|
|
78
|
+
id,
|
|
79
|
+
task,
|
|
80
|
+
paths,
|
|
81
|
+
reasons,
|
|
82
|
+
maxCandidates,
|
|
83
|
+
requiredMain: readOptionalString(value.required_main),
|
|
84
|
+
requiredCandidates,
|
|
85
|
+
requiredAdjuncts,
|
|
86
|
+
forbiddenCandidates,
|
|
87
|
+
};
|
|
88
|
+
if (!fixture.requiredMain &&
|
|
89
|
+
fixture.requiredCandidates.length === 0 &&
|
|
90
|
+
fixture.requiredAdjuncts.length === 0 &&
|
|
91
|
+
fixture.forbiddenCandidates.length === 0) {
|
|
92
|
+
issues.push({
|
|
93
|
+
kind: 'invalid',
|
|
94
|
+
message: `${pointer} must declare required_main, required_candidates, required_adjuncts, or forbidden_candidates`,
|
|
95
|
+
});
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return fixture;
|
|
99
|
+
}
|
|
100
|
+
function formatSkill(value) {
|
|
101
|
+
return value ?? 'none';
|
|
102
|
+
}
|
|
103
|
+
function validateFixtureCase(projectRoot, fixture) {
|
|
104
|
+
const report = resolveSkillRoutes(projectRoot, {
|
|
105
|
+
taskText: fixture.task,
|
|
106
|
+
paths: fixture.paths,
|
|
107
|
+
reasons: fixture.reasons,
|
|
108
|
+
maxCandidates: fixture.maxCandidates,
|
|
109
|
+
});
|
|
110
|
+
const issues = [];
|
|
111
|
+
const candidateSkills = new Set(report.candidates.map((candidate) => candidate.skill));
|
|
112
|
+
const adjunctSkills = new Set(report.selected.adjuncts.map((candidate) => candidate.skill));
|
|
113
|
+
if (fixture.requiredMain && report.selected.main?.skill !== fixture.requiredMain) {
|
|
114
|
+
issues.push({
|
|
115
|
+
kind: 'mismatch',
|
|
116
|
+
message: `Skill route fixture "${fixture.id}" expected selected main "${fixture.requiredMain}" but got "${formatSkill(report.selected.main?.skill)}"`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const skill of fixture.requiredCandidates) {
|
|
120
|
+
if (!candidateSkills.has(skill)) {
|
|
121
|
+
issues.push({
|
|
122
|
+
kind: 'mismatch',
|
|
123
|
+
message: `Skill route fixture "${fixture.id}" expected candidate "${skill}" within top ${report.input.max_candidates}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const skill of fixture.requiredAdjuncts) {
|
|
128
|
+
if (!adjunctSkills.has(skill)) {
|
|
129
|
+
issues.push({
|
|
130
|
+
kind: 'mismatch',
|
|
131
|
+
message: `Skill route fixture "${fixture.id}" expected selected adjunct "${skill}"`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const skill of fixture.forbiddenCandidates) {
|
|
136
|
+
if (candidateSkills.has(skill) || adjunctSkills.has(skill) || report.selected.main?.skill === skill) {
|
|
137
|
+
issues.push({
|
|
138
|
+
kind: 'mismatch',
|
|
139
|
+
message: `Skill route fixture "${fixture.id}" forbids selected or candidate skill "${skill}"`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return issues;
|
|
144
|
+
}
|
|
145
|
+
export function validateSkillRouteFixtures(projectRoot) {
|
|
146
|
+
const fixturePath = path.join(projectRoot, ...SKILL_ROUTE_FIXTURES_PATH.split('/'));
|
|
147
|
+
if (!existsSync(fixturePath)) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
const issues = [];
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(readUtf8FileInsideWithoutSymlinks(projectRoot, fixturePath, { maxBytes: FIXTURE_MAX_BYTES }));
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
157
|
+
return [{ kind: 'invalid', message: `${SKILL_ROUTE_FIXTURES_PATH} is not valid JSON: ${message}` }];
|
|
158
|
+
}
|
|
159
|
+
if (!isRecord(parsed) || parsed.schema_version !== '1' || !Array.isArray(parsed.cases)) {
|
|
160
|
+
return [{
|
|
161
|
+
kind: 'invalid',
|
|
162
|
+
message: `${SKILL_ROUTE_FIXTURES_PATH} must contain schema_version "1" and a cases array`,
|
|
163
|
+
}];
|
|
164
|
+
}
|
|
165
|
+
for (const [index, entry] of parsed.cases.entries()) {
|
|
166
|
+
const fixture = parseFixtureCase(entry, index, issues);
|
|
167
|
+
if (!fixture) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
issues.push(...validateFixtureCase(projectRoot, fixture));
|
|
171
|
+
}
|
|
172
|
+
return issues;
|
|
173
|
+
}
|