agent-skillboard 0.2.14 → 0.2.16

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.
@@ -0,0 +1,146 @@
1
+ const FORMATTERS = {
2
+ brief: {
3
+ prefix: "- ",
4
+ candidatePrefix: " - ",
5
+ guardReasonPrefix: " - ",
6
+ text: safeText,
7
+ skill: code,
8
+ command: (value) => code(value, Number.POSITIVE_INFINITY),
9
+ list: formatCodeList,
10
+ guardReason: safeText,
11
+ noMatchNextStep: "ask a clarifying question before choosing a skill.",
12
+ includeFallbackWhenNoRecommendation: false
13
+ },
14
+ cli: {
15
+ prefix: "",
16
+ candidatePrefix: "- ",
17
+ guardReasonPrefix: " - ",
18
+ text: String,
19
+ skill: String,
20
+ command: String,
21
+ list: formatRawCodeList,
22
+ guardReason: String,
23
+ noMatchNextStep: null,
24
+ includeFallbackWhenNoRecommendation: true
25
+ }
26
+ };
27
+
28
+ export function renderRouteSectionLines(route, options = {}) {
29
+ const formatter = FORMATTERS[options.format ?? "cli"] ?? FORMATTERS.cli;
30
+ const lines = [];
31
+ pushRouteMatchLines(lines, route, formatter, options);
32
+ if (route.recommended_skill === null) {
33
+ pushNoRecommendationLines(lines, route, formatter);
34
+ return lines;
35
+ }
36
+ pushRecommendationLines(lines, route, formatter, options);
37
+ return lines;
38
+ }
39
+
40
+ function pushRouteMatchLines(lines, route, formatter, options) {
41
+ lines.push(`${formatter.prefix}Intent: ${formatter.text(route.intent)}`);
42
+ if (options.includeWorkflow === true) {
43
+ lines.push(`${formatter.prefix}Workflow: ${formatter.text(route.workflow)}`);
44
+ }
45
+ lines.push(`${formatter.prefix}Match source: ${route.match_source}`);
46
+ lines.push(`${formatter.prefix}Matched capability: ${route.matched_capability ?? "none"}`);
47
+ lines.push(`${formatter.prefix}Matched skill: ${route.matched_skill === null || route.matched_skill === undefined ? "none" : formatter.skill(route.matched_skill)}`);
48
+ lines.push(`${formatter.prefix}Confidence: ${route.confidence}`);
49
+ lines.push(`${formatter.prefix}Why: ${formatter.text(route.recommendation_reason, 320)}`);
50
+ lines.push(`${formatter.prefix}Matched terms: ${formatter.list(route.matched_terms)}`);
51
+ }
52
+
53
+ function pushNoRecommendationLines(lines, route, formatter) {
54
+ lines.push(`${formatter.prefix}Recommended skill: none`);
55
+ if (formatter.includeFallbackWhenNoRecommendation) {
56
+ lines.push(`${formatter.prefix}Fallback skills: ${formatter.list(route.fallback_skills)}`);
57
+ }
58
+ if (formatter.noMatchNextStep !== null) {
59
+ lines.push(`${formatter.prefix}Next step: ${formatter.noMatchNextStep}`);
60
+ }
61
+ }
62
+
63
+ function pushRecommendationLines(lines, route, formatter, options) {
64
+ lines.push(`${formatter.prefix}Recommended skill: ${formatter.skill(route.recommended_skill)}`);
65
+ lines.push(`${formatter.prefix}Fallback skills: ${formatter.list(route.fallback_skills)}`);
66
+ if (route.overlap_resolution?.status === "resolved") {
67
+ lines.push(`${formatter.prefix}Overlap: ${safeText(route.overlap_resolution.summary, 320)}`);
68
+ }
69
+ if (route.policy_memory?.status === "applied") {
70
+ lines.push(`${formatter.prefix}Policy preference: ${safeText(route.policy_memory.summary, 320)}`);
71
+ }
72
+ pushCandidateLines(lines, route.route_candidates ?? [], formatter);
73
+ if (typeof options.nextStep === "string" && options.nextStep.length > 0) {
74
+ lines.push(`${formatter.prefix}Next step: ${safeText(options.nextStep, 360)}`);
75
+ }
76
+ if (route.guard_command !== null) {
77
+ lines.push(`${formatter.prefix}Guard: ${formatter.command(route.guard_command)}`);
78
+ }
79
+ pushDisclosureLines(lines, route, formatter);
80
+ pushPostUsePolicySuggestion(lines, route.post_use_policy_suggestion, formatter);
81
+ }
82
+
83
+ function pushCandidateLines(lines, candidates, formatter) {
84
+ if (candidates.length === 0) {
85
+ return;
86
+ }
87
+ lines.push(`${formatter.prefix}Route candidates:`);
88
+ for (const candidate of candidates) {
89
+ lines.push(`${formatter.candidatePrefix}${formatter.skill(candidate.skill)} (${routeCandidateStatus(candidate)})`);
90
+ if (!candidate.guard_allowed && candidate.guard_reasons.length > 0) {
91
+ lines.push(`${formatter.guardReasonPrefix}${formatter.guardReason(candidate.guard_reasons[0])}`);
92
+ }
93
+ }
94
+ }
95
+
96
+ function pushDisclosureLines(lines, route, formatter) {
97
+ if (route.usage_disclosure === null || route.usage_disclosure === undefined) {
98
+ return;
99
+ }
100
+ const skillLabel = formatter.skill(route.recommended_skill);
101
+ lines.push(`${formatter.prefix}Disclosure: ${routeDisclosureText(skillLabel)}`);
102
+ lines.push(`${formatter.prefix}Say before use: "${formatter.text(route.usage_disclosure.start_message)}"`);
103
+ lines.push(`${formatter.prefix}Say after completion: "${formatter.text(route.usage_disclosure.finish_message)}"`);
104
+ }
105
+
106
+ function pushPostUsePolicySuggestion(lines, suggestion, formatter) {
107
+ if (suggestion === null || suggestion === undefined) {
108
+ return;
109
+ }
110
+ lines.push(`${formatter.prefix}After completion: ${formatter.text(afterUsePromptText(suggestion.question))}`);
111
+ lines.push(`${formatter.prefix}Policy command after confirmation: ${formatter.command(suggestion.suggested_policy.command_hint)}`);
112
+ }
113
+
114
+ function routeCandidateStatus(candidate) {
115
+ return [
116
+ candidate.role,
117
+ candidate.selected ? "selected" : null,
118
+ candidate.guard_allowed ? "allowed" : "denied"
119
+ ].filter((value) => value !== null).join(", ");
120
+ }
121
+
122
+ function routeDisclosureText(skillLabel) {
123
+ return `run the guard automatically, state at the start that ${skillLabel} is being used, and state at completion that it was used. No extra user approval is needed when the guard allows it.`;
124
+ }
125
+
126
+ function afterUsePromptText(question) {
127
+ return question.replace(/^Should I /u, "ask whether to ").replace(/\?$/u, ".");
128
+ }
129
+
130
+ function safeText(value, maxLength = 180) {
131
+ const compact = String(value).replace(/\s+/g, " ").trim();
132
+ const withoutTicks = compact.replace(/`/g, "'");
133
+ return withoutTicks.length > maxLength ? `${withoutTicks.slice(0, maxLength - 3)}...` : withoutTicks;
134
+ }
135
+
136
+ function code(value, maxLength = 180) {
137
+ return `\`${safeText(value, maxLength)}\``;
138
+ }
139
+
140
+ function formatCodeList(values) {
141
+ return values.length === 0 ? "none" : values.map((value) => code(value)).join(", ");
142
+ }
143
+
144
+ function formatRawCodeList(values) {
145
+ return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
146
+ }
@@ -0,0 +1,220 @@
1
+ import { canUseSkill } from "./control/can-use-guard.mjs";
2
+ import { canonicalRouteToken, phraseKey, tokensFor } from "./route-tokens.mjs";
3
+
4
+ const HIGH_CONFIDENCE = 4;
5
+ const MEDIUM_CONFIDENCE = 2;
6
+
7
+ export function selectRoute(workspace, workflow, intent) {
8
+ const intentTokens = tokensFor(intent);
9
+ const skillCandidates = skillRouteCandidates(workspace, workflow, intent, intentTokens);
10
+ const explicitSkill = explicitAllowedSkillRouteCandidate(intent, skillCandidates);
11
+ const candidates = capabilityRouteCandidates(workspace, workflow)
12
+ .map((candidate) => scoreCandidate(workspace, candidate, intent, intentTokens))
13
+ .sort((left, right) => right.score - left.score || left.capability.localeCompare(right.capability));
14
+ const best = candidates.find((candidate) => candidate.score > 0);
15
+ const skillBest = skillCandidates.find((candidate) => candidate.score > 0 && candidate.allowed)
16
+ ?? skillCandidates.find((candidate) => candidate.score > 0);
17
+
18
+ return {
19
+ candidates,
20
+ skillCandidates,
21
+ explicitSkill,
22
+ best,
23
+ skillBest
24
+ };
25
+ }
26
+
27
+ function explicitAllowedSkillRouteCandidate(intent, skillCandidates) {
28
+ const intentKey = phraseKey(intent);
29
+ return skillCandidates.find((candidate) => candidate.allowed && exactSkillIdMention(intentKey, candidate.skill_id));
30
+ }
31
+
32
+ function exactSkillIdMention(intentKey, skillId) {
33
+ const skillKey = phraseKey(skillId);
34
+ return skillKey.length > 0 && intentKey.includes(skillKey);
35
+ }
36
+
37
+ function capabilityRouteCandidates(workspace, workflow) {
38
+ const workflowSkillIds = new Set(workflowBoundSkillIds(workspace, workflow));
39
+ return workspace.capabilities.flatMap((capability) => {
40
+ const requirement = workflow.requiredCapabilities.find((candidate) => candidate.name === capability.name);
41
+ const requiredSkillIds = uniqueStrings([
42
+ requirement?.preferred ?? "",
43
+ ...(requirement?.fallback ?? [])
44
+ ]);
45
+ const configuredSkillIds = uniqueStrings([
46
+ ...requiredSkillIds,
47
+ capability.canonical,
48
+ ...capability.alternatives
49
+ ]);
50
+ const boundSkillIds = configuredSkillIds.filter((skillId) => workflowSkillIds.has(skillId));
51
+ if (requirement === undefined && boundSkillIds.length === 0) {
52
+ return [];
53
+ }
54
+ const skillIds = requirement === undefined ? boundSkillIds : uniqueStrings([...requiredSkillIds, ...boundSkillIds]);
55
+ const skills = skillIds
56
+ .map((skillId) => workspace.skills.find((skill) => skill.id === skillId))
57
+ .filter((skill) => skill !== undefined);
58
+ return [{
59
+ capability: capability.name,
60
+ default_policy: capability.defaultPolicy,
61
+ skill_ids: skillIds,
62
+ skills,
63
+ required_by_workflow: requirement !== undefined
64
+ }];
65
+ });
66
+ }
67
+
68
+ function workflowBoundSkillIds(workspace, workflow) {
69
+ return uniqueStrings([
70
+ ...workflow.activeSkills,
71
+ ...workflow.requiredCapabilities.flatMap((capability) => [capability.preferred, ...capability.fallback]),
72
+ ...workspace.skills.filter((skill) => skill.invocation === "global-auto").map((skill) => skill.id)
73
+ ]);
74
+ }
75
+
76
+ function scoreCandidate(workspace, candidate, intent, intentTokens) {
77
+ const capabilityTokens = tokensFor(candidate.capability);
78
+ let score = phraseMatchScore(intent, candidate.capability);
79
+ const matched_tokens = [];
80
+ const matched_fields = [];
81
+ if (score > 0) {
82
+ matched_fields.push("capability name");
83
+ }
84
+ for (const token of capabilityTokens) {
85
+ if (intentTokens.has(token)) {
86
+ score += 2;
87
+ matched_tokens.push(token);
88
+ matched_fields.push("capability name");
89
+ }
90
+ }
91
+ const metadataMatch = scoreMetadataTerms(
92
+ candidate.skills.flatMap((skill) => skillMetadataTerms(workspace, skill.id, 1)),
93
+ intentTokens
94
+ );
95
+ score += metadataMatch.score;
96
+ matched_tokens.push(...metadataMatch.matchedTokens);
97
+ matched_fields.push(...metadataMatch.matchedFields);
98
+ return {
99
+ capability: candidate.capability,
100
+ confidence: confidenceFor(score),
101
+ score,
102
+ matched_tokens: uniqueStrings(matched_tokens).sort(),
103
+ matched_fields: uniqueStrings(matched_fields),
104
+ recommended_skill: candidate.skill_ids[0] ?? null,
105
+ fallback_skills: candidate.skill_ids.slice(1),
106
+ skill_ids: candidate.skill_ids,
107
+ required_by_workflow: candidate.required_by_workflow
108
+ };
109
+ }
110
+
111
+ function skillRouteCandidates(workspace, workflow, intent, intentTokens) {
112
+ return possibleWorkflowSkills(workspace, workflow)
113
+ .map((skill) => scoreSkillCandidate(workspace, skill, intent, intentTokens))
114
+ .sort((left, right) => right.score - left.score || left.skill_id.localeCompare(right.skill_id));
115
+ }
116
+
117
+ function scoreSkillCandidate(workspace, possibleSkill, intent, intentTokens) {
118
+ let score = phraseMatchScore(intent, possibleSkill.id);
119
+ const matched_fields = score > 0 ? ["skill id"] : [];
120
+ const metadataMatch = scoreMetadataTerms(skillMetadataTerms(workspace, possibleSkill.id, 2), intentTokens);
121
+ score += metadataMatch.score;
122
+ matched_fields.push(...metadataMatch.matchedFields);
123
+ const matched_tokens = metadataMatch.matchedTokens;
124
+ return {
125
+ skill_id: possibleSkill.id,
126
+ category: possibleSkill.category,
127
+ allowed: possibleSkill.allowed,
128
+ confidence: confidenceFor(score),
129
+ score,
130
+ matched_tokens: uniqueStrings(matched_tokens).sort(),
131
+ matched_fields: uniqueStrings(matched_fields)
132
+ };
133
+ }
134
+
135
+ function scoreMetadataTerms(terms, intentTokens) {
136
+ let score = 0;
137
+ const matchedTokens = [];
138
+ const matchedFields = [];
139
+ const seenTokens = new Set();
140
+ for (const term of terms) {
141
+ for (const token of tokensFor(term.value)) {
142
+ if (!intentTokens.has(token)) {
143
+ continue;
144
+ }
145
+ matchedFields.push(term.label);
146
+ const matchedToken = canonicalRouteToken(token);
147
+ if (seenTokens.has(matchedToken)) {
148
+ continue;
149
+ }
150
+ seenTokens.add(matchedToken);
151
+ score += term.weight;
152
+ matchedTokens.push(matchedToken);
153
+ }
154
+ }
155
+ return {
156
+ score,
157
+ matchedTokens,
158
+ matchedFields: uniqueStrings(matchedFields)
159
+ };
160
+ }
161
+
162
+ function skillMetadataTerms(workspace, skillId, weight) {
163
+ const skill = workspace.skills.find((candidate) => candidate.id === skillId);
164
+ const installed = installedSkillFor(workspace, skillId);
165
+ return [
166
+ { label: "skill id", value: skill?.id ?? skillId, weight },
167
+ { label: "skill path", value: skill?.path ?? installed?.path ?? "", weight },
168
+ { label: "category", value: skill?.category ?? "", weight },
169
+ { label: "SKILL.md name", value: installed?.name ?? "", weight },
170
+ { label: "SKILL.md description", value: installed?.description ?? "", weight }
171
+ ];
172
+ }
173
+
174
+ function installedSkillFor(workspace, skillId) {
175
+ const skill = workspace.skills.find((candidate) => candidate.id === skillId);
176
+ return workspace.installedSkills.find((installed) => installed.id === skillId || installed.path === skill?.path);
177
+ }
178
+
179
+ function phraseMatchScore(intent, capability) {
180
+ const normalizedIntent = phraseKey(intent);
181
+ const normalizedCapability = phraseKey(capability);
182
+ if (normalizedIntent.length === 0 || normalizedCapability.length === 0) {
183
+ return 0;
184
+ }
185
+ return normalizedIntent.includes(normalizedCapability) || normalizedCapability.includes(normalizedIntent) ? 4 : 0;
186
+ }
187
+
188
+ export function possibleWorkflowSkills(workspace, workflow) {
189
+ const ids = workflowBoundSkillIds(workspace, workflow);
190
+ return ids.map((skillId) => {
191
+ const skill = workspace.skills.find((candidate) => candidate.id === skillId);
192
+ const guard = canUseSkill(workspace, skillId, workflow.name);
193
+ return {
194
+ id: skillId,
195
+ status: skill?.status ?? null,
196
+ invocation: skill?.invocation ?? null,
197
+ category: skill?.category ?? null,
198
+ roles: guard.roles,
199
+ capability_roles: guard.capabilityRoles,
200
+ allowed: guard.allowed
201
+ };
202
+ });
203
+ }
204
+
205
+ export function confidenceFor(score) {
206
+ if (score >= HIGH_CONFIDENCE) {
207
+ return "high";
208
+ }
209
+ if (score >= MEDIUM_CONFIDENCE) {
210
+ return "medium";
211
+ }
212
+ if (score > 0) {
213
+ return "low";
214
+ }
215
+ return "none";
216
+ }
217
+
218
+ function uniqueStrings(values) {
219
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
220
+ }
@@ -0,0 +1,68 @@
1
+ const ROUTE_STOP_WORDS = new Set([
2
+ "a",
3
+ "an",
4
+ "and",
5
+ "are",
6
+ "as",
7
+ "at",
8
+ "be",
9
+ "been",
10
+ "before",
11
+ "being",
12
+ "by",
13
+ "for",
14
+ "from",
15
+ "in",
16
+ "is",
17
+ "it",
18
+ "of",
19
+ "on",
20
+ "or",
21
+ "that",
22
+ "the",
23
+ "these",
24
+ "this",
25
+ "those",
26
+ "to",
27
+ "was",
28
+ "were",
29
+ "with",
30
+ "without",
31
+ "you",
32
+ "your"
33
+ ]);
34
+
35
+ export function tokensFor(value) {
36
+ return new Set(String(value)
37
+ .toLowerCase()
38
+ .split(/[^a-z0-9]+/u)
39
+ .flatMap(tokenForms)
40
+ .filter(isRouteToken));
41
+ }
42
+
43
+ function tokenForms(token) {
44
+ const singular = singularRouteToken(token);
45
+ return singular === token ? [token] : [token, singular];
46
+ }
47
+
48
+ export function canonicalRouteToken(token) {
49
+ return singularRouteToken(token);
50
+ }
51
+
52
+ function singularRouteToken(token) {
53
+ if (token.length > 4 && token.endsWith("ies")) {
54
+ return `${token.slice(0, -3)}y`;
55
+ }
56
+ if (token.length > 3 && token.endsWith("s") && !/(?:ss|us|is)$/u.test(token)) {
57
+ return token.slice(0, -1);
58
+ }
59
+ return token;
60
+ }
61
+
62
+ function isRouteToken(token) {
63
+ return token.length > 1 && !ROUTE_STOP_WORDS.has(token);
64
+ }
65
+
66
+ export function phraseKey(value) {
67
+ return [...tokensFor(value)].join(" ");
68
+ }