mustflow 2.114.6 → 2.114.12
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/dist/cli/commands/skill.js +35 -1
- package/dist/cli/lib/validation/index.js +112 -0
- package/dist/core/check-issues.js +13 -1
- package/dist/core/skill-route-resolution.js +242 -9
- package/package.json +1 -1
- package/schemas/README.md +3 -3
- package/schemas/skill-route-report.schema.json +110 -0
- package/templates/default/i18n.toml +6 -6
- package/templates/default/locales/en/.mustflow/skills/routes.toml +54 -0
- package/templates/default/locales/en/AGENTS.md +10 -1
- package/templates/default/locales/es/AGENTS.md +4 -1
- package/templates/default/locales/fr/AGENTS.md +4 -1
- package/templates/default/locales/hi/AGENTS.md +4 -1
- package/templates/default/locales/ko/AGENTS.md +4 -1
- package/templates/default/locales/zh/AGENTS.md +4 -1
- package/templates/default/manifest.toml +1 -1
|
@@ -111,7 +111,29 @@ function createSkillRouteScriptPackSuggestions(mustflowRoot, report, paths) {
|
|
|
111
111
|
issues: suggestionReport.issues,
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
function routeDependencyReason(candidate) {
|
|
115
|
+
return candidate.selection_reasons.find((reason) => reason.startsWith('route_dependency:')) ?? null;
|
|
116
|
+
}
|
|
117
|
+
function conflictHintCandidates(report) {
|
|
118
|
+
const candidates = [
|
|
119
|
+
...(report.selected.main ? [report.selected.main] : []),
|
|
120
|
+
...report.selected.adjuncts,
|
|
121
|
+
...report.candidates,
|
|
122
|
+
];
|
|
123
|
+
const seenSkills = new Set();
|
|
124
|
+
return candidates.filter((candidate) => {
|
|
125
|
+
if (seenSkills.has(candidate.skill) || candidate.route_card.route_dependencies.conflicts_with.length === 0) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
seenSkills.add(candidate.skill);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
114
132
|
function renderSkillRouteReport(report, lang, warnings = []) {
|
|
133
|
+
const dependencyReads = report.selected.adjuncts
|
|
134
|
+
.map((candidate) => ({ candidate, reason: routeDependencyReason(candidate) }))
|
|
135
|
+
.filter((entry) => entry.reason !== null);
|
|
136
|
+
const conflictHints = conflictHintCandidates(report);
|
|
115
137
|
const lines = [
|
|
116
138
|
'mustflow skill route',
|
|
117
139
|
`${t(lang, 'label.mustflowRoot')}: ${resolveMustflowRoot()}`,
|
|
@@ -125,7 +147,19 @@ function renderSkillRouteReport(report, lang, warnings = []) {
|
|
|
125
147
|
}
|
|
126
148
|
else {
|
|
127
149
|
for (const candidate of report.candidates) {
|
|
128
|
-
lines.push(`- ${candidate.skill} (${candidate.route_type}, score ${candidate.score.toFixed(1)})`, ` path: ${candidate.skill_path}`, ` reasons: ${candidate.selection_reasons.join(', ') || t(lang, 'value.none')}`);
|
|
150
|
+
lines.push(`- ${candidate.skill} (${candidate.route_type}, score ${candidate.score.toFixed(1)})`, ` path: ${candidate.skill_path}`, ` reasons: ${candidate.selection_reasons.join(', ') || t(lang, 'value.none')}`, ` matched_dimensions: ${candidate.matched_dimensions.join(', ') || t(lang, 'value.none')}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (dependencyReads.length > 0) {
|
|
154
|
+
lines.push('', 'Dependency reads');
|
|
155
|
+
for (const { candidate, reason } of dependencyReads) {
|
|
156
|
+
lines.push(`- ${candidate.skill}`, ` reason: ${reason}`, ` path: ${candidate.skill_path}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (conflictHints.length > 0) {
|
|
160
|
+
lines.push('', 'Conflict hints');
|
|
161
|
+
for (const candidate of conflictHints) {
|
|
162
|
+
lines.push(`- ${candidate.skill}`, ` conflicts_with: ${candidate.route_card.route_dependencies.conflicts_with.join(', ')}`, ` path: ${candidate.skill_path}`);
|
|
129
163
|
}
|
|
130
164
|
}
|
|
131
165
|
lines.push('', 'Read plan', ...report.read_plan.selected_skill_paths.map((skillPath) => `- read selected skill: ${skillPath}`), `- avoid by default: ${report.read_plan.avoid_by_default.join(', ') || t(lang, 'value.none')}`, `- fallback route metadata: ${report.read_plan.fallback_route_metadata.path}`, '', 'Source files', ...report.source_files.map((sourceFile) => `- ${sourceFile}`));
|
|
@@ -683,6 +683,63 @@ function readOptionalStringArray(value, label, issues) {
|
|
|
683
683
|
}
|
|
684
684
|
return value.map((entry) => entry.trim());
|
|
685
685
|
}
|
|
686
|
+
function readOptionalSlugArray(value, label, issues) {
|
|
687
|
+
const values = readOptionalStringArray(value, label, issues);
|
|
688
|
+
for (const value of values) {
|
|
689
|
+
if (!/^[a-z][a-z0-9_-]*$/u.test(value)) {
|
|
690
|
+
pushStrictIssue(issues, `${label} entry "${value}" must use lowercase slug text`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return values;
|
|
694
|
+
}
|
|
695
|
+
function readSkillRouteMetadataContexts(value, label, issues) {
|
|
696
|
+
if (value !== undefined && !isRecord(value)) {
|
|
697
|
+
pushStrictIssue(issues, `${label}.contexts must be a TOML table`);
|
|
698
|
+
}
|
|
699
|
+
const contexts = isRecord(value) ? value : {};
|
|
700
|
+
return {
|
|
701
|
+
fileTypes: readOptionalSlugArray(contexts.file_types, `${label}.contexts.file_types`, issues),
|
|
702
|
+
frameworks: readOptionalSlugArray(contexts.frameworks, `${label}.contexts.frameworks`, issues),
|
|
703
|
+
layers: readOptionalSlugArray(contexts.layers, `${label}.contexts.layers`, issues),
|
|
704
|
+
patternCategories: readOptionalSlugArray(contexts.pattern_categories, `${label}.contexts.pattern_categories`, issues),
|
|
705
|
+
positiveTerms: readOptionalSlugArray(contexts.positive_terms, `${label}.contexts.positive_terms`, issues),
|
|
706
|
+
negativeTerms: readOptionalSlugArray(contexts.negative_terms, `${label}.contexts.negative_terms`, issues),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function readSkillRouteUnlockRules(value, label, issues) {
|
|
710
|
+
if (value === undefined) {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
if (!Array.isArray(value) || value.some((entry) => !isRecord(entry))) {
|
|
714
|
+
pushStrictIssue(issues, `${label} must be an array of TOML tables`);
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
return value
|
|
718
|
+
.map((entry, index) => {
|
|
719
|
+
const signal = typeof entry.signal === 'string' ? entry.signal.trim() : '';
|
|
720
|
+
const skill = typeof entry.skill === 'string' ? entry.skill.trim() : '';
|
|
721
|
+
if (!/^[a-z][a-z0-9_-]*$/u.test(signal)) {
|
|
722
|
+
pushStrictIssue(issues, `${label}[${index}].signal must use lowercase slug text`);
|
|
723
|
+
}
|
|
724
|
+
if (!/^[a-z][a-z0-9-]*$/u.test(skill)) {
|
|
725
|
+
pushStrictIssue(issues, `${label}[${index}].skill must be a skill folder name`);
|
|
726
|
+
}
|
|
727
|
+
return { signal, skill };
|
|
728
|
+
})
|
|
729
|
+
.filter((entry) => entry.signal && entry.skill);
|
|
730
|
+
}
|
|
731
|
+
function readSkillRouteMetadataDependencies(value, label, issues) {
|
|
732
|
+
if (value !== undefined && !isRecord(value)) {
|
|
733
|
+
pushStrictIssue(issues, `${label}.dependencies must be a TOML table`);
|
|
734
|
+
}
|
|
735
|
+
const dependencies = isRecord(value) ? value : {};
|
|
736
|
+
return {
|
|
737
|
+
requiresSkills: readOptionalSlugArray(dependencies.requires_skills, `${label}.dependencies.requires_skills`, issues),
|
|
738
|
+
suggestsAdjuncts: readOptionalSlugArray(dependencies.suggests_adjuncts, `${label}.dependencies.suggests_adjuncts`, issues),
|
|
739
|
+
conflictsWith: readOptionalSlugArray(dependencies.conflicts_with, `${label}.dependencies.conflicts_with`, issues),
|
|
740
|
+
unlocksOn: readSkillRouteUnlockRules(dependencies.unlocks_on, `${label}.dependencies.unlocks_on`, issues),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
686
743
|
function validateSkillRouteMetadataTable(skillName, route, issues) {
|
|
687
744
|
const label = `${SKILL_ROUTES_METADATA_PATH} routes.${skillName}`;
|
|
688
745
|
const rawCategory = typeof route.category === 'string' ? route.category : undefined;
|
|
@@ -693,6 +750,8 @@ function validateSkillRouteMetadataTable(skillName, route, issues) {
|
|
|
693
750
|
const profiles = readOptionalStringArray(route.profiles, `${label}.profiles`, issues);
|
|
694
751
|
const appliesToReasons = readOptionalStringArray(route.applies_to_reasons, `${label}.applies_to_reasons`, issues);
|
|
695
752
|
const mutuallyExclusiveWith = readOptionalStringArray(route.mutually_exclusive_with, `${label}.mutually_exclusive_with`, issues);
|
|
753
|
+
const contexts = readSkillRouteMetadataContexts(route.contexts, label, issues);
|
|
754
|
+
const dependencies = readSkillRouteMetadataDependencies(route.dependencies, label, issues);
|
|
696
755
|
if (!category) {
|
|
697
756
|
pushStrictIssue(issues, `${label}.category must be one of ${[...ALLOWED_SKILL_ROUTE_CATEGORIES].join(', ')}`);
|
|
698
757
|
}
|
|
@@ -718,6 +777,8 @@ function validateSkillRouteMetadataTable(skillName, route, issues) {
|
|
|
718
777
|
routeType,
|
|
719
778
|
priority: route.priority,
|
|
720
779
|
mutuallyExclusiveWith,
|
|
780
|
+
contexts,
|
|
781
|
+
dependencies,
|
|
721
782
|
};
|
|
722
783
|
}
|
|
723
784
|
function readSkillRouteMetadata(projectRoot, issues) {
|
|
@@ -759,6 +820,16 @@ function readSkillRouteMetadata(projectRoot, issues) {
|
|
|
759
820
|
}
|
|
760
821
|
return metadata;
|
|
761
822
|
}
|
|
823
|
+
function validateAdjunctRouteDependency(metadata, issues, skillName, targetSkill, relation) {
|
|
824
|
+
const targetRoute = metadata.get(targetSkill);
|
|
825
|
+
if (!targetRoute || targetRoute.routeType === 'adjunct') {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
pushStrictIssue(issues, [
|
|
829
|
+
`${SKILL_ROUTES_METADATA_PATH} route "${skillName}" ${relation} route "${targetSkill}"`,
|
|
830
|
+
` must point to an adjunct route, found "${targetRoute.routeType ?? 'unknown'}"`,
|
|
831
|
+
].join(''));
|
|
832
|
+
}
|
|
762
833
|
function validateSkillRouteMetadataAlignment(metadata, routeSkillNames, expectedSkillNames, issues) {
|
|
763
834
|
for (const skillName of routeSkillNames) {
|
|
764
835
|
if (!metadata.has(skillName)) {
|
|
@@ -783,6 +854,47 @@ function validateSkillRouteMetadataAlignment(metadata, routeSkillNames, expected
|
|
|
783
854
|
pushStrictWarning(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" lists "${otherSkillName}" as mutually exclusive but the reverse route does not`);
|
|
784
855
|
}
|
|
785
856
|
}
|
|
857
|
+
for (const requiredSkill of route.dependencies.requiresSkills) {
|
|
858
|
+
if (requiredSkill === skillName) {
|
|
859
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" cannot require itself`);
|
|
860
|
+
}
|
|
861
|
+
else if (!metadata.has(requiredSkill)) {
|
|
862
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" requires unknown route "${requiredSkill}"`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
for (const adjunctSkill of route.dependencies.suggestsAdjuncts) {
|
|
866
|
+
if (adjunctSkill === skillName) {
|
|
867
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" cannot suggest itself as an adjunct`);
|
|
868
|
+
}
|
|
869
|
+
else if (!metadata.has(adjunctSkill)) {
|
|
870
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" suggests unknown adjunct route "${adjunctSkill}"`);
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
validateAdjunctRouteDependency(metadata, issues, skillName, adjunctSkill, 'suggests adjunct');
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
for (const conflictingSkill of route.dependencies.conflictsWith) {
|
|
877
|
+
if (conflictingSkill === skillName) {
|
|
878
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" cannot conflict with itself`);
|
|
879
|
+
}
|
|
880
|
+
else if (!metadata.has(conflictingSkill)) {
|
|
881
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" conflicts with unknown route "${conflictingSkill}"`);
|
|
882
|
+
}
|
|
883
|
+
else if (!metadata.get(conflictingSkill)?.dependencies.conflictsWith.includes(skillName)) {
|
|
884
|
+
pushStrictWarning(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" conflicts with "${conflictingSkill}" but the reverse route does not`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
for (const unlockRule of route.dependencies.unlocksOn) {
|
|
888
|
+
if (unlockRule.skill === skillName) {
|
|
889
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" cannot unlock itself`);
|
|
890
|
+
}
|
|
891
|
+
else if (!metadata.has(unlockRule.skill)) {
|
|
892
|
+
pushStrictIssue(issues, `${SKILL_ROUTES_METADATA_PATH} route "${skillName}" unlocks unknown route "${unlockRule.skill}" on signal "${unlockRule.signal}"`);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
validateAdjunctRouteDependency(metadata, issues, skillName, unlockRule.skill, `unlocks on signal "${unlockRule.signal}"`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
786
898
|
}
|
|
787
899
|
}
|
|
788
900
|
function validateSkillIndexRoutes(projectRoot, commandsToml, skillFiles, issues) {
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
const ROUTE_METADATA_UNKNOWN_REFERENCE_PATTERN = new RegExp('^Strict: \\.mustflow\\/skills\\/routes\\.toml route "[^"]+" ' +
|
|
2
|
+
'(?:references unknown mutually exclusive route|requires unknown route|' +
|
|
3
|
+
'suggests unknown adjunct route|conflicts with unknown route|unlocks unknown route) ' +
|
|
4
|
+
'"[^"]+"(?: on signal "[^"]+")?$', 'u');
|
|
5
|
+
const ROUTE_METADATA_INVALID_DEPENDENCY_PATTERN = new RegExp('^Strict: \\.mustflow\\/skills\\/routes\\.toml route "[^"]+" ' +
|
|
6
|
+
'(?:suggests adjunct|unlocks on signal "[^"]+") route "[^"]+" ' +
|
|
7
|
+
'must point to an adjunct route, found "[^"]+"$', 'u');
|
|
1
8
|
const CHECK_ISSUE_ID_RULES = [
|
|
2
9
|
['mustflow.command_contract.intent_table_missing', /^Missing \[intents\] table in \.mustflow\/config\/commands\.toml$/u],
|
|
3
10
|
['mustflow.command_contract.intent_not_table', /^Intent [^\s]+ must be a TOML table$/u],
|
|
@@ -58,8 +65,13 @@ const CHECK_ISSUE_ID_RULES = [
|
|
|
58
65
|
['mustflow.skill.route_metadata_unlisted', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" is not listed in \.mustflow\/skills\/INDEX\.md$/u],
|
|
59
66
|
['mustflow.skill.route_metadata_missing_document', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" points to a missing skill document$/u],
|
|
60
67
|
['mustflow.skill.route_metadata_category_mismatch', /^Strict: \.mustflow\/skills\/INDEX\.md route "[^"]+" must appear under the .+ category section from \.mustflow\/skills\/routes\.toml$/u],
|
|
61
|
-
[
|
|
68
|
+
[
|
|
69
|
+
'mustflow.skill.route_metadata_unknown_reference',
|
|
70
|
+
ROUTE_METADATA_UNKNOWN_REFERENCE_PATTERN,
|
|
71
|
+
],
|
|
72
|
+
['mustflow.skill.route_metadata_invalid_dependency', ROUTE_METADATA_INVALID_DEPENDENCY_PATTERN],
|
|
62
73
|
['mustflow.skill.route_metadata_asymmetric_exclusion', /^Strict warning: \.mustflow\/skills\/routes\.toml route "[^"]+" lists "[^"]+" as mutually exclusive but the reverse route does not$/u],
|
|
74
|
+
['mustflow.skill.route_metadata_asymmetric_conflict', /^Strict warning: \.mustflow\/skills\/routes\.toml route "[^"]+" conflicts with "[^"]+" but the reverse route does not$/u],
|
|
63
75
|
['mustflow.skill.route_fixture_invalid', /^Strict: \.mustflow\/skills\/route-fixtures\.json (?:must|is not valid JSON|cases\[\d+\])/u],
|
|
64
76
|
['mustflow.skill.route_fixture_mismatch', /^Strict: Skill route fixture "[^"]+" (?:expected|forbids)/u],
|
|
65
77
|
['mustflow.skill.template_profile_empty_category', /^Strict: template profile "[^"]+" (?:skill index category ".+" has no route rows|route category gate references ".+" without route rows)$/u],
|
|
@@ -13,6 +13,10 @@ const DEFAULT_MAX_CANDIDATES = 5;
|
|
|
13
13
|
const DEFAULT_MAX_MAIN = 1;
|
|
14
14
|
const DEFAULT_MAX_ADJUNCTS = 2;
|
|
15
15
|
const PATH_SKILL_HINT_SCORE = 25;
|
|
16
|
+
const PATTERN_SIGNAL_TERM_SCORE = 12;
|
|
17
|
+
const PATTERN_SIGNAL_MAX_SCORE = 48;
|
|
18
|
+
const NEGATIVE_SIGNAL_TERM_PENALTY = -25;
|
|
19
|
+
const NEGATIVE_SIGNAL_MAX_PENALTY = -75;
|
|
16
20
|
const DOCS_TREE_MARKDOWN_PATH_PATTERN = /(?:^|\/)(?:docs|docs-site|documentation|\.mustflow\/docs|\.mustflow\/context)\/.+\.(?:md|mdx)$/u;
|
|
17
21
|
const ROOT_DOCUMENT_BASENAMES = [
|
|
18
22
|
'readme',
|
|
@@ -33,6 +37,12 @@ const ROOT_DOCUMENT_BASENAMES = [
|
|
|
33
37
|
'architecture',
|
|
34
38
|
'api',
|
|
35
39
|
];
|
|
40
|
+
const EMPTY_ROUTE_DEPENDENCIES = {
|
|
41
|
+
requires_skills: [],
|
|
42
|
+
suggests_adjuncts: [],
|
|
43
|
+
conflicts_with: [],
|
|
44
|
+
unlocks_on: [],
|
|
45
|
+
};
|
|
36
46
|
const ROUTE_TYPE_WEIGHTS = {
|
|
37
47
|
primary: 18,
|
|
38
48
|
authoring: 16,
|
|
@@ -101,6 +111,46 @@ function readStringArrayFromTable(table, key) {
|
|
|
101
111
|
? value.map((entry) => entry.trim()).filter(Boolean)
|
|
102
112
|
: [];
|
|
103
113
|
}
|
|
114
|
+
function readRouteSignalProfile(route) {
|
|
115
|
+
const contexts = route.contexts;
|
|
116
|
+
if (!isRecord(contexts)) {
|
|
117
|
+
return {
|
|
118
|
+
positiveTerms: [],
|
|
119
|
+
negativeTerms: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
positiveTerms: tokenize(readStringArrayFromTable(contexts, 'positive_terms').join(' ')),
|
|
124
|
+
negativeTerms: tokenize(readStringArrayFromTable(contexts, 'negative_terms').join(' ')),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function readRouteUnlockRules(table) {
|
|
128
|
+
const value = table.unlocks_on;
|
|
129
|
+
if (!Array.isArray(value)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
return value
|
|
133
|
+
.filter(isRecord)
|
|
134
|
+
.map((entry) => {
|
|
135
|
+
return {
|
|
136
|
+
signal: typeof entry.signal === 'string' ? entry.signal.trim() : '',
|
|
137
|
+
skill: typeof entry.skill === 'string' ? entry.skill.trim() : '',
|
|
138
|
+
};
|
|
139
|
+
})
|
|
140
|
+
.filter((entry) => entry.signal && entry.skill);
|
|
141
|
+
}
|
|
142
|
+
function readRouteDependencies(route) {
|
|
143
|
+
const dependencies = route.dependencies;
|
|
144
|
+
if (!isRecord(dependencies)) {
|
|
145
|
+
return EMPTY_ROUTE_DEPENDENCIES;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
requires_skills: readStringArrayFromTable(dependencies, 'requires_skills'),
|
|
149
|
+
suggests_adjuncts: readStringArrayFromTable(dependencies, 'suggests_adjuncts'),
|
|
150
|
+
conflicts_with: readStringArrayFromTable(dependencies, 'conflicts_with'),
|
|
151
|
+
unlocks_on: readRouteUnlockRules(dependencies),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
104
154
|
function readFrontmatterBlock(content) {
|
|
105
155
|
if (!content.startsWith('---')) {
|
|
106
156
|
return [];
|
|
@@ -174,6 +224,8 @@ function readSkillRouteMetadata(projectRoot) {
|
|
|
174
224
|
priority: Number.isInteger(route.priority) ? Number(route.priority) : 0,
|
|
175
225
|
appliesToReasons: readStringArrayFromTable(route, 'applies_to_reasons'),
|
|
176
226
|
mutuallyExclusiveWith: readStringArrayFromTable(route, 'mutually_exclusive_with'),
|
|
227
|
+
signalProfile: readRouteSignalProfile(route),
|
|
228
|
+
dependencies: readRouteDependencies(route),
|
|
177
229
|
});
|
|
178
230
|
}
|
|
179
231
|
}
|
|
@@ -271,6 +323,10 @@ function countMatches(needles, haystack) {
|
|
|
271
323
|
const haystackSet = new Set(haystack);
|
|
272
324
|
return needles.filter((needle) => haystackSet.has(needle)).length;
|
|
273
325
|
}
|
|
326
|
+
function collectMatchedTerms(needles, haystack) {
|
|
327
|
+
const haystackSet = new Set(haystack);
|
|
328
|
+
return [...new Set(needles.filter((needle) => haystackSet.has(needle)))].sort((left, right) => left.localeCompare(right));
|
|
329
|
+
}
|
|
274
330
|
function routeTextTerms(route, skillName) {
|
|
275
331
|
return tokenize([
|
|
276
332
|
skillName,
|
|
@@ -282,6 +338,65 @@ function routeTextTerms(route, skillName) {
|
|
|
282
338
|
route.skillPath,
|
|
283
339
|
].join(' '));
|
|
284
340
|
}
|
|
341
|
+
function createExcerptReference(skillPath, section) {
|
|
342
|
+
return {
|
|
343
|
+
source_path: skillPath,
|
|
344
|
+
section,
|
|
345
|
+
read_when: [
|
|
346
|
+
'candidate scores tie within the same route category',
|
|
347
|
+
'matched dimensions are too broad to choose a single skill',
|
|
348
|
+
'task language overlaps a listed negative or conflicting signal',
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function createRouteCard(skillPath, matchedDimensions, routeDependencies) {
|
|
353
|
+
return {
|
|
354
|
+
source: 'route_metadata_and_skill_frontmatter',
|
|
355
|
+
index_read_policy: 'fallback_only',
|
|
356
|
+
compact_fields: [
|
|
357
|
+
'skill',
|
|
358
|
+
'skill_path',
|
|
359
|
+
'trigger',
|
|
360
|
+
'category',
|
|
361
|
+
'route_type',
|
|
362
|
+
'priority',
|
|
363
|
+
'applies_to_reasons',
|
|
364
|
+
'score_breakdown',
|
|
365
|
+
'selection_reasons',
|
|
366
|
+
'verification_intents',
|
|
367
|
+
'route_dependencies',
|
|
368
|
+
],
|
|
369
|
+
matched_dimensions: matchedDimensions,
|
|
370
|
+
route_dependencies: routeDependencies,
|
|
371
|
+
use_when_excerpt: createExcerptReference(skillPath, 'use-when'),
|
|
372
|
+
do_not_use_excerpt: createExcerptReference(skillPath, 'do-not-use-when'),
|
|
373
|
+
read_strategy: [
|
|
374
|
+
'Read the selected SKILL.md before editing matching scope.',
|
|
375
|
+
'For close ties, compare only Use When and Do Not Use When excerpts before loading full competing skills.',
|
|
376
|
+
'Use route_dependencies to add required or suggested adjunct skill reads without loading the expanded index.',
|
|
377
|
+
'Keep .mustflow/skills/INDEX.md out of the prompt unless route metadata and excerpts are insufficient.',
|
|
378
|
+
],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function createPatternSignalBreakdown(signalProfile, taskTerms, pathTerms) {
|
|
382
|
+
if (signalProfile.positiveTerms.length === 0 && signalProfile.negativeTerms.length === 0) {
|
|
383
|
+
return {
|
|
384
|
+
positiveMatches: [],
|
|
385
|
+
negativeMatches: [],
|
|
386
|
+
patternScore: 0,
|
|
387
|
+
negativePenalty: 0,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const inputTerms = [...new Set([...taskTerms, ...pathTerms])];
|
|
391
|
+
const positiveMatches = collectMatchedTerms(signalProfile.positiveTerms, inputTerms);
|
|
392
|
+
const negativeMatches = collectMatchedTerms(signalProfile.negativeTerms, inputTerms);
|
|
393
|
+
return {
|
|
394
|
+
positiveMatches,
|
|
395
|
+
negativeMatches,
|
|
396
|
+
patternScore: Math.min(positiveMatches.length * PATTERN_SIGNAL_TERM_SCORE, PATTERN_SIGNAL_MAX_SCORE),
|
|
397
|
+
negativePenalty: Math.max(negativeMatches.length * NEGATIVE_SIGNAL_TERM_PENALTY, NEGATIVE_SIGNAL_MAX_PENALTY),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
285
400
|
function createCandidate(route, metadata, taskTerms, pathTerms, pathSkillHints, reasons) {
|
|
286
401
|
const skill = skillNameFromPath(route.skillPath);
|
|
287
402
|
const terms = routeTextTerms(route, skill);
|
|
@@ -289,19 +404,38 @@ function createCandidate(route, metadata, taskTerms, pathTerms, pathSkillHints,
|
|
|
289
404
|
const taskMatches = countMatches(taskTerms, terms);
|
|
290
405
|
const pathMatches = countMatches(pathTerms, terms);
|
|
291
406
|
const pathSkillHintMatched = pathSkillHints.has(skill);
|
|
407
|
+
const patternSignals = createPatternSignalBreakdown(metadata.signalProfile, taskTerms, pathTerms);
|
|
292
408
|
const breakdown = {
|
|
293
409
|
reason_match: matchedReasons.length * 35,
|
|
294
410
|
task_text_match: taskMatches * 6,
|
|
295
411
|
path_match: pathMatches * 6 + (pathSkillHintMatched ? PATH_SKILL_HINT_SCORE : 0),
|
|
412
|
+
pattern_signal_match: patternSignals.patternScore,
|
|
413
|
+
negative_signal_penalty: patternSignals.negativePenalty,
|
|
296
414
|
route_type_weight: ROUTE_TYPE_WEIGHTS[metadata.routeType] ?? 0,
|
|
297
415
|
priority_weight: Math.max(0, Math.min(metadata.priority, 100)) / 5,
|
|
298
416
|
};
|
|
299
417
|
const score = Object.values(breakdown).reduce((total, value) => total + value, 0);
|
|
418
|
+
const matchedDimensions = [
|
|
419
|
+
...(matchedReasons.length > 0 ? ['reason'] : []),
|
|
420
|
+
...(taskMatches > 0 ? ['task_terms'] : []),
|
|
421
|
+
...(pathMatches > 0 ? ['path_terms'] : []),
|
|
422
|
+
...(pathSkillHintMatched ? ['path_skill_hint'] : []),
|
|
423
|
+
...(patternSignals.positiveMatches.length > 0 ? ['pattern_signal'] : []),
|
|
424
|
+
...(patternSignals.negativeMatches.length > 0 ? ['negative_signal'] : []),
|
|
425
|
+
...(metadata.routeType !== 'unknown' ? ['route_type'] : []),
|
|
426
|
+
...(metadata.priority > 0 ? ['priority'] : []),
|
|
427
|
+
];
|
|
300
428
|
const selectionReasons = [
|
|
301
429
|
...matchedReasons.map((reason) => `reason:${reason}`),
|
|
302
430
|
...(taskMatches > 0 ? [`task_terms:${taskMatches}`] : []),
|
|
303
431
|
...(pathMatches > 0 ? [`path_terms:${pathMatches}`] : []),
|
|
304
432
|
...(pathSkillHintMatched ? [`path_skill_hint:${skill}`] : []),
|
|
433
|
+
...(patternSignals.positiveMatches.length > 0
|
|
434
|
+
? [`pattern_terms:${patternSignals.positiveMatches.join('|')}`]
|
|
435
|
+
: []),
|
|
436
|
+
...(patternSignals.negativeMatches.length > 0
|
|
437
|
+
? [`negative_terms:${patternSignals.negativeMatches.join('|')}`]
|
|
438
|
+
: []),
|
|
305
439
|
`route_type:${metadata.routeType}`,
|
|
306
440
|
`priority:${metadata.priority}`,
|
|
307
441
|
];
|
|
@@ -316,6 +450,8 @@ function createCandidate(route, metadata, taskTerms, pathTerms, pathSkillHints,
|
|
|
316
450
|
score,
|
|
317
451
|
score_breakdown: breakdown,
|
|
318
452
|
selection_reasons: selectionReasons,
|
|
453
|
+
matched_dimensions: matchedDimensions,
|
|
454
|
+
route_card: createRouteCard(route.skillPath, matchedDimensions, metadata.dependencies),
|
|
319
455
|
verification_intents: route.commandIntents,
|
|
320
456
|
};
|
|
321
457
|
}
|
|
@@ -333,20 +469,107 @@ function sortCandidates(left, right) {
|
|
|
333
469
|
function isSelectableMain(candidate) {
|
|
334
470
|
return candidate.route_type === 'primary' || candidate.route_type === 'authoring' || candidate.route_type === 'external';
|
|
335
471
|
}
|
|
336
|
-
function
|
|
472
|
+
function hasDependencySignal(signal, dependencySignals, taskTerms, pathTerms) {
|
|
473
|
+
if (dependencySignals.has(signal)) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
const inputTerms = new Set([...taskTerms, ...pathTerms]);
|
|
477
|
+
const signalTerms = signal
|
|
478
|
+
.split('_')
|
|
479
|
+
.map((term) => (term === 'changed' ? 'change' : term))
|
|
480
|
+
.filter((term) => term !== 'or');
|
|
481
|
+
return signalTerms.length > 0 && signalTerms.every((term) => inputTerms.has(term));
|
|
482
|
+
}
|
|
483
|
+
function collectDependencySignals(paths, reasons, taskTerms, pathTerms) {
|
|
484
|
+
const dependencySignals = new Set(reasons);
|
|
485
|
+
const inputTerms = new Set([...taskTerms, ...pathTerms]);
|
|
486
|
+
if (inputTerms.has('output') &&
|
|
487
|
+
(inputTerms.has('machine') || inputTerms.has('json') || inputTerms.has('jsonl') || inputTerms.has('cli'))) {
|
|
488
|
+
dependencySignals.add('machine_output_changed');
|
|
489
|
+
}
|
|
490
|
+
if (inputTerms.has('schema') ||
|
|
491
|
+
inputTerms.has('fixture') ||
|
|
492
|
+
paths.some((pathValue) => /(?:^|\/)(?:schemas|fixtures|tests\/fixtures)(?:\/|$)/u.test(pathValue))) {
|
|
493
|
+
dependencySignals.add('schema_or_fixture_changed');
|
|
494
|
+
}
|
|
495
|
+
if (inputTerms.has('followup') ||
|
|
496
|
+
(inputTerms.has('follow') && inputTerms.has('up')) ||
|
|
497
|
+
(inputTerms.has('next') && inputTerms.has('action'))) {
|
|
498
|
+
dependencySignals.add('concrete_followup_exists');
|
|
499
|
+
}
|
|
500
|
+
return dependencySignals;
|
|
501
|
+
}
|
|
502
|
+
function addDependencySelectionReason(candidate, reason) {
|
|
503
|
+
const matchedDimensions = [...new Set([...candidate.matched_dimensions, 'route_dependency'])];
|
|
504
|
+
return {
|
|
505
|
+
...candidate,
|
|
506
|
+
selection_reasons: [...new Set([...candidate.selection_reasons, reason])],
|
|
507
|
+
matched_dimensions: matchedDimensions,
|
|
508
|
+
route_card: {
|
|
509
|
+
...candidate.route_card,
|
|
510
|
+
matched_dimensions: matchedDimensions,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function routeConflictsFor(candidate, metadata) {
|
|
515
|
+
const routeMetadata = metadata.get(candidate.skill);
|
|
516
|
+
return new Set([
|
|
517
|
+
...(routeMetadata?.mutuallyExclusiveWith ?? []),
|
|
518
|
+
...candidate.route_card.route_dependencies.conflicts_with,
|
|
519
|
+
]);
|
|
520
|
+
}
|
|
521
|
+
function routesConflict(left, right, metadata) {
|
|
522
|
+
return routeConflictsFor(left, metadata).has(right.skill) || routeConflictsFor(right, metadata).has(left.skill);
|
|
523
|
+
}
|
|
524
|
+
function selectAdjuncts(main, scoredCandidates, allCandidatesBySkill, metadata, dependencySignals, taskTerms, pathTerms) {
|
|
337
525
|
if (!main) {
|
|
338
526
|
return [];
|
|
339
527
|
}
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
528
|
+
const selectedMain = main;
|
|
529
|
+
const mainMetadata = metadata.get(selectedMain.skill);
|
|
530
|
+
const excluded = new Set([
|
|
531
|
+
selectedMain.skill,
|
|
532
|
+
...(mainMetadata?.mutuallyExclusiveWith ?? []),
|
|
533
|
+
...selectedMain.route_card.route_dependencies.conflicts_with,
|
|
534
|
+
]);
|
|
535
|
+
const selected = [];
|
|
536
|
+
function addDependencySkill(skill, reason) {
|
|
537
|
+
const dependencyCandidate = allCandidatesBySkill.get(skill);
|
|
538
|
+
if (!dependencyCandidate || excluded.has(dependencyCandidate.skill)) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if ([selectedMain, ...selected].some((candidate) => routesConflict(candidate, dependencyCandidate, metadata))) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (selected.some((candidate) => candidate.skill === dependencyCandidate.skill)) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
selected.push(addDependencySelectionReason(dependencyCandidate, reason));
|
|
548
|
+
}
|
|
549
|
+
for (const skill of selectedMain.route_card.route_dependencies.requires_skills) {
|
|
550
|
+
addDependencySkill(skill, `route_dependency:requires:${selectedMain.skill}`);
|
|
551
|
+
}
|
|
552
|
+
for (const skill of selectedMain.route_card.route_dependencies.suggests_adjuncts) {
|
|
553
|
+
addDependencySkill(skill, `route_dependency:suggested_by:${selectedMain.skill}`);
|
|
554
|
+
}
|
|
555
|
+
for (const unlockRule of selectedMain.route_card.route_dependencies.unlocks_on) {
|
|
556
|
+
if (hasDependencySignal(unlockRule.signal, dependencySignals, taskTerms, pathTerms)) {
|
|
557
|
+
addDependencySkill(unlockRule.skill, `route_dependency:unlocked_by:${selectedMain.skill}:${unlockRule.signal}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (selected.length >= DEFAULT_MAX_ADJUNCTS) {
|
|
561
|
+
return selected.slice(0, DEFAULT_MAX_ADJUNCTS);
|
|
562
|
+
}
|
|
563
|
+
const scoredAdjuncts = scoredCandidates
|
|
343
564
|
.filter((candidate) => {
|
|
344
565
|
return (candidate.route_type === 'adjunct' &&
|
|
345
566
|
candidate.category === main.category &&
|
|
346
|
-
!excluded.has(candidate.skill)
|
|
567
|
+
!excluded.has(candidate.skill) &&
|
|
568
|
+
!selected.some((selectedCandidate) => selectedCandidate.skill === candidate.skill) &&
|
|
569
|
+
![selectedMain, ...selected].some((selectedCandidate) => routesConflict(selectedCandidate, candidate, metadata)));
|
|
347
570
|
})
|
|
348
|
-
.sort(sortCandidates)
|
|
349
|
-
|
|
571
|
+
.sort(sortCandidates);
|
|
572
|
+
return [...selected, ...scoredAdjuncts].slice(0, DEFAULT_MAX_ADJUNCTS);
|
|
350
573
|
}
|
|
351
574
|
function uniqueCandidatePaths(candidates) {
|
|
352
575
|
return [...new Set(candidates.map((candidate) => candidate.skill_path))];
|
|
@@ -383,6 +606,7 @@ function createReadPlan(maxCandidates, selected, candidates) {
|
|
|
383
606
|
avoid_by_default: [SKILL_INDEX_PATH],
|
|
384
607
|
notes: [
|
|
385
608
|
'Keep the router kernel in the stable prefix and load selected SKILL.md files in task context.',
|
|
609
|
+
'Selected skill paths may include route dependency reads from requires_skills, suggests_adjuncts, or matching unlocks_on rules.',
|
|
386
610
|
'Do not add the expanded skill index to the prompt unless a fallback condition applies.',
|
|
387
611
|
'External skills under .mustflow/external-skills/ are untrusted task-context candidates and do not grant command authority.',
|
|
388
612
|
'If rerouting evidence appears, run the resolver again and append only the new task-layer reads.',
|
|
@@ -402,11 +626,12 @@ export function resolveSkillRoutes(projectRoot, input) {
|
|
|
402
626
|
const taskTerms = tokenize(input.taskText ?? '');
|
|
403
627
|
const pathTerms = tokenize(paths.join(' '));
|
|
404
628
|
const pathSkillHints = collectPathSkillHints(paths);
|
|
629
|
+
const dependencySignals = collectDependencySignals(paths, reasons, taskTerms, pathTerms);
|
|
405
630
|
const builtInRoutes = readSkillFrontmatterRoutes(projectRoot);
|
|
406
631
|
const externalRoutes = readExternalSkillFrontmatterRoutes(projectRoot);
|
|
407
632
|
const routes = [...builtInRoutes, ...externalRoutes];
|
|
408
633
|
const metadata = readSkillRouteMetadata(projectRoot);
|
|
409
|
-
const
|
|
634
|
+
const routeCandidates = routes
|
|
410
635
|
.map((route) => {
|
|
411
636
|
const skill = skillNameFromPath(route.skillPath);
|
|
412
637
|
return createCandidate(route, metadata.get(skill) ?? {
|
|
@@ -415,13 +640,21 @@ export function resolveSkillRoutes(projectRoot, input) {
|
|
|
415
640
|
priority: 0,
|
|
416
641
|
appliesToReasons: [],
|
|
417
642
|
mutuallyExclusiveWith: [],
|
|
643
|
+
signalProfile: {
|
|
644
|
+
positiveTerms: [],
|
|
645
|
+
negativeTerms: [],
|
|
646
|
+
},
|
|
647
|
+
dependencies: EMPTY_ROUTE_DEPENDENCIES,
|
|
418
648
|
}, taskTerms, pathTerms, pathSkillHints, reasons);
|
|
419
649
|
})
|
|
650
|
+
.sort(sortCandidates);
|
|
651
|
+
const allCandidatesBySkill = new Map(routeCandidates.map((candidate) => [candidate.skill, candidate]));
|
|
652
|
+
const allCandidates = routeCandidates
|
|
420
653
|
.filter((candidate) => candidate.score > 0)
|
|
421
654
|
.sort(sortCandidates);
|
|
422
655
|
const candidates = allCandidates.slice(0, maxCandidates);
|
|
423
656
|
const main = candidates.find(isSelectableMain) ?? null;
|
|
424
|
-
const adjuncts = selectAdjuncts(main, candidates, metadata);
|
|
657
|
+
const adjuncts = selectAdjuncts(main, candidates, allCandidatesBySkill, metadata, dependencySignals, taskTerms, pathTerms);
|
|
425
658
|
const selected = {
|
|
426
659
|
main,
|
|
427
660
|
adjuncts,
|
package/package.json
CHANGED
package/schemas/README.md
CHANGED
|
@@ -189,9 +189,9 @@ Current schemas:
|
|
|
189
189
|
candidates from direct imports, importers, same-basename siblings, parent configuration files, and
|
|
190
190
|
package boundaries for source-oriented repository navigation
|
|
191
191
|
- `skill-route-report.schema.json`: output of `mf skill route --json`, containing compact route
|
|
192
|
-
candidates, selected main and adjunct skills, score breakdowns, route
|
|
193
|
-
shards, and optional read-only script-pack helper suggestions without
|
|
194
|
-
or replacing selected `SKILL.md` reads
|
|
192
|
+
candidates, selected main and adjunct skills, score breakdowns, route cards with dependencies,
|
|
193
|
+
route read plans, source route shards, and optional read-only script-pack helper suggestions without
|
|
194
|
+
granting command authority or replacing selected `SKILL.md` reads
|
|
195
195
|
- `skill-import-report.schema.json`: output of `mf skill import <github-url> --json`, containing
|
|
196
196
|
GitHub source provenance, target `.mustflow/external-skills/<name>/` paths, imported file hashes,
|
|
197
197
|
warnings for inert external scripts, rejection issues, and whether files were written
|
|
@@ -350,6 +350,8 @@
|
|
|
350
350
|
"reason_match": { "type": "number" },
|
|
351
351
|
"task_text_match": { "type": "number" },
|
|
352
352
|
"path_match": { "type": "number" },
|
|
353
|
+
"pattern_signal_match": { "type": "number" },
|
|
354
|
+
"negative_signal_penalty": { "type": "number" },
|
|
353
355
|
"route_type_weight": { "type": "number" },
|
|
354
356
|
"priority_weight": { "type": "number" }
|
|
355
357
|
}
|
|
@@ -358,12 +360,120 @@
|
|
|
358
360
|
"type": "array",
|
|
359
361
|
"items": { "type": "string" }
|
|
360
362
|
},
|
|
363
|
+
"matched_dimensions": {
|
|
364
|
+
"type": "array",
|
|
365
|
+
"items": {
|
|
366
|
+
"type": "string",
|
|
367
|
+
"enum": [
|
|
368
|
+
"reason",
|
|
369
|
+
"task_terms",
|
|
370
|
+
"path_terms",
|
|
371
|
+
"path_skill_hint",
|
|
372
|
+
"pattern_signal",
|
|
373
|
+
"negative_signal",
|
|
374
|
+
"route_type",
|
|
375
|
+
"priority"
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
"route_card": {
|
|
380
|
+
"type": "object",
|
|
381
|
+
"additionalProperties": false,
|
|
382
|
+
"required": [
|
|
383
|
+
"source",
|
|
384
|
+
"index_read_policy",
|
|
385
|
+
"compact_fields",
|
|
386
|
+
"matched_dimensions",
|
|
387
|
+
"route_dependencies",
|
|
388
|
+
"use_when_excerpt",
|
|
389
|
+
"do_not_use_excerpt",
|
|
390
|
+
"read_strategy"
|
|
391
|
+
],
|
|
392
|
+
"properties": {
|
|
393
|
+
"source": { "const": "route_metadata_and_skill_frontmatter" },
|
|
394
|
+
"index_read_policy": { "const": "fallback_only" },
|
|
395
|
+
"compact_fields": {
|
|
396
|
+
"type": "array",
|
|
397
|
+
"items": { "type": "string" }
|
|
398
|
+
},
|
|
399
|
+
"matched_dimensions": {
|
|
400
|
+
"type": "array",
|
|
401
|
+
"items": {
|
|
402
|
+
"type": "string",
|
|
403
|
+
"enum": [
|
|
404
|
+
"reason",
|
|
405
|
+
"task_terms",
|
|
406
|
+
"path_terms",
|
|
407
|
+
"path_skill_hint",
|
|
408
|
+
"pattern_signal",
|
|
409
|
+
"negative_signal",
|
|
410
|
+
"route_type",
|
|
411
|
+
"priority"
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
"route_dependencies": { "$ref": "#/$defs/routeDependencies" },
|
|
416
|
+
"use_when_excerpt": { "$ref": "#/$defs/routeExcerptReference" },
|
|
417
|
+
"do_not_use_excerpt": { "$ref": "#/$defs/routeExcerptReference" },
|
|
418
|
+
"read_strategy": {
|
|
419
|
+
"type": "array",
|
|
420
|
+
"items": { "type": "string" },
|
|
421
|
+
"minItems": 1
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
},
|
|
361
425
|
"verification_intents": {
|
|
362
426
|
"type": "array",
|
|
363
427
|
"items": { "type": "string" }
|
|
364
428
|
}
|
|
365
429
|
}
|
|
366
430
|
},
|
|
431
|
+
"routeDependencies": {
|
|
432
|
+
"type": "object",
|
|
433
|
+
"additionalProperties": false,
|
|
434
|
+
"required": ["requires_skills", "suggests_adjuncts", "conflicts_with", "unlocks_on"],
|
|
435
|
+
"properties": {
|
|
436
|
+
"requires_skills": {
|
|
437
|
+
"type": "array",
|
|
438
|
+
"items": { "type": "string" }
|
|
439
|
+
},
|
|
440
|
+
"suggests_adjuncts": {
|
|
441
|
+
"type": "array",
|
|
442
|
+
"items": { "type": "string" }
|
|
443
|
+
},
|
|
444
|
+
"conflicts_with": {
|
|
445
|
+
"type": "array",
|
|
446
|
+
"items": { "type": "string" }
|
|
447
|
+
},
|
|
448
|
+
"unlocks_on": {
|
|
449
|
+
"type": "array",
|
|
450
|
+
"items": { "$ref": "#/$defs/routeUnlockRule" }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
"routeUnlockRule": {
|
|
455
|
+
"type": "object",
|
|
456
|
+
"additionalProperties": false,
|
|
457
|
+
"required": ["signal", "skill"],
|
|
458
|
+
"properties": {
|
|
459
|
+
"signal": { "type": "string" },
|
|
460
|
+
"skill": { "type": "string" }
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
"routeExcerptReference": {
|
|
464
|
+
"type": "object",
|
|
465
|
+
"additionalProperties": false,
|
|
466
|
+
"required": ["source_path", "section", "read_when"],
|
|
467
|
+
"properties": {
|
|
468
|
+
"source_path": { "type": "string" },
|
|
469
|
+
"section": { "type": "string", "enum": ["use-when", "do-not-use-when"] },
|
|
470
|
+
"read_when": {
|
|
471
|
+
"type": "array",
|
|
472
|
+
"items": { "type": "string" },
|
|
473
|
+
"minItems": 1
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
},
|
|
367
477
|
"readPlanFile": {
|
|
368
478
|
"type": "object",
|
|
369
479
|
"additionalProperties": false,
|
|
@@ -10,12 +10,12 @@ status_values = ["current", "stale", "needs_review", "missing"]
|
|
|
10
10
|
[documents."agents.root"]
|
|
11
11
|
source = "locales/en/AGENTS.md"
|
|
12
12
|
source_locale = "en"
|
|
13
|
-
revision =
|
|
14
|
-
translations.ko = { path = "locales/ko/AGENTS.md", source_revision =
|
|
15
|
-
translations.zh = { path = "locales/zh/AGENTS.md", source_revision =
|
|
16
|
-
translations.es = { path = "locales/es/AGENTS.md", source_revision =
|
|
17
|
-
translations.fr = { path = "locales/fr/AGENTS.md", source_revision =
|
|
18
|
-
translations.hi = { path = "locales/hi/AGENTS.md", source_revision =
|
|
13
|
+
revision = 22
|
|
14
|
+
translations.ko = { path = "locales/ko/AGENTS.md", source_revision = 22, status = "current" }
|
|
15
|
+
translations.zh = { path = "locales/zh/AGENTS.md", source_revision = 22, status = "needs_review" }
|
|
16
|
+
translations.es = { path = "locales/es/AGENTS.md", source_revision = 22, status = "needs_review" }
|
|
17
|
+
translations.fr = { path = "locales/fr/AGENTS.md", source_revision = 22, status = "needs_review" }
|
|
18
|
+
translations.hi = { path = "locales/hi/AGENTS.md", source_revision = 22, status = "needs_review" }
|
|
19
19
|
|
|
20
20
|
[documents."context.index"]
|
|
21
21
|
source = "locales/en/.mustflow/context/INDEX.md"
|
|
@@ -48,18 +48,42 @@ route_type = "primary"
|
|
|
48
48
|
priority = 80
|
|
49
49
|
applies_to_reasons = ["code_change"]
|
|
50
50
|
|
|
51
|
+
[routes."composition-over-inheritance".contexts]
|
|
52
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
53
|
+
layers = ["domain", "service", "model"]
|
|
54
|
+
pattern_categories = ["delegation", "class_hierarchy"]
|
|
55
|
+
positive_terms = ["class", "composition", "delegate", "delegation", "extend", "inheritance", "mixin", "subclass"]
|
|
56
|
+
negative_terms = ["command", "facade", "state", "strategy"]
|
|
57
|
+
|
|
51
58
|
[routes."strategy-pattern"]
|
|
52
59
|
category = "architecture_patterns"
|
|
53
60
|
route_type = "primary"
|
|
54
61
|
priority = 80
|
|
55
62
|
applies_to_reasons = ["code_change"]
|
|
56
63
|
|
|
64
|
+
[routes."strategy-pattern".dependencies]
|
|
65
|
+
conflicts_with = ["state-machine-pattern"]
|
|
66
|
+
|
|
67
|
+
[routes."strategy-pattern".contexts]
|
|
68
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
69
|
+
layers = ["domain", "service", "policy", "pricing", "permission", "provider_selection"]
|
|
70
|
+
pattern_categories = ["variant_selection", "algorithm_policy", "branch_extraction"]
|
|
71
|
+
positive_terms = ["algorithm", "branch", "discount", "plan", "policy", "pricing", "provider", "region", "selection", "strategy", "switch", "variant"]
|
|
72
|
+
negative_terms = ["adapter", "audit", "history", "idempotency", "lifecycle", "phase", "protocol", "retry", "state", "status", "transaction", "transition"]
|
|
73
|
+
|
|
57
74
|
[routes."command-pattern"]
|
|
58
75
|
category = "architecture_patterns"
|
|
59
76
|
route_type = "primary"
|
|
60
77
|
priority = 80
|
|
61
78
|
applies_to_reasons = ["code_change", "behavior_change"]
|
|
62
79
|
|
|
80
|
+
[routes."command-pattern".contexts]
|
|
81
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
82
|
+
layers = ["application", "service", "workflow", "queue", "mutation"]
|
|
83
|
+
pattern_categories = ["durable_mutation", "undo_redo", "transaction_command"]
|
|
84
|
+
positive_terms = ["audit", "command", "durable", "execute", "idempotency", "mutation", "queue", "redo", "retry", "transaction", "undo"]
|
|
85
|
+
negative_terms = ["facade", "pricing", "provider", "state", "strategy", "transition", "variant"]
|
|
86
|
+
|
|
63
87
|
[routes."command-contract-authoring"]
|
|
64
88
|
category = "workflow_contracts"
|
|
65
89
|
route_type = "authoring"
|
|
@@ -90,12 +114,25 @@ route_type = "primary"
|
|
|
90
114
|
priority = 82
|
|
91
115
|
applies_to_reasons = ["public_api_change", "behavior_change", "docs_change", "test_change", "package_metadata_change", "release_risk"]
|
|
92
116
|
|
|
117
|
+
[routes."public-json-contract-change".dependencies]
|
|
118
|
+
suggests_adjuncts = ["cli-output-contract-review", "completion-evidence-gate"]
|
|
119
|
+
unlocks_on = [
|
|
120
|
+
{ signal = "machine_output_changed", skill = "cli-output-contract-review" },
|
|
121
|
+
{ signal = "schema_or_fixture_changed", skill = "completion-evidence-gate" },
|
|
122
|
+
]
|
|
123
|
+
|
|
93
124
|
[routes."completion-evidence-gate"]
|
|
94
125
|
category = "workflow_contracts"
|
|
95
126
|
route_type = "adjunct"
|
|
96
127
|
priority = 85
|
|
97
128
|
applies_to_reasons = ["unknown_change", "code_change", "behavior_change", "public_api_change", "test_change", "docs_change", "mustflow_docs_change", "mustflow_config_change", "package_metadata_change", "release_risk"]
|
|
98
129
|
|
|
130
|
+
[routes."completion-evidence-gate".dependencies]
|
|
131
|
+
suggests_adjuncts = ["next-action-menu"]
|
|
132
|
+
unlocks_on = [
|
|
133
|
+
{ signal = "concrete_followup_exists", skill = "next-action-menu" },
|
|
134
|
+
]
|
|
135
|
+
|
|
99
136
|
[routes."next-action-menu"]
|
|
100
137
|
category = "workflow_contracts"
|
|
101
138
|
route_type = "adjunct"
|
|
@@ -126,6 +163,13 @@ route_type = "primary"
|
|
|
126
163
|
priority = 80
|
|
127
164
|
applies_to_reasons = ["code_change"]
|
|
128
165
|
|
|
166
|
+
[routes."facade-pattern".contexts]
|
|
167
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
168
|
+
layers = ["application", "service", "orchestration", "integration"]
|
|
169
|
+
pattern_categories = ["subsystem_simplification", "orchestration_boundary"]
|
|
170
|
+
positive_terms = ["coordinate", "coordination", "facade", "orchestrate", "orchestration", "simplify", "subsystem", "wrapper"]
|
|
171
|
+
negative_terms = ["algorithm", "state", "strategy", "transition", "variant"]
|
|
172
|
+
|
|
129
173
|
[routes."pure-core-imperative-shell"]
|
|
130
174
|
category = "architecture_patterns"
|
|
131
175
|
route_type = "primary"
|
|
@@ -138,6 +182,16 @@ route_type = "primary"
|
|
|
138
182
|
priority = 80
|
|
139
183
|
applies_to_reasons = ["code_change", "behavior_change"]
|
|
140
184
|
|
|
185
|
+
[routes."state-machine-pattern".dependencies]
|
|
186
|
+
conflicts_with = ["strategy-pattern"]
|
|
187
|
+
|
|
188
|
+
[routes."state-machine-pattern".contexts]
|
|
189
|
+
file_types = ["ts", "tsx", "js", "jsx", "py", "go", "rs"]
|
|
190
|
+
layers = ["domain", "workflow", "lifecycle", "state"]
|
|
191
|
+
pattern_categories = ["state_transition", "lifecycle_model", "allowed_action"]
|
|
192
|
+
positive_terms = ["allowed", "history", "irreversible", "lifecycle", "phase", "state", "status", "transition", "workflow"]
|
|
193
|
+
negative_terms = ["algorithm", "pricing", "provider", "strategy", "variant", "wrapper"]
|
|
194
|
+
|
|
141
195
|
[routes."result-option"]
|
|
142
196
|
category = "architecture_patterns"
|
|
143
197
|
route_type = "primary"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
mustflow_doc: agents.root
|
|
3
3
|
locale: en
|
|
4
4
|
canonical: true
|
|
5
|
-
revision:
|
|
5
|
+
revision: 22
|
|
6
6
|
lifecycle: user-editable
|
|
7
7
|
authority: binding
|
|
8
8
|
---
|
|
@@ -123,6 +123,15 @@ mustflow-managed details are under `.mustflow/`.
|
|
|
123
123
|
- If the nested repository has no local preferences file, apply the nearest parent mustflow
|
|
124
124
|
preferences as inherited defaults while still following the nested repository's `AGENTS.md` and
|
|
125
125
|
command contract.
|
|
126
|
+
- When this mustflow root is used as a workspace for nested repositories, treat this root's
|
|
127
|
+
`.mustflow/skills/` router, routes, index, and installed `SKILL.md` files as a shared
|
|
128
|
+
workspace skill registry. Check the shared registry for task procedures in addition to the
|
|
129
|
+
nested repository's local routing files.
|
|
130
|
+
- Shared workspace skills provide procedure guidance only. They do not override the nested
|
|
131
|
+
repository's `AGENTS.md`, command contract, edit scope, safety rules, or source of truth.
|
|
132
|
+
- If a nested repository lacks a local skill index or uses a different local agent convention,
|
|
133
|
+
do not treat that absence as meaning no applicable workspace-level skill exists; consult the
|
|
134
|
+
shared workspace registry while keeping child-repository authority local.
|
|
126
135
|
- In repository farms, prefer each child repository's own command contract for repository-owned
|
|
127
136
|
commands. If the parent root intentionally orchestrates children, split parent-owned commands
|
|
128
137
|
into repo-named fragments under `.mustflow/config/commands/`; use
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
mustflow_doc: agents.root
|
|
3
3
|
locale: es
|
|
4
4
|
canonical: false
|
|
5
|
-
revision:
|
|
5
|
+
revision: 11
|
|
6
6
|
lifecycle: user-editable
|
|
7
7
|
authority: binding
|
|
8
8
|
---
|
|
@@ -59,6 +59,9 @@ Los detalles gestionados por mustflow se encuentran en `.mustflow/`.
|
|
|
59
59
|
- Las reglas de seguridad sobre secretos, privacidad, comandos destructivos y rutas permitidas de edición son acumulativas. Aplica la regla más estricta.
|
|
60
60
|
- Al entrar en un repositorio anidado, vuelve a leer su `AGENTS.md` y `.mustflow/config/*.toml` antes de editar.
|
|
61
61
|
- Si el repositorio anidado no tiene un archivo local de preferences, aplica las preferences del mustflow padre más cercano como valores heredados mientras sigues obedeciendo el `AGENTS.md` y el command contract del repositorio anidado.
|
|
62
|
+
- Cuando este mustflow root se use como workspace para repositorios anidados, trata el router, las routes, el index y los archivos `SKILL.md` instalados bajo `.mustflow/skills/` de este root como un shared workspace skill registry. Revisa ese registro compartido para elegir procedimientos de tarea junto con los archivos locales de routing del repositorio anidado.
|
|
63
|
+
- Las shared workspace skills solo orientan el procedimiento. No reemplazan el `AGENTS.md`, el command contract, el alcance de edición, las reglas de seguridad ni la fuente de verdad del repositorio anidado.
|
|
64
|
+
- Si un repositorio anidado no tiene un skill index local o usa una convención local de agentes diferente, no concluyas que no existe ninguna skill de workspace aplicable; consulta el shared workspace registry manteniendo local la autoridad del repositorio hijo.
|
|
62
65
|
- No edites fuera del repositorio hijo seleccionado salvo solicitud explícita.
|
|
63
66
|
|
|
64
67
|
## Compatibilidad con Instrucciones del Host
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
mustflow_doc: agents.root
|
|
3
3
|
locale: fr
|
|
4
4
|
canonical: false
|
|
5
|
-
revision:
|
|
5
|
+
revision: 11
|
|
6
6
|
lifecycle: user-editable
|
|
7
7
|
authority: binding
|
|
8
8
|
---
|
|
@@ -60,6 +60,9 @@ Les détails gérés par mustflow se trouvent dans `.mustflow/`.
|
|
|
60
60
|
- Les règles de sécurité sur les secrets, la vie privée, les commandes destructrices et les chemins d’édition autorisés sont cumulatives. Appliquer la règle la plus stricte.
|
|
61
61
|
- Lors de la navigation vers un dépôt imbriqué, relire le `AGENTS.md` de ce dépôt et `.mustflow/config/*.toml` avant d’éditer.
|
|
62
62
|
- Si le dépôt imbriqué n’a pas de fichier preferences local, appliquer les preferences du mustflow parent le plus proche comme valeurs héritées tout en continuant de suivre le `AGENTS.md` et le command contract du dépôt imbriqué.
|
|
63
|
+
- Lorsque ce mustflow root sert de workspace pour des dépôts imbriqués, traiter le router, les routes, l’index et les fichiers `SKILL.md` installés sous `.mustflow/skills/` dans ce root comme un shared workspace skill registry. Consulter ce registre partagé pour les procédures de tâche en plus des fichiers de routing locaux du dépôt imbriqué.
|
|
64
|
+
- Les shared workspace skills ne fournissent qu’un guidage de procédure. Elles ne remplacent pas le `AGENTS.md`, le command contract, le périmètre d’édition, les règles de sécurité ni la source de vérité du dépôt imbriqué.
|
|
65
|
+
- Si un dépôt imbriqué n’a pas d’index de skills local ou utilise une convention d’agent locale différente, ne pas conclure qu’aucune skill de workspace ne s’applique ; consulter le shared workspace registry tout en gardant l’autorité du dépôt enfant locale.
|
|
63
66
|
- Ne pas éditer en dehors du dépôt enfant sélectionné, sauf demande explicite.
|
|
64
67
|
|
|
65
68
|
## Compatibilité avec les instructions du host
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
mustflow_doc: agents.root
|
|
3
3
|
locale: hi
|
|
4
4
|
canonical: false
|
|
5
|
-
revision:
|
|
5
|
+
revision: 11
|
|
6
6
|
lifecycle: user-editable
|
|
7
7
|
authority: binding
|
|
8
8
|
---
|
|
@@ -59,6 +59,9 @@ mustflow द्वारा प्रबंधित विवरण `.mustflow/
|
|
|
59
59
|
- secrets, गोपनीयता, destructive commands, और अनुमत संपादन मार्गों के सुरक्षा नियम संचयी होते हैं। अधिक सख्त नियम अपनाएं।
|
|
60
60
|
- nested रिपॉजिटरी में जाने पर संपादन से पहले उसका `AGENTS.md` और `.mustflow/config/*.toml` फिर से पढ़ें।
|
|
61
61
|
- यदि nested repository में local preferences file नहीं है, तो nested repository के `AGENTS.md` और command contract का पालन करते हुए निकटतम parent mustflow preferences को inherited defaults के रूप में लागू करें।
|
|
62
|
+
- जब यह mustflow root nested repositories के लिए workspace के रूप में उपयोग हो, तो इस root के `.mustflow/skills/` router, routes, index, और installed `SKILL.md` files को shared workspace skill registry मानें। nested repository की local routing files के साथ इस shared registry को भी task procedures चुनने के लिए देखें।
|
|
63
|
+
- Shared workspace skills केवल procedure guidance देती हैं। वे nested repository के `AGENTS.md`, command contract, edit scope, safety rules, या source of truth को override नहीं करतीं।
|
|
64
|
+
- यदि किसी nested repository में local skill index नहीं है या वह अलग local agent convention उपयोग करती है, तो यह न मानें कि कोई workspace-level skill लागू नहीं है; child-repository authority को local रखते हुए shared workspace registry देखें।
|
|
62
65
|
- स्पष्ट अनुरोध के बिना चुनी हुई child रिपॉजिटरी के बाहर संपादन न करें।
|
|
63
66
|
|
|
64
67
|
## होस्ट-विशिष्ट निर्देश अनुकूलता
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
mustflow_doc: agents.root
|
|
3
3
|
locale: ko
|
|
4
4
|
canonical: false
|
|
5
|
-
revision:
|
|
5
|
+
revision: 29
|
|
6
6
|
lifecycle: user-editable
|
|
7
7
|
authority: binding
|
|
8
8
|
---
|
|
@@ -74,6 +74,9 @@ mustflow가 관리하는 세부 문서와 설정은 `.mustflow/` 폴더 아래
|
|
|
74
74
|
- 보안, 비밀 정보, 개인정보, 파괴적 명령, 수정 가능 경로 등 안전 규칙은 누적 적용하며 더 엄격한 규칙을 따릅니다.
|
|
75
75
|
- 상위 작업 공간에서 하위 저장소로 진입한 경우, 하위 저장소의 `AGENTS.md`와 `.mustflow/config/*.toml`을 다시 읽은 뒤 작업합니다.
|
|
76
76
|
- 하위 저장소에 로컬 preferences 파일이 없다면, 하위 저장소의 `AGENTS.md`와 command contract를 계속 따르면서 가장 가까운 상위 mustflow preferences를 상속 기본값으로 적용합니다.
|
|
77
|
+
- 이 mustflow 루트를 하위 저장소 작업 공간으로 사용하는 경우, 이 루트의 `.mustflow/skills/` router, routes, index, 설치된 `SKILL.md` 파일을 공유 workspace skill registry로 취급합니다. 하위 저장소의 로컬 라우팅 파일과 함께 이 공유 registry도 확인해 현재 작업에 맞는 절차를 선택합니다.
|
|
78
|
+
- 공유 workspace skill은 절차 안내일 뿐입니다. 하위 저장소의 `AGENTS.md`, command contract, 수정 범위, 안전 규칙, source of truth를 덮어쓰지 않습니다.
|
|
79
|
+
- 하위 저장소에 로컬 skill index가 없거나 다른 로컬 agent convention을 사용하더라도, 적용 가능한 workspace-level skill이 없다고 단정하지 않습니다. 하위 저장소의 권한은 로컬에 둔 채 공유 workspace registry를 함께 확인합니다.
|
|
77
80
|
- repository farm에서는 각 하위 저장소가 소유한 명령은 해당 저장소의 command contract를 우선합니다. 상위 루트가 하위 저장소를 의도적으로 orchestration해야 한다면 상위 소유 명령을 `.mustflow/config/commands/` 아래 repo 이름별 fragment로 나누고, `mf workspace command-fragments`로 읽기 전용 안내를 확인합니다.
|
|
78
81
|
- 명시적 요청 없이 선택한 하위 저장소 밖의 파일은 수정하지 않습니다.
|
|
79
82
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
mustflow_doc: agents.root
|
|
3
3
|
locale: zh
|
|
4
4
|
canonical: false
|
|
5
|
-
revision:
|
|
5
|
+
revision: 11
|
|
6
6
|
lifecycle: user-editable
|
|
7
7
|
authority: binding
|
|
8
8
|
---
|
|
@@ -60,6 +60,9 @@ authority: binding
|
|
|
60
60
|
- 针对 secrets、隐私、破坏性命令和允许编辑路径的安全规则为累加,执行更严格的规则。
|
|
61
61
|
- 进入嵌套仓库时,编辑前应重新阅读该仓库的 `AGENTS.md` 和 `.mustflow/config/*.toml`。
|
|
62
62
|
- 如果嵌套仓库没有本地 preferences 文件,在继续遵循该仓库 `AGENTS.md` 和 command contract 的同时,使用最近父级 mustflow preferences 作为继承默认值。
|
|
63
|
+
- 当此 mustflow root 被用作嵌套仓库的 workspace 时,应将此 root 下 `.mustflow/skills/` 的 router、routes、index 以及已安装的 `SKILL.md` 文件视为 shared workspace skill registry。除嵌套仓库的本地 routing 文件外,也要检查该共享 registry 来选择任务流程。
|
|
64
|
+
- Shared workspace skills 只提供流程指导,不会覆盖嵌套仓库的 `AGENTS.md`、command contract、编辑范围、安全规则或事实来源。
|
|
65
|
+
- 如果嵌套仓库没有本地 skill index,或使用不同的本地 agent convention,不要因此认定没有适用的 workspace-level skill;在保持子仓库权限本地化的前提下检查 shared workspace registry。
|
|
63
66
|
- 未经明确请求,不得编辑所选子仓库之外的内容。
|
|
64
67
|
|
|
65
68
|
## 宿主专用指令兼容性
|