mustflow 2.114.6 → 2.114.11

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.
@@ -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
- ['mustflow.skill.route_metadata_unknown_reference', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" references unknown mutually exclusive route "[^"]+"$/u],
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 selectAdjuncts(main, allCandidates, metadata) {
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 mainMetadata = metadata.get(main.skill);
341
- const excluded = new Set([main.skill, ...(mainMetadata?.mutuallyExclusiveWith ?? [])]);
342
- return allCandidates
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
- .slice(0, DEFAULT_MAX_ADJUNCTS);
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 allCandidates = routes
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.114.6",
3
+ "version": "2.114.11",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
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 read plans, source route
193
- shards, and optional read-only script-pack helper suggestions without granting command authority
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,
@@ -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"
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.114.6"
3
+ version = "2.114.11"
4
4
  description = "Minimal workflow for LLM agents to read, edit, and verify their work in a repository."
5
5
  common_root = "common"
6
6
  locales_root = "locales"