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
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isRecord, readMustflowOwnedTomlFile } from './config-loading.js';
|
|
4
|
+
import { readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
5
|
+
const MUSTFLOW_TEXT_MAX_BYTES = 1024 * 1024;
|
|
6
|
+
const SKILL_INDEX_PATH = '.mustflow/skills/INDEX.md';
|
|
7
|
+
const SKILL_ROUTER_PATH = '.mustflow/skills/router.toml';
|
|
8
|
+
const SKILL_ROUTES_METADATA_PATH = '.mustflow/skills/routes.toml';
|
|
9
|
+
const SKILL_FRONTMATTER_SOURCE = '.mustflow/skills/*/SKILL.md';
|
|
10
|
+
const DEFAULT_MAX_CANDIDATES = 5;
|
|
11
|
+
const DEFAULT_MAX_MAIN = 1;
|
|
12
|
+
const DEFAULT_MAX_ADJUNCTS = 2;
|
|
13
|
+
const PATH_SKILL_HINT_SCORE = 25;
|
|
14
|
+
const DOCS_TREE_MARKDOWN_PATH_PATTERN = /(?:^|\/)(?:docs|docs-site|documentation|\.mustflow\/docs|\.mustflow\/context)\/.+\.(?:md|mdx)$/u;
|
|
15
|
+
const ROOT_DOCUMENT_BASENAMES = [
|
|
16
|
+
'readme',
|
|
17
|
+
'changelog',
|
|
18
|
+
'contributing',
|
|
19
|
+
'security',
|
|
20
|
+
'support',
|
|
21
|
+
'governance',
|
|
22
|
+
'maintainers',
|
|
23
|
+
'releasing',
|
|
24
|
+
'release',
|
|
25
|
+
'testing',
|
|
26
|
+
'deployment',
|
|
27
|
+
'operations',
|
|
28
|
+
'runbook',
|
|
29
|
+
'configuration',
|
|
30
|
+
'troubleshooting',
|
|
31
|
+
'architecture',
|
|
32
|
+
'api',
|
|
33
|
+
];
|
|
34
|
+
const ROUTE_TYPE_WEIGHTS = {
|
|
35
|
+
primary: 18,
|
|
36
|
+
authoring: 16,
|
|
37
|
+
adjunct: 8,
|
|
38
|
+
event: 4,
|
|
39
|
+
};
|
|
40
|
+
function normalizeSkillPath(value) {
|
|
41
|
+
return value.replace(/\\/gu, '/');
|
|
42
|
+
}
|
|
43
|
+
function skillNameFromPath(skillPath) {
|
|
44
|
+
const match = /^\.mustflow\/skills\/([^/]+)\/SKILL\.md$/u.exec(skillPath);
|
|
45
|
+
return match?.[1] ?? skillPath;
|
|
46
|
+
}
|
|
47
|
+
function tokenize(value) {
|
|
48
|
+
return [
|
|
49
|
+
...new Set(value
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/\.mustflow\/skills\/[^/\s]+\/skill\.md/giu, ' ')
|
|
52
|
+
.replace(/[^a-z0-9]+/gu, ' ')
|
|
53
|
+
.split(/\s+/u)
|
|
54
|
+
.map((token) => token.trim())
|
|
55
|
+
.filter((token) => token.length >= 3)),
|
|
56
|
+
].sort((left, right) => left.localeCompare(right));
|
|
57
|
+
}
|
|
58
|
+
function collectPathSkillHints(paths) {
|
|
59
|
+
const hints = new Set();
|
|
60
|
+
for (const pathValue of paths) {
|
|
61
|
+
const lower = pathValue.toLowerCase();
|
|
62
|
+
if (/\.(?:cts|mts|ts|tsx)$/u.test(lower) || lower.endsWith('tsconfig.json')) {
|
|
63
|
+
hints.add('typescript-code-change');
|
|
64
|
+
}
|
|
65
|
+
if (/\.(?:cjs|mjs|js|jsx)$/u.test(lower)) {
|
|
66
|
+
hints.add('javascript-code-change');
|
|
67
|
+
}
|
|
68
|
+
if (/\.py$/u.test(lower) || /(?:^|\/)(?:pyproject\.toml|requirements\.txt|poetry\.lock)$/u.test(lower)) {
|
|
69
|
+
hints.add('python-code-change');
|
|
70
|
+
}
|
|
71
|
+
if (/\.go$/u.test(lower) || /(?:^|\/)go\.(?:mod|sum)$/u.test(lower)) {
|
|
72
|
+
hints.add('go-code-change');
|
|
73
|
+
}
|
|
74
|
+
if (/\.rs$/u.test(lower) || /(?:^|\/)(?:cargo\.toml|cargo\.lock)$/u.test(lower)) {
|
|
75
|
+
hints.add('rust-code-change');
|
|
76
|
+
}
|
|
77
|
+
if (/\.ps1$/u.test(lower)) {
|
|
78
|
+
hints.add('powershell-code-change');
|
|
79
|
+
}
|
|
80
|
+
if (DOCS_TREE_MARKDOWN_PATH_PATTERN.test(lower) || isRootDocumentationPath(lower)) {
|
|
81
|
+
hints.add('docs-update');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return hints;
|
|
85
|
+
}
|
|
86
|
+
function isRootDocumentationPath(lowercasePath) {
|
|
87
|
+
const basename = lowercasePath.split('/').pop();
|
|
88
|
+
if (!basename?.endsWith('.md')) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const rootName = basename.replace(/\.md$/u, '');
|
|
92
|
+
return ROOT_DOCUMENT_BASENAMES.includes(rootName);
|
|
93
|
+
}
|
|
94
|
+
function readStringArrayFromTable(table, key) {
|
|
95
|
+
const value = table[key];
|
|
96
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string')
|
|
97
|
+
? value.map((entry) => entry.trim()).filter(Boolean)
|
|
98
|
+
: [];
|
|
99
|
+
}
|
|
100
|
+
function readFrontmatterBlock(content) {
|
|
101
|
+
if (!content.startsWith('---')) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const firstLineEnd = content.indexOf('\n');
|
|
105
|
+
if (firstLineEnd < 0) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const end = content.indexOf('\n---', firstLineEnd + 1);
|
|
109
|
+
if (end < 0) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
return content.slice(firstLineEnd + 1, end).split(/\r?\n/u);
|
|
113
|
+
}
|
|
114
|
+
function readFrontmatterScalar(lines, key) {
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const match = /^([a-zA-Z0-9_]+):\s*(.*)$/u.exec(line);
|
|
117
|
+
if (match?.[1] === key) {
|
|
118
|
+
return match[2].trim().replace(/^["']|["']$/gu, '') || null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function readFrontmatterList(lines, key) {
|
|
124
|
+
const values = [];
|
|
125
|
+
let inList = false;
|
|
126
|
+
let baseIndent = 0;
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
const keyMatch = new RegExp(`^(\\s*)${key}:\\s*$`, 'u').exec(line);
|
|
129
|
+
if (keyMatch) {
|
|
130
|
+
inList = true;
|
|
131
|
+
baseIndent = keyMatch[1].length;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!inList) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const itemMatch = /^(\s*)-\s+(.+?)\s*$/u.exec(line);
|
|
138
|
+
if (itemMatch && itemMatch[1].length > baseIndent) {
|
|
139
|
+
values.push(itemMatch[2].trim().replace(/^["']|["']$/gu, ''));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (line.trim() && !line.startsWith(' '.repeat(baseIndent + 1))) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return values;
|
|
147
|
+
}
|
|
148
|
+
function readSkillFrontmatterSummary(content) {
|
|
149
|
+
const lines = readFrontmatterBlock(content);
|
|
150
|
+
return {
|
|
151
|
+
name: readFrontmatterScalar(lines, 'name'),
|
|
152
|
+
description: readFrontmatterScalar(lines, 'description'),
|
|
153
|
+
commandIntents: readFrontmatterList(lines, 'command_intents'),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function readSkillRouteMetadata(projectRoot) {
|
|
157
|
+
const metadata = new Map();
|
|
158
|
+
try {
|
|
159
|
+
const parsed = readMustflowOwnedTomlFile(projectRoot, SKILL_ROUTES_METADATA_PATH);
|
|
160
|
+
if (!isRecord(parsed) || !isRecord(parsed.routes)) {
|
|
161
|
+
return metadata;
|
|
162
|
+
}
|
|
163
|
+
for (const [skillName, route] of Object.entries(parsed.routes)) {
|
|
164
|
+
if (!isRecord(route)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
metadata.set(skillName, {
|
|
168
|
+
category: typeof route.category === 'string' ? route.category : null,
|
|
169
|
+
routeType: typeof route.route_type === 'string' ? route.route_type : 'unknown',
|
|
170
|
+
priority: Number.isInteger(route.priority) ? Number(route.priority) : 0,
|
|
171
|
+
appliesToReasons: readStringArrayFromTable(route, 'applies_to_reasons'),
|
|
172
|
+
mutuallyExclusiveWith: readStringArrayFromTable(route, 'mutually_exclusive_with'),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return metadata;
|
|
178
|
+
}
|
|
179
|
+
return metadata;
|
|
180
|
+
}
|
|
181
|
+
function readSkillFrontmatterRoutes(projectRoot) {
|
|
182
|
+
const skillRoot = path.join(projectRoot, '.mustflow', 'skills');
|
|
183
|
+
if (!existsSync(skillRoot)) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const routes = [];
|
|
187
|
+
const skillDirectories = readdirSync(skillRoot, { withFileTypes: true })
|
|
188
|
+
.filter((entry) => entry.isDirectory())
|
|
189
|
+
.map((entry) => entry.name)
|
|
190
|
+
.sort((left, right) => left.localeCompare(right));
|
|
191
|
+
for (const skillDirectory of skillDirectories) {
|
|
192
|
+
const skillPath = `.mustflow/skills/${skillDirectory}/SKILL.md`;
|
|
193
|
+
const absoluteSkillPath = path.join(projectRoot, ...skillPath.split('/'));
|
|
194
|
+
if (!existsSync(absoluteSkillPath)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const content = readUtf8FileInsideWithoutSymlinks(projectRoot, absoluteSkillPath, {
|
|
198
|
+
maxBytes: MUSTFLOW_TEXT_MAX_BYTES,
|
|
199
|
+
});
|
|
200
|
+
const summary = readSkillFrontmatterSummary(content);
|
|
201
|
+
const skillName = summary.name ?? skillDirectory;
|
|
202
|
+
routes.push({
|
|
203
|
+
trigger: summary.description ?? skillName,
|
|
204
|
+
skillPath,
|
|
205
|
+
requiredInput: '',
|
|
206
|
+
editScope: '',
|
|
207
|
+
risk: '',
|
|
208
|
+
commandIntents: summary.commandIntents,
|
|
209
|
+
expectedOutput: '',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return routes;
|
|
213
|
+
}
|
|
214
|
+
function countMatches(needles, haystack) {
|
|
215
|
+
const haystackSet = new Set(haystack);
|
|
216
|
+
return needles.filter((needle) => haystackSet.has(needle)).length;
|
|
217
|
+
}
|
|
218
|
+
function routeTextTerms(route, skillName) {
|
|
219
|
+
return tokenize([
|
|
220
|
+
skillName,
|
|
221
|
+
route.trigger,
|
|
222
|
+
route.requiredInput,
|
|
223
|
+
route.editScope,
|
|
224
|
+
route.risk,
|
|
225
|
+
route.expectedOutput,
|
|
226
|
+
route.skillPath,
|
|
227
|
+
].join(' '));
|
|
228
|
+
}
|
|
229
|
+
function createCandidate(route, metadata, taskTerms, pathTerms, pathSkillHints, reasons) {
|
|
230
|
+
const skill = skillNameFromPath(route.skillPath);
|
|
231
|
+
const terms = routeTextTerms(route, skill);
|
|
232
|
+
const matchedReasons = reasons.filter((reason) => metadata.appliesToReasons.includes(reason));
|
|
233
|
+
const taskMatches = countMatches(taskTerms, terms);
|
|
234
|
+
const pathMatches = countMatches(pathTerms, terms);
|
|
235
|
+
const pathSkillHintMatched = pathSkillHints.has(skill);
|
|
236
|
+
const breakdown = {
|
|
237
|
+
reason_match: matchedReasons.length * 35,
|
|
238
|
+
task_text_match: taskMatches * 6,
|
|
239
|
+
path_match: pathMatches * 6 + (pathSkillHintMatched ? PATH_SKILL_HINT_SCORE : 0),
|
|
240
|
+
route_type_weight: ROUTE_TYPE_WEIGHTS[metadata.routeType] ?? 0,
|
|
241
|
+
priority_weight: Math.max(0, Math.min(metadata.priority, 100)) / 5,
|
|
242
|
+
};
|
|
243
|
+
const score = Object.values(breakdown).reduce((total, value) => total + value, 0);
|
|
244
|
+
const selectionReasons = [
|
|
245
|
+
...matchedReasons.map((reason) => `reason:${reason}`),
|
|
246
|
+
...(taskMatches > 0 ? [`task_terms:${taskMatches}`] : []),
|
|
247
|
+
...(pathMatches > 0 ? [`path_terms:${pathMatches}`] : []),
|
|
248
|
+
...(pathSkillHintMatched ? [`path_skill_hint:${skill}`] : []),
|
|
249
|
+
`route_type:${metadata.routeType}`,
|
|
250
|
+
`priority:${metadata.priority}`,
|
|
251
|
+
];
|
|
252
|
+
return {
|
|
253
|
+
skill,
|
|
254
|
+
skill_path: route.skillPath,
|
|
255
|
+
trigger: route.trigger,
|
|
256
|
+
category: metadata.category,
|
|
257
|
+
route_type: metadata.routeType,
|
|
258
|
+
priority: metadata.priority,
|
|
259
|
+
applies_to_reasons: metadata.appliesToReasons,
|
|
260
|
+
score,
|
|
261
|
+
score_breakdown: breakdown,
|
|
262
|
+
selection_reasons: selectionReasons,
|
|
263
|
+
verification_intents: route.commandIntents,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function sortCandidates(left, right) {
|
|
267
|
+
const score = right.score - left.score;
|
|
268
|
+
if (score !== 0) {
|
|
269
|
+
return score;
|
|
270
|
+
}
|
|
271
|
+
const priority = right.priority - left.priority;
|
|
272
|
+
if (priority !== 0) {
|
|
273
|
+
return priority;
|
|
274
|
+
}
|
|
275
|
+
return left.skill.localeCompare(right.skill);
|
|
276
|
+
}
|
|
277
|
+
function isSelectableMain(candidate) {
|
|
278
|
+
return candidate.route_type === 'primary' || candidate.route_type === 'authoring';
|
|
279
|
+
}
|
|
280
|
+
function selectAdjuncts(main, allCandidates, metadata) {
|
|
281
|
+
if (!main) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
const mainMetadata = metadata.get(main.skill);
|
|
285
|
+
const excluded = new Set([main.skill, ...(mainMetadata?.mutuallyExclusiveWith ?? [])]);
|
|
286
|
+
return allCandidates
|
|
287
|
+
.filter((candidate) => {
|
|
288
|
+
return (candidate.route_type === 'adjunct' &&
|
|
289
|
+
candidate.category === main.category &&
|
|
290
|
+
!excluded.has(candidate.skill));
|
|
291
|
+
})
|
|
292
|
+
.sort(sortCandidates)
|
|
293
|
+
.slice(0, DEFAULT_MAX_ADJUNCTS);
|
|
294
|
+
}
|
|
295
|
+
function uniqueCandidatePaths(candidates) {
|
|
296
|
+
return [...new Set(candidates.map((candidate) => candidate.skill_path))];
|
|
297
|
+
}
|
|
298
|
+
function createReadPlan(maxCandidates, selected, candidates) {
|
|
299
|
+
const selectedCandidates = [selected.main, ...selected.adjuncts].filter((candidate) => candidate !== null);
|
|
300
|
+
return {
|
|
301
|
+
selection_limits: {
|
|
302
|
+
candidates: maxCandidates,
|
|
303
|
+
main: DEFAULT_MAX_MAIN,
|
|
304
|
+
adjuncts: DEFAULT_MAX_ADJUNCTS,
|
|
305
|
+
},
|
|
306
|
+
stable_kernel: [SKILL_ROUTER_PATH],
|
|
307
|
+
selected_skill_paths: uniqueCandidatePaths(selectedCandidates),
|
|
308
|
+
candidate_skill_paths: uniqueCandidatePaths(candidates),
|
|
309
|
+
fallback_route_metadata: {
|
|
310
|
+
path: SKILL_ROUTES_METADATA_PATH,
|
|
311
|
+
read_when: [
|
|
312
|
+
'router taxonomy is insufficient',
|
|
313
|
+
'task edits skill routing',
|
|
314
|
+
'detailed route metadata is needed',
|
|
315
|
+
'category or confidence is ambiguous',
|
|
316
|
+
'selected skill paths are empty',
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
expanded_index: {
|
|
320
|
+
path: SKILL_INDEX_PATH,
|
|
321
|
+
read_when: [
|
|
322
|
+
'full route metadata is insufficient',
|
|
323
|
+
'task edits the expanded route table',
|
|
324
|
+
'human-readable trigger evidence is needed',
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
avoid_by_default: [SKILL_INDEX_PATH],
|
|
328
|
+
notes: [
|
|
329
|
+
'Keep the router kernel in the stable prefix and load selected SKILL.md files in task context.',
|
|
330
|
+
'Do not add the expanded skill index to the prompt unless a fallback condition applies.',
|
|
331
|
+
'If rerouting evidence appears, run the resolver again and append only the new task-layer reads.',
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function clampCandidateLimit(value) {
|
|
336
|
+
if (value === undefined || !Number.isInteger(value)) {
|
|
337
|
+
return DEFAULT_MAX_CANDIDATES;
|
|
338
|
+
}
|
|
339
|
+
return Math.max(1, Math.min(value, 10));
|
|
340
|
+
}
|
|
341
|
+
export function resolveSkillRoutes(projectRoot, input) {
|
|
342
|
+
const maxCandidates = clampCandidateLimit(input.maxCandidates);
|
|
343
|
+
const paths = input.paths.map(normalizeSkillPath);
|
|
344
|
+
const reasons = [...new Set(input.reasons.map((reason) => reason.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right));
|
|
345
|
+
const taskTerms = tokenize(input.taskText ?? '');
|
|
346
|
+
const pathTerms = tokenize(paths.join(' '));
|
|
347
|
+
const pathSkillHints = collectPathSkillHints(paths);
|
|
348
|
+
const routes = readSkillFrontmatterRoutes(projectRoot);
|
|
349
|
+
const metadata = readSkillRouteMetadata(projectRoot);
|
|
350
|
+
const allCandidates = routes
|
|
351
|
+
.map((route) => {
|
|
352
|
+
const skill = skillNameFromPath(route.skillPath);
|
|
353
|
+
return createCandidate(route, metadata.get(skill) ?? {
|
|
354
|
+
category: route.category ?? null,
|
|
355
|
+
routeType: 'unknown',
|
|
356
|
+
priority: 0,
|
|
357
|
+
appliesToReasons: [],
|
|
358
|
+
mutuallyExclusiveWith: [],
|
|
359
|
+
}, taskTerms, pathTerms, pathSkillHints, reasons);
|
|
360
|
+
})
|
|
361
|
+
.filter((candidate) => candidate.score > 0)
|
|
362
|
+
.sort(sortCandidates);
|
|
363
|
+
const candidates = allCandidates.slice(0, maxCandidates);
|
|
364
|
+
const main = candidates.find(isSelectableMain) ?? null;
|
|
365
|
+
const adjuncts = selectAdjuncts(main, candidates, metadata);
|
|
366
|
+
const selected = {
|
|
367
|
+
main,
|
|
368
|
+
adjuncts,
|
|
369
|
+
};
|
|
370
|
+
return {
|
|
371
|
+
schema_version: '1',
|
|
372
|
+
kind: 'skill_route_resolution',
|
|
373
|
+
input: {
|
|
374
|
+
task_text_present: Boolean(input.taskText?.trim()),
|
|
375
|
+
paths,
|
|
376
|
+
reasons,
|
|
377
|
+
max_candidates: maxCandidates,
|
|
378
|
+
},
|
|
379
|
+
signals: {
|
|
380
|
+
task_terms: taskTerms,
|
|
381
|
+
path_terms: pathTerms,
|
|
382
|
+
reasons,
|
|
383
|
+
read_shards: [SKILL_ROUTES_METADATA_PATH, SKILL_FRONTMATTER_SOURCE],
|
|
384
|
+
},
|
|
385
|
+
selected,
|
|
386
|
+
candidates,
|
|
387
|
+
read_plan: createReadPlan(maxCandidates, selected, candidates),
|
|
388
|
+
source_files: [SKILL_ROUTES_METADATA_PATH, SKILL_FRONTMATTER_SOURCE],
|
|
389
|
+
gap_notes: [
|
|
390
|
+
[
|
|
391
|
+
'This resolver is a read-only routing prepass.',
|
|
392
|
+
'It narrows skill candidates from route metadata and skill frontmatter',
|
|
393
|
+
'but does not replace reading the selected SKILL.md.',
|
|
394
|
+
].join(' '),
|
|
395
|
+
'Command execution authority still comes only from .mustflow/config/commands.toml.',
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
@@ -265,11 +265,90 @@ export function sourceAnchorPathIsGeneratedOrVendor(relativePath) {
|
|
|
265
265
|
export function sourceAnchorTextContainsSecretLike(value) {
|
|
266
266
|
return textContainsSecretLike(value);
|
|
267
267
|
}
|
|
268
|
-
|
|
268
|
+
function collectCommentLines(content) {
|
|
269
269
|
const lines = content.split(/\r?\n/);
|
|
270
|
+
const commentLines = [];
|
|
271
|
+
let insideBlockComment = false;
|
|
272
|
+
let stringQuote = null;
|
|
273
|
+
let escaped = false;
|
|
274
|
+
let lineIndex = 0;
|
|
275
|
+
while (lineIndex < lines.length) {
|
|
276
|
+
const line = lines[lineIndex] ?? '';
|
|
277
|
+
let index = 0;
|
|
278
|
+
while (index < line.length) {
|
|
279
|
+
if (insideBlockComment) {
|
|
280
|
+
const endIndex = line.indexOf('*/', index);
|
|
281
|
+
const segmentEnd = endIndex === -1 ? line.length : endIndex;
|
|
282
|
+
commentLines.push({ lineNumber: lineIndex + 1, text: line.slice(index, segmentEnd) });
|
|
283
|
+
if (endIndex === -1) {
|
|
284
|
+
index = line.length;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
insideBlockComment = false;
|
|
288
|
+
index = endIndex + 2;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const character = line[index];
|
|
292
|
+
const nextCharacter = line[index + 1];
|
|
293
|
+
if (stringQuote) {
|
|
294
|
+
if (escaped) {
|
|
295
|
+
escaped = false;
|
|
296
|
+
index += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (character === '\\') {
|
|
300
|
+
escaped = true;
|
|
301
|
+
index += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (character === stringQuote) {
|
|
305
|
+
stringQuote = null;
|
|
306
|
+
}
|
|
307
|
+
index += 1;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (character === '"' || character === "'" || character === '`') {
|
|
311
|
+
stringQuote = character;
|
|
312
|
+
index += 1;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (character === '/' && nextCharacter === '*') {
|
|
316
|
+
const commentStart = index + 2;
|
|
317
|
+
const endIndex = line.indexOf('*/', commentStart);
|
|
318
|
+
const segmentEnd = endIndex === -1 ? line.length : endIndex;
|
|
319
|
+
commentLines.push({ lineNumber: lineIndex + 1, text: line.slice(commentStart, segmentEnd) });
|
|
320
|
+
if (endIndex === -1) {
|
|
321
|
+
insideBlockComment = true;
|
|
322
|
+
index = line.length;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
index = endIndex + 2;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (character === '/' && nextCharacter === '/') {
|
|
329
|
+
commentLines.push({ lineNumber: lineIndex + 1, text: line.slice(index + 2) });
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
if (character === '#' && line.slice(0, index).trim().length === 0) {
|
|
333
|
+
commentLines.push({ lineNumber: lineIndex + 1, text: line.slice(index + 1) });
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
index += 1;
|
|
337
|
+
}
|
|
338
|
+
if (stringQuote !== '`') {
|
|
339
|
+
stringQuote = null;
|
|
340
|
+
}
|
|
341
|
+
escaped = false;
|
|
342
|
+
lineIndex += 1;
|
|
343
|
+
}
|
|
344
|
+
return commentLines;
|
|
345
|
+
}
|
|
346
|
+
export function parseSourceAnchorsInContent(relativePath, content) {
|
|
347
|
+
const lines = collectCommentLines(content);
|
|
270
348
|
const anchors = [];
|
|
271
349
|
for (let index = 0; index < lines.length; index += 1) {
|
|
272
|
-
const
|
|
350
|
+
const commentLine = lines[index];
|
|
351
|
+
const normalized = stripSourceAnchorCommentPrefix(commentLine?.text ?? '');
|
|
273
352
|
if (!normalized.startsWith('mf:anchor')) {
|
|
274
353
|
continue;
|
|
275
354
|
}
|
|
@@ -278,12 +357,18 @@ export function parseSourceAnchorsInContent(relativePath, content) {
|
|
|
278
357
|
const fields = new Map();
|
|
279
358
|
const unsupportedFields = [];
|
|
280
359
|
const rawLines = [normalized];
|
|
360
|
+
let previousLineNumber = commentLine?.lineNumber ?? 0;
|
|
281
361
|
for (let fieldIndex = index + 1; fieldIndex < lines.length; fieldIndex += 1) {
|
|
282
|
-
const
|
|
362
|
+
const nextCommentLine = lines[fieldIndex];
|
|
363
|
+
if (!nextCommentLine || nextCommentLine.lineNumber > previousLineNumber + 1) {
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
const fieldLine = stripSourceAnchorCommentPrefix(nextCommentLine.text);
|
|
283
367
|
if (fieldLine.length === 0) {
|
|
368
|
+
previousLineNumber = nextCommentLine.lineNumber;
|
|
284
369
|
continue;
|
|
285
370
|
}
|
|
286
|
-
if (fieldLine.startsWith('@') || /^[A-Za-z_$][\w$]*\s/u.test(fieldLine)) {
|
|
371
|
+
if (fieldLine.startsWith('mf:anchor') || fieldLine.startsWith('@') || /^[A-Za-z_$][\w$]*\s/u.test(fieldLine)) {
|
|
287
372
|
break;
|
|
288
373
|
}
|
|
289
374
|
const field = readSourceAnchorField(fieldLine);
|
|
@@ -291,6 +376,7 @@ export function parseSourceAnchorsInContent(relativePath, content) {
|
|
|
291
376
|
break;
|
|
292
377
|
}
|
|
293
378
|
rawLines.push(fieldLine);
|
|
379
|
+
previousLineNumber = nextCommentLine.lineNumber;
|
|
294
380
|
if (!SOURCE_ANCHOR_ALLOWED_FIELDS.has(field.key)) {
|
|
295
381
|
unsupportedFields.push(field.key);
|
|
296
382
|
continue;
|
|
@@ -302,7 +388,7 @@ export function parseSourceAnchorsInContent(relativePath, content) {
|
|
|
302
388
|
rawId,
|
|
303
389
|
idValid: SOURCE_ANCHOR_ID_PATTERN.test(rawId),
|
|
304
390
|
path: relativePath,
|
|
305
|
-
lineStart: index + 1,
|
|
391
|
+
lineStart: commentLine?.lineNumber ?? index + 1,
|
|
306
392
|
fields,
|
|
307
393
|
unsupportedFields,
|
|
308
394
|
rawText: rawLines.join('\n'),
|
package/package.json
CHANGED
package/schemas/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Current schemas:
|
|
|
7
7
|
|
|
8
8
|
- `doctor-report.schema.json`: output of `mf doctor --json`
|
|
9
9
|
- `adapter-compatibility-report.schema.json`: output of `mf adapters status --json`
|
|
10
|
-
- `context-report.schema.json`: output of `mf context --json
|
|
10
|
+
- `context-report.schema.json`: output of `mf context --json`, including prompt-cache profiles and optional cache audit data
|
|
11
11
|
- `workspace-summary.schema.json`: output of `mf api workspace-summary --json`
|
|
12
12
|
- `command-catalog.schema.json`: output of `mf api command-catalog --json`
|
|
13
13
|
- `verification-plan.schema.json`: output of `mf api verification-plan --changed --json`
|
|
@@ -51,7 +51,12 @@ Current schemas:
|
|
|
51
51
|
`mf line-endings normalize --json`
|
|
52
52
|
- `quality-gaming-report.schema.json`: output of `mf quality check --json`, containing changed-file
|
|
53
53
|
quality-gaming risks such as line stuffing, validation suppressions, test bypass markers, type
|
|
54
|
-
escapes, generated/vendor logic, and placeholder implementations
|
|
54
|
+
escapes, generated/vendor logic, empty catch swallowing, and placeholder implementations
|
|
55
|
+
- `skill-route-report.schema.json`: output of `mf skill route --json`, containing compact route
|
|
56
|
+
candidates, selected main and adjunct skills, score breakdowns, route read plans, and source
|
|
57
|
+
route shards without granting command authority or replacing selected `SKILL.md` reads
|
|
58
|
+
- `route-fixture.schema.json`: parsed `.mustflow/skills/route-fixtures.json`, containing strict
|
|
59
|
+
skill-route golden cases with required and forbidden route expectations
|
|
55
60
|
- `latest-run-pointer.schema.json`: `.mustflow/state/runs/latest.json` when `mf verify` writes a
|
|
56
61
|
pointer to the latest verify run bundle, including the verify completion verdict, evidence model,
|
|
57
62
|
and coverage matrix
|