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.
- package/CHANGELOG.md +29 -0
- package/README.md +64 -35
- package/bin/postinstall.mjs +5 -3
- package/docs/ai-skill-routing-goal.md +15 -3
- package/docs/install.md +98 -68
- package/docs/positioning.md +8 -6
- package/docs/reference.md +22 -10
- package/docs/routing.md +15 -0
- package/docs/user-flow.md +61 -32
- package/docs/value-proof.md +6 -3
- package/package.json +3 -2
- package/src/advisor/guidance.mjs +23 -3
- package/src/agent-integration-cli.mjs +184 -0
- package/src/agent-integration-content.mjs +48 -0
- package/src/agent-integration-files.mjs +202 -0
- package/src/agent-integration-home.mjs +185 -0
- package/src/agent-skill-roots.mjs +1 -0
- package/src/brief-renderer.mjs +36 -57
- package/src/cli.mjs +168 -169
- package/src/control/can-use-guard.mjs +12 -0
- package/src/doctor.mjs +4 -1
- package/src/lifecycle-cli.mjs +23 -302
- package/src/lifecycle-content.mjs +4 -2
- package/src/route-advisory.mjs +214 -0
- package/src/route-renderer.mjs +146 -0
- package/src/route-selection.mjs +220 -0
- package/src/route-tokens.mjs +68 -0
- package/src/route.mjs +48 -413
- package/src/source-profiles.mjs +151 -150
- package/src/uninstall.mjs +56 -9
- package/src/workspace.mjs +79 -79
package/src/route.mjs
CHANGED
|
@@ -1,43 +1,16 @@
|
|
|
1
|
-
import { command } from "./advisor/action-core.mjs";
|
|
2
1
|
import { canUseSkill } from "./control/can-use-guard.mjs";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"be",
|
|
16
|
-
"been",
|
|
17
|
-
"before",
|
|
18
|
-
"being",
|
|
19
|
-
"by",
|
|
20
|
-
"for",
|
|
21
|
-
"from",
|
|
22
|
-
"in",
|
|
23
|
-
"is",
|
|
24
|
-
"it",
|
|
25
|
-
"of",
|
|
26
|
-
"on",
|
|
27
|
-
"or",
|
|
28
|
-
"that",
|
|
29
|
-
"the",
|
|
30
|
-
"these",
|
|
31
|
-
"this",
|
|
32
|
-
"those",
|
|
33
|
-
"to",
|
|
34
|
-
"was",
|
|
35
|
-
"were",
|
|
36
|
-
"with",
|
|
37
|
-
"without",
|
|
38
|
-
"you",
|
|
39
|
-
"your"
|
|
40
|
-
]);
|
|
2
|
+
import {
|
|
3
|
+
allowedFallbackSkills,
|
|
4
|
+
guardCommand,
|
|
5
|
+
overlapResolutionForRoute,
|
|
6
|
+
policyMemoryForRoute,
|
|
7
|
+
postUsePolicySuggestionForCapabilityRoute,
|
|
8
|
+
recommendationReason,
|
|
9
|
+
routeCandidate,
|
|
10
|
+
selectedRouteSkill,
|
|
11
|
+
usageDisclosure
|
|
12
|
+
} from "./route-advisory.mjs";
|
|
13
|
+
import { confidenceFor, possibleWorkflowSkills, selectRoute } from "./route-selection.mjs";
|
|
41
14
|
|
|
42
15
|
export function routeSkill(workspace, options) {
|
|
43
16
|
const intent = options.intent.trim();
|
|
@@ -49,15 +22,17 @@ export function routeSkill(workspace, options) {
|
|
|
49
22
|
throw new Error(`Unknown workflow: ${options.workflow}`);
|
|
50
23
|
}
|
|
51
24
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
25
|
+
const {
|
|
26
|
+
candidates,
|
|
27
|
+
skillCandidates,
|
|
28
|
+
explicitSkill,
|
|
29
|
+
best,
|
|
30
|
+
skillBest
|
|
31
|
+
} = selectRoute(workspace, workflow, intent);
|
|
32
|
+
if (explicitSkill !== undefined) {
|
|
33
|
+
return skillRoute({ intent, workflow, candidate: explicitSkill, candidates, skillCandidates, options, workspace });
|
|
34
|
+
}
|
|
57
35
|
if (best === undefined) {
|
|
58
|
-
const skillCandidates = skillRouteCandidates(workspace, workflow, intent, intentTokens);
|
|
59
|
-
const skillBest = skillCandidates.find((candidate) => candidate.score > 0 && candidate.allowed)
|
|
60
|
-
?? skillCandidates.find((candidate) => candidate.score > 0);
|
|
61
36
|
return skillBest === undefined
|
|
62
37
|
? noRoute({ intent, workflow, candidates, skillCandidates, workspace })
|
|
63
38
|
: skillRoute({ intent, workflow, candidate: skillBest, candidates, skillCandidates, options, workspace });
|
|
@@ -70,6 +45,19 @@ export function routeSkill(workspace, options) {
|
|
|
70
45
|
}));
|
|
71
46
|
const recommended = selectedRouteSkill(routedSkills);
|
|
72
47
|
const fallbackSkills = allowedFallbackSkills(routedSkills, recommended);
|
|
48
|
+
const overlapResolution = overlapResolutionForRoute({
|
|
49
|
+
matchedCapability: best.capability,
|
|
50
|
+
recommended,
|
|
51
|
+
routedSkills,
|
|
52
|
+
workflowName: workflow.name
|
|
53
|
+
});
|
|
54
|
+
const policyMemory = policyMemoryForRoute({
|
|
55
|
+
matchedCapability: best.capability,
|
|
56
|
+
routeMatch: best,
|
|
57
|
+
recommended,
|
|
58
|
+
routedSkills,
|
|
59
|
+
workflowName: workflow.name
|
|
60
|
+
});
|
|
73
61
|
const postUsePolicySuggestion = postUsePolicySuggestionForCapabilityRoute({
|
|
74
62
|
matchedCapability: best.capability,
|
|
75
63
|
routeMatch: best,
|
|
@@ -98,9 +86,11 @@ export function routeSkill(workspace, options) {
|
|
|
98
86
|
recommended_skill: recommended?.skill ?? null,
|
|
99
87
|
fallback_skills: fallbackSkills,
|
|
100
88
|
route_candidates: routedSkills.map((entry) => routeCandidate(entry, entry.skill === recommended?.skill)),
|
|
89
|
+
overlap_resolution: overlapResolution,
|
|
90
|
+
policy_memory: policyMemory,
|
|
101
91
|
guard_command: recommended === null ? null : guardCommand(recommended.skill, workflow.name, options),
|
|
102
92
|
guard: recommended?.guard ?? null,
|
|
103
|
-
usage_disclosure: recommended?.guard.allowed === true ? usageDisclosure(recommended.skill) : null,
|
|
93
|
+
usage_disclosure: recommended?.guard.allowed === true ? usageDisclosure(recommended.skill, policyMemory) : null,
|
|
104
94
|
post_use_policy_suggestion: postUsePolicySuggestion,
|
|
105
95
|
possible_skills: possibleWorkflowSkills(workspace, workflow),
|
|
106
96
|
candidates
|
|
@@ -121,6 +111,8 @@ function noRoute({ intent, workflow, candidates, skillCandidates, workspace }) {
|
|
|
121
111
|
recommended_skill: null,
|
|
122
112
|
fallback_skills: [],
|
|
123
113
|
route_candidates: [],
|
|
114
|
+
overlap_resolution: null,
|
|
115
|
+
policy_memory: null,
|
|
124
116
|
guard_command: null,
|
|
125
117
|
guard: null,
|
|
126
118
|
usage_disclosure: null,
|
|
@@ -142,6 +134,12 @@ function skillRoute({ intent, workflow, candidate, candidates, skillCandidates,
|
|
|
142
134
|
const selectedCandidate = routeCandidates.find((entry) => entry.skill === candidate.skill_id);
|
|
143
135
|
const guard = selectedCandidate?.guard ?? canUseSkill(workspace, candidate.skill_id, workflow.name);
|
|
144
136
|
const fallbackSkills = allowedFallbackSkills(routeCandidates, selectedCandidate);
|
|
137
|
+
const overlapResolution = overlapResolutionForRoute({
|
|
138
|
+
matchedCapability: candidate.skill_id,
|
|
139
|
+
recommended: selectedCandidate ?? null,
|
|
140
|
+
routedSkills: routeCandidates,
|
|
141
|
+
workflowName: workflow.name
|
|
142
|
+
});
|
|
145
143
|
return {
|
|
146
144
|
ok: true,
|
|
147
145
|
intent,
|
|
@@ -161,6 +159,8 @@ function skillRoute({ intent, workflow, candidate, candidates, skillCandidates,
|
|
|
161
159
|
recommended_skill: candidate.skill_id,
|
|
162
160
|
fallback_skills: fallbackSkills,
|
|
163
161
|
route_candidates: routeCandidates.map((entry) => routeCandidate(entry, entry.skill === candidate.skill_id)),
|
|
162
|
+
overlap_resolution: overlapResolution,
|
|
163
|
+
policy_memory: null,
|
|
164
164
|
guard_command: guardCommand(candidate.skill_id, workflow.name, options),
|
|
165
165
|
guard,
|
|
166
166
|
usage_disclosure: guard.allowed ? usageDisclosure(candidate.skill_id) : null,
|
|
@@ -170,368 +170,3 @@ function skillRoute({ intent, workflow, candidate, candidates, skillCandidates,
|
|
|
170
170
|
skill_candidates: skillCandidates
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
-
|
|
174
|
-
function routeCandidate(entry, selected) {
|
|
175
|
-
return {
|
|
176
|
-
skill: entry.skill,
|
|
177
|
-
role: entry.role,
|
|
178
|
-
selected,
|
|
179
|
-
guard_allowed: entry.guard.allowed,
|
|
180
|
-
guard_reasons: entry.guard.reasons,
|
|
181
|
-
guard_roles: entry.guard.roles,
|
|
182
|
-
capability_roles: entry.guard.capabilityRoles
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function usageDisclosure(skillId) {
|
|
187
|
-
return {
|
|
188
|
-
confirmation_required: false,
|
|
189
|
-
start: `State at the start that ${skillId} is being used for this request.`,
|
|
190
|
-
finish: `State at completion that ${skillId} was used.`,
|
|
191
|
-
start_message: `I will use ${skillId} for this request.`,
|
|
192
|
-
finish_message: `I used ${skillId} for this request.`,
|
|
193
|
-
guard: "Run the guard automatically immediately before invocation. Ask the user only if the guard denies use or a policy-changing action is needed."
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function selectedRouteSkill(routedSkills) {
|
|
198
|
-
return routedSkills.find((entry) => entry.guard.allowed) ?? routedSkills[0] ?? null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function allowedFallbackSkills(routedSkills, recommended) {
|
|
202
|
-
return routedSkills
|
|
203
|
-
.filter((entry) => entry.guard.allowed && entry.skill !== recommended?.skill)
|
|
204
|
-
.map((entry) => entry.skill);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function postUsePolicySuggestionForCapabilityRoute({ matchedCapability, routeMatch, recommended, routedSkills, workflowName, options }) {
|
|
208
|
-
return postUsePolicySuggestionForDeniedPreferredFallback({
|
|
209
|
-
matchedCapability,
|
|
210
|
-
recommended,
|
|
211
|
-
routedSkills,
|
|
212
|
-
workflowName,
|
|
213
|
-
options
|
|
214
|
-
}) ?? postUsePolicySuggestionForAllowedAmbiguity({
|
|
215
|
-
matchedCapability,
|
|
216
|
-
routeMatch,
|
|
217
|
-
recommended,
|
|
218
|
-
routedSkills,
|
|
219
|
-
workflowName,
|
|
220
|
-
options
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function postUsePolicySuggestionForDeniedPreferredFallback({ matchedCapability, recommended, routedSkills, workflowName, options }) {
|
|
225
|
-
if (recommended === null || recommended.role !== "fallback" || recommended.guard.allowed !== true) {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
const preferred = routedSkills.find((entry) => entry.role === "preferred");
|
|
229
|
-
if (preferred === undefined || preferred.guard.allowed === true) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
timing: "after_use",
|
|
234
|
-
mode: "ask_after_use",
|
|
235
|
-
reason: `SkillBoard selected fallback ${recommended.skill} because preferred skill ${preferred.skill} is denied. After completing the task, ask whether to remember ${recommended.skill} as the preferred skill for ${matchedCapability} in ${workflowName}.`,
|
|
236
|
-
question: `Should I remember ${recommended.skill} as the preferred skill for similar ${matchedCapability} requests in ${workflowName}?`,
|
|
237
|
-
requires_confirmation: true,
|
|
238
|
-
suggested_policy: {
|
|
239
|
-
kind: "prefer-skill",
|
|
240
|
-
skill: recommended.skill,
|
|
241
|
-
workflow: workflowName,
|
|
242
|
-
capability: matchedCapability,
|
|
243
|
-
command_hint: command([
|
|
244
|
-
"skillboard", "prefer", recommended.skill,
|
|
245
|
-
"--workflow", workflowName,
|
|
246
|
-
"--capability", matchedCapability,
|
|
247
|
-
"--config", routeConfigPath(options),
|
|
248
|
-
"--skills", routeSkillsRoot(options)
|
|
249
|
-
]).display
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function postUsePolicySuggestionForAllowedAmbiguity({ matchedCapability, routeMatch, recommended, routedSkills, workflowName, options }) {
|
|
255
|
-
if (recommended === null || recommended.guard.allowed !== true || routeMatch.required_by_workflow) {
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
const allowedSkills = routedSkills.filter((entry) => entry.guard.allowed);
|
|
259
|
-
if (allowedSkills.length < 2) {
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
return {
|
|
263
|
-
timing: "after_use",
|
|
264
|
-
mode: "ask_after_use",
|
|
265
|
-
reason: `SkillBoard found multiple allowed skills for ${matchedCapability} and selected ${recommended.skill}. After completing the task, ask whether to remember ${recommended.skill} as the preferred skill for ${matchedCapability} in ${workflowName} to reduce future ambiguity.`,
|
|
266
|
-
question: `Should I remember ${recommended.skill} as the preferred skill for similar ${matchedCapability} requests in ${workflowName}?`,
|
|
267
|
-
requires_confirmation: true,
|
|
268
|
-
suggested_policy: {
|
|
269
|
-
kind: "prefer-skill",
|
|
270
|
-
skill: recommended.skill,
|
|
271
|
-
workflow: workflowName,
|
|
272
|
-
capability: matchedCapability,
|
|
273
|
-
command_hint: command([
|
|
274
|
-
"skillboard", "prefer", recommended.skill,
|
|
275
|
-
"--workflow", workflowName,
|
|
276
|
-
"--capability", matchedCapability,
|
|
277
|
-
"--config", routeConfigPath(options),
|
|
278
|
-
"--skills", routeSkillsRoot(options)
|
|
279
|
-
]).display
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function capabilityRouteCandidates(workspace, workflow) {
|
|
285
|
-
const workflowSkillIds = new Set(workflowBoundSkillIds(workspace, workflow));
|
|
286
|
-
return workspace.capabilities.flatMap((capability) => {
|
|
287
|
-
const requirement = workflow.requiredCapabilities.find((candidate) => candidate.name === capability.name);
|
|
288
|
-
const requiredSkillIds = uniqueStrings([
|
|
289
|
-
requirement?.preferred ?? "",
|
|
290
|
-
...(requirement?.fallback ?? [])
|
|
291
|
-
]);
|
|
292
|
-
const configuredSkillIds = uniqueStrings([
|
|
293
|
-
...requiredSkillIds,
|
|
294
|
-
capability.canonical,
|
|
295
|
-
...capability.alternatives
|
|
296
|
-
]);
|
|
297
|
-
const boundSkillIds = configuredSkillIds.filter((skillId) => workflowSkillIds.has(skillId));
|
|
298
|
-
if (requirement === undefined && boundSkillIds.length === 0) {
|
|
299
|
-
return [];
|
|
300
|
-
}
|
|
301
|
-
const skillIds = requirement === undefined ? boundSkillIds : uniqueStrings([...requiredSkillIds, ...boundSkillIds]);
|
|
302
|
-
const skills = skillIds
|
|
303
|
-
.map((skillId) => workspace.skills.find((skill) => skill.id === skillId))
|
|
304
|
-
.filter((skill) => skill !== undefined);
|
|
305
|
-
return [{
|
|
306
|
-
capability: capability.name,
|
|
307
|
-
default_policy: capability.defaultPolicy,
|
|
308
|
-
skill_ids: skillIds,
|
|
309
|
-
skills,
|
|
310
|
-
required_by_workflow: requirement !== undefined
|
|
311
|
-
}];
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function workflowBoundSkillIds(workspace, workflow) {
|
|
316
|
-
return uniqueStrings([
|
|
317
|
-
...workflow.activeSkills,
|
|
318
|
-
...workflow.requiredCapabilities.flatMap((capability) => [capability.preferred, ...capability.fallback]),
|
|
319
|
-
...workspace.skills.filter((skill) => skill.invocation === "global-auto").map((skill) => skill.id)
|
|
320
|
-
]);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function scoreCandidate(workspace, candidate, intent, intentTokens) {
|
|
324
|
-
const capabilityTokens = tokensFor(candidate.capability);
|
|
325
|
-
let score = phraseMatchScore(intent, candidate.capability);
|
|
326
|
-
const matched_tokens = [];
|
|
327
|
-
const matched_fields = [];
|
|
328
|
-
if (score > 0) {
|
|
329
|
-
matched_fields.push("capability name");
|
|
330
|
-
}
|
|
331
|
-
for (const token of capabilityTokens) {
|
|
332
|
-
if (intentTokens.has(token)) {
|
|
333
|
-
score += 2;
|
|
334
|
-
matched_tokens.push(token);
|
|
335
|
-
matched_fields.push("capability name");
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const metadataMatch = scoreMetadataTerms(
|
|
339
|
-
candidate.skills.flatMap((skill) => skillMetadataTerms(workspace, skill.id, 1)),
|
|
340
|
-
intentTokens
|
|
341
|
-
);
|
|
342
|
-
score += metadataMatch.score;
|
|
343
|
-
matched_tokens.push(...metadataMatch.matchedTokens);
|
|
344
|
-
matched_fields.push(...metadataMatch.matchedFields);
|
|
345
|
-
return {
|
|
346
|
-
capability: candidate.capability,
|
|
347
|
-
confidence: confidenceFor(score),
|
|
348
|
-
score,
|
|
349
|
-
matched_tokens: uniqueStrings(matched_tokens).sort(),
|
|
350
|
-
matched_fields: uniqueStrings(matched_fields),
|
|
351
|
-
recommended_skill: candidate.skill_ids[0] ?? null,
|
|
352
|
-
fallback_skills: candidate.skill_ids.slice(1),
|
|
353
|
-
skill_ids: candidate.skill_ids,
|
|
354
|
-
required_by_workflow: candidate.required_by_workflow
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function skillRouteCandidates(workspace, workflow, intent, intentTokens) {
|
|
359
|
-
return possibleWorkflowSkills(workspace, workflow)
|
|
360
|
-
.map((skill) => scoreSkillCandidate(workspace, skill, intent, intentTokens))
|
|
361
|
-
.sort((left, right) => right.score - left.score || left.skill_id.localeCompare(right.skill_id));
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function scoreSkillCandidate(workspace, possibleSkill, intent, intentTokens) {
|
|
365
|
-
const skill = workspace.skills.find((candidate) => candidate.id === possibleSkill.id);
|
|
366
|
-
let score = phraseMatchScore(intent, possibleSkill.id);
|
|
367
|
-
const matched_fields = score > 0 ? ["skill id"] : [];
|
|
368
|
-
const metadataMatch = scoreMetadataTerms(skillMetadataTerms(workspace, possibleSkill.id, 2), intentTokens);
|
|
369
|
-
score += metadataMatch.score;
|
|
370
|
-
matched_fields.push(...metadataMatch.matchedFields);
|
|
371
|
-
const matched_tokens = metadataMatch.matchedTokens;
|
|
372
|
-
return {
|
|
373
|
-
skill_id: possibleSkill.id,
|
|
374
|
-
category: possibleSkill.category,
|
|
375
|
-
allowed: possibleSkill.allowed,
|
|
376
|
-
confidence: confidenceFor(score),
|
|
377
|
-
score,
|
|
378
|
-
matched_tokens: uniqueStrings(matched_tokens).sort(),
|
|
379
|
-
matched_fields: uniqueStrings(matched_fields)
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function scoreMetadataTerms(terms, intentTokens) {
|
|
384
|
-
let score = 0;
|
|
385
|
-
const matchedTokens = [];
|
|
386
|
-
const matchedFields = [];
|
|
387
|
-
const seenTokens = new Set();
|
|
388
|
-
for (const term of terms) {
|
|
389
|
-
for (const token of tokensFor(term.value)) {
|
|
390
|
-
if (!intentTokens.has(token)) {
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
matchedFields.push(term.label);
|
|
394
|
-
const matchedToken = canonicalRouteToken(token);
|
|
395
|
-
if (seenTokens.has(matchedToken)) {
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
seenTokens.add(matchedToken);
|
|
399
|
-
score += term.weight;
|
|
400
|
-
matchedTokens.push(matchedToken);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
score,
|
|
405
|
-
matchedTokens,
|
|
406
|
-
matchedFields: uniqueStrings(matchedFields)
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function skillMetadataTerms(workspace, skillId, weight) {
|
|
411
|
-
const skill = workspace.skills.find((candidate) => candidate.id === skillId);
|
|
412
|
-
const installed = installedSkillFor(workspace, skillId);
|
|
413
|
-
return [
|
|
414
|
-
{ label: "skill id", value: skill?.id ?? skillId, weight },
|
|
415
|
-
{ label: "skill path", value: skill?.path ?? installed?.path ?? "", weight },
|
|
416
|
-
{ label: "category", value: skill?.category ?? "", weight },
|
|
417
|
-
{ label: "SKILL.md name", value: installed?.name ?? "", weight },
|
|
418
|
-
{ label: "SKILL.md description", value: installed?.description ?? "", weight }
|
|
419
|
-
];
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function installedSkillFor(workspace, skillId) {
|
|
423
|
-
const skill = workspace.skills.find((candidate) => candidate.id === skillId);
|
|
424
|
-
return workspace.installedSkills.find((installed) => installed.id === skillId || installed.path === skill?.path);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function recommendationReason({ match, matchedFields, matchedTerms, skill, guard }) {
|
|
428
|
-
const fieldText = matchedFields.length === 0
|
|
429
|
-
? ""
|
|
430
|
-
: ` through ${matchedFields.join(", ")}`;
|
|
431
|
-
const termText = matchedTerms.length === 0
|
|
432
|
-
? ""
|
|
433
|
-
: ` (${matchedTerms.join(", ")})`;
|
|
434
|
-
if (skill === null || guard === null) {
|
|
435
|
-
return `${match}${fieldText}${termText}, but no workflow-bound skill is available.`;
|
|
436
|
-
}
|
|
437
|
-
if (guard.allowed) {
|
|
438
|
-
return `${match}${fieldText}${termText}; recommended ${skill} because the guard allows ${skill}.`;
|
|
439
|
-
}
|
|
440
|
-
const reason = guard.reasons[0] ?? "the guard did not provide a reason";
|
|
441
|
-
return `${match}${fieldText}${termText}; recommended ${skill}, but the guard currently denies it: ${reason}`;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function phraseMatchScore(intent, capability) {
|
|
445
|
-
const normalizedIntent = phraseKey(intent);
|
|
446
|
-
const normalizedCapability = phraseKey(capability);
|
|
447
|
-
if (normalizedIntent.length === 0 || normalizedCapability.length === 0) {
|
|
448
|
-
return 0;
|
|
449
|
-
}
|
|
450
|
-
return normalizedIntent.includes(normalizedCapability) || normalizedCapability.includes(normalizedIntent) ? 4 : 0;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function possibleWorkflowSkills(workspace, workflow) {
|
|
454
|
-
const ids = workflowBoundSkillIds(workspace, workflow);
|
|
455
|
-
return ids.map((skillId) => {
|
|
456
|
-
const skill = workspace.skills.find((candidate) => candidate.id === skillId);
|
|
457
|
-
const guard = canUseSkill(workspace, skillId, workflow.name);
|
|
458
|
-
return {
|
|
459
|
-
id: skillId,
|
|
460
|
-
status: skill?.status ?? null,
|
|
461
|
-
invocation: skill?.invocation ?? null,
|
|
462
|
-
category: skill?.category ?? null,
|
|
463
|
-
roles: guard.roles,
|
|
464
|
-
capability_roles: guard.capabilityRoles,
|
|
465
|
-
allowed: guard.allowed
|
|
466
|
-
};
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function guardCommand(skillId, workflowName, options) {
|
|
471
|
-
return command([
|
|
472
|
-
"skillboard", "guard", "use", skillId,
|
|
473
|
-
"--workflow", workflowName,
|
|
474
|
-
"--config", routeConfigPath(options),
|
|
475
|
-
"--skills", routeSkillsRoot(options)
|
|
476
|
-
]).display;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function routeConfigPath(options) {
|
|
480
|
-
return options.configPath ?? DEFAULT_CONFIG_PATH;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function routeSkillsRoot(options) {
|
|
484
|
-
return options.skillsRoot ?? DEFAULT_SKILLS_ROOT;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function confidenceFor(score) {
|
|
488
|
-
if (score >= HIGH_CONFIDENCE) {
|
|
489
|
-
return "high";
|
|
490
|
-
}
|
|
491
|
-
if (score >= MEDIUM_CONFIDENCE) {
|
|
492
|
-
return "medium";
|
|
493
|
-
}
|
|
494
|
-
if (score > 0) {
|
|
495
|
-
return "low";
|
|
496
|
-
}
|
|
497
|
-
return "none";
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function tokensFor(value) {
|
|
501
|
-
return new Set(String(value)
|
|
502
|
-
.toLowerCase()
|
|
503
|
-
.split(/[^a-z0-9]+/u)
|
|
504
|
-
.flatMap(tokenForms)
|
|
505
|
-
.filter(isRouteToken));
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function tokenForms(token) {
|
|
509
|
-
const singular = singularRouteToken(token);
|
|
510
|
-
return singular === token ? [token] : [token, singular];
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function canonicalRouteToken(token) {
|
|
514
|
-
return singularRouteToken(token);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function singularRouteToken(token) {
|
|
518
|
-
if (token.length > 4 && token.endsWith("ies")) {
|
|
519
|
-
return `${token.slice(0, -3)}y`;
|
|
520
|
-
}
|
|
521
|
-
if (token.length > 3 && token.endsWith("s") && !/(?:ss|us|is)$/u.test(token)) {
|
|
522
|
-
return token.slice(0, -1);
|
|
523
|
-
}
|
|
524
|
-
return token;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function isRouteToken(token) {
|
|
528
|
-
return token.length > 1 && !ROUTE_STOP_WORDS.has(token);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function phraseKey(value) {
|
|
532
|
-
return [...tokensFor(value)].join(" ");
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function uniqueStrings(values) {
|
|
536
|
-
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
|
|
537
|
-
}
|