@feelingmindful/thinking-graph 1.10.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +8 -0
- package/dist/tools/execute-plan.d.ts +195 -0
- package/dist/tools/execute-plan.js +416 -0
- package/dist/tools/execute-skills.d.ts +104 -0
- package/dist/tools/execute-skills.js +274 -0
- package/dist/tools/learn.d.ts +3 -0
- package/dist/tools/learn.js +4 -1
- package/dist/tools/plan-skills.d.ts +40 -0
- package/dist/tools/plan-skills.js +40 -0
- package/dist/tools/recommend-skills.js +2 -2
- package/dist/tools/research.d.ts +9 -3
- package/dist/tools/research.js +45 -0
- package/dist/tools/route-skills.d.ts +34 -0
- package/dist/tools/route-skills.js +26 -0
- package/dist/tools/skill-routing.d.ts +63 -0
- package/dist/tools/skill-routing.js +343 -0
- package/dist/tools/think.js +3 -3
- package/package.json +1 -1
- package/seeds/skill-registry.json +144 -54
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { routeSkills } from './skill-routing.js';
|
|
3
|
+
export const routeSkillsSchema = z.object({
|
|
4
|
+
request: z.string().describe('Task or problem to route to the best installed skill workflow'),
|
|
5
|
+
platform: z.enum(['ios', 'android', 'web', 'all']).optional().describe('Preferred platform filter'),
|
|
6
|
+
goalVerb: z.string().optional().describe('Desired outcome verb such as audit, create, refactor, configure, research, or full'),
|
|
7
|
+
areas: z.array(z.string()).optional().describe('Explicit areas to weight such as accessibility, auth, observability, or copy'),
|
|
8
|
+
currentState: z.enum(['new', 'existing', 'unknown']).optional().describe('Whether the target is greenfield or an existing project'),
|
|
9
|
+
detectedNeeds: z.array(z.string()).optional().describe('Known states such as missing, needs-work, or good'),
|
|
10
|
+
maxCandidates: z.coerce.number().int().min(1).max(10).optional().describe('Maximum ranked candidates to return'),
|
|
11
|
+
});
|
|
12
|
+
export async function routeSkillsHandler(graph, input) {
|
|
13
|
+
const routed = await routeSkills(graph, input);
|
|
14
|
+
return {
|
|
15
|
+
content: [{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: JSON.stringify({
|
|
18
|
+
request: input.request,
|
|
19
|
+
desiredVerb: routed.desiredVerb,
|
|
20
|
+
inferredAreas: routed.inferredAreas,
|
|
21
|
+
heuristicNotes: routed.heuristicNotes,
|
|
22
|
+
candidates: routed.candidates,
|
|
23
|
+
}),
|
|
24
|
+
}],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ThinkingGraph } from '../engine/graph.js';
|
|
2
|
+
export interface SkillRoutingInput {
|
|
3
|
+
request: string;
|
|
4
|
+
platform?: 'ios' | 'android' | 'web' | 'all';
|
|
5
|
+
goalVerb?: string;
|
|
6
|
+
areas?: string[];
|
|
7
|
+
currentState?: 'new' | 'existing' | 'unknown';
|
|
8
|
+
detectedNeeds?: string[];
|
|
9
|
+
maxCandidates?: number;
|
|
10
|
+
selectedInvocation?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RoutedSkillCandidate {
|
|
13
|
+
id: string;
|
|
14
|
+
plugin: string;
|
|
15
|
+
skill: string;
|
|
16
|
+
invocation: string;
|
|
17
|
+
verb?: string;
|
|
18
|
+
platform?: string;
|
|
19
|
+
areas: string[];
|
|
20
|
+
detects: string[];
|
|
21
|
+
produces: string[];
|
|
22
|
+
invokes: string[];
|
|
23
|
+
score: number;
|
|
24
|
+
reasons: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface SkillPlanStep {
|
|
27
|
+
stepNumber: number;
|
|
28
|
+
invocation: string;
|
|
29
|
+
plugin: string;
|
|
30
|
+
skill: string;
|
|
31
|
+
verb?: string;
|
|
32
|
+
purpose: string;
|
|
33
|
+
approvalGateIds: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface ApprovalGate {
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
beforeStepNumber: number;
|
|
39
|
+
reason: string;
|
|
40
|
+
prompt: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function buildPlanId(input: {
|
|
43
|
+
request?: string;
|
|
44
|
+
plan: SkillPlanStep[];
|
|
45
|
+
approvalGates: ApprovalGate[];
|
|
46
|
+
}): string;
|
|
47
|
+
export declare function routeSkills(graph: ThinkingGraph, input: SkillRoutingInput): Promise<{
|
|
48
|
+
desiredVerb?: string;
|
|
49
|
+
inferredAreas: string[];
|
|
50
|
+
heuristicNotes: string[];
|
|
51
|
+
candidates: RoutedSkillCandidate[];
|
|
52
|
+
}>;
|
|
53
|
+
export declare function buildSkillPlan(graph: ThinkingGraph, input: SkillRoutingInput & {
|
|
54
|
+
requireApproval?: boolean;
|
|
55
|
+
}): Promise<{
|
|
56
|
+
desiredVerb?: string;
|
|
57
|
+
inferredAreas: string[];
|
|
58
|
+
heuristicNotes: string[];
|
|
59
|
+
route: RoutedSkillCandidate | null;
|
|
60
|
+
plan: SkillPlanStep[];
|
|
61
|
+
approvalGates: ApprovalGate[];
|
|
62
|
+
fallbacks: RoutedSkillCandidate[];
|
|
63
|
+
}>;
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export function buildPlanId(input) {
|
|
3
|
+
const payload = JSON.stringify({
|
|
4
|
+
request: input.request ?? null,
|
|
5
|
+
plan: input.plan.map(step => ({
|
|
6
|
+
stepNumber: step.stepNumber,
|
|
7
|
+
invocation: step.invocation,
|
|
8
|
+
plugin: step.plugin,
|
|
9
|
+
skill: step.skill,
|
|
10
|
+
verb: step.verb ?? null,
|
|
11
|
+
purpose: step.purpose,
|
|
12
|
+
approvalGateIds: [...step.approvalGateIds].sort(),
|
|
13
|
+
})),
|
|
14
|
+
approvalGates: input.approvalGates.map(gate => ({
|
|
15
|
+
id: gate.id,
|
|
16
|
+
title: gate.title,
|
|
17
|
+
beforeStepNumber: gate.beforeStepNumber,
|
|
18
|
+
reason: gate.reason,
|
|
19
|
+
prompt: gate.prompt,
|
|
20
|
+
})),
|
|
21
|
+
});
|
|
22
|
+
return `plan_${createHash('sha1').update(payload).digest('hex').slice(0, 12)}`;
|
|
23
|
+
}
|
|
24
|
+
const WORKFLOW_ORDER = ['research', 'init', 'configure', 'refactor', 'create', 'growth', 'humanize', 'audit'];
|
|
25
|
+
const AREA_ALIASES = {
|
|
26
|
+
accessibility: ['accessibility'],
|
|
27
|
+
analytics: ['observability'],
|
|
28
|
+
api: ['api', 'backend', 'fullstack'],
|
|
29
|
+
auth: ['auth', 'security'],
|
|
30
|
+
backend: ['backend', 'api', 'fullstack', 'data'],
|
|
31
|
+
content: ['content-sites', 'seo'],
|
|
32
|
+
copy: ['copy'],
|
|
33
|
+
dashboard: ['frontend', 'architecture', 'routing'],
|
|
34
|
+
data: ['data', 'backend', 'fullstack'],
|
|
35
|
+
design: ['design-tokens', 'icons', 'typography', 'animations'],
|
|
36
|
+
form: ['frontend'],
|
|
37
|
+
infra: ['infra', 'observability'],
|
|
38
|
+
marketing: ['copy', 'seo'],
|
|
39
|
+
monetization: ['monetization', 'subscriptions', 'iap', 'receipts'],
|
|
40
|
+
observability: ['observability', 'performance'],
|
|
41
|
+
payment: ['monetization', 'receipts'],
|
|
42
|
+
performance: ['performance', 'observability'],
|
|
43
|
+
queue: ['jobs', 'queues', 'background-processing'],
|
|
44
|
+
realtime: ['realtime', 'websockets', 'sse', 'data'],
|
|
45
|
+
routing: ['routing', 'react-router', 'app-router'],
|
|
46
|
+
seo: ['seo', 'content-sites'],
|
|
47
|
+
state: ['architecture', 'routing', 'tanstack-query', 'zustand'],
|
|
48
|
+
tanstack: ['tanstack-query'],
|
|
49
|
+
ui: ['frontend', 'design-tokens', 'icons', 'typography', 'animations'],
|
|
50
|
+
upload: ['media', 'uploads', 'cdn'],
|
|
51
|
+
webhooks: ['webhooks', 'integrations'],
|
|
52
|
+
zustand: ['zustand', 'architecture'],
|
|
53
|
+
};
|
|
54
|
+
function tokenize(input) {
|
|
55
|
+
return input
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.split(/[^a-z0-9-]+/)
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
function inferGoalVerb(request, currentState, explicitGoal) {
|
|
61
|
+
if (explicitGoal)
|
|
62
|
+
return explicitGoal;
|
|
63
|
+
const tokens = tokenize(request);
|
|
64
|
+
if (tokens.some(t => ['audit', 'review', 'check', 'inspect'].includes(t)))
|
|
65
|
+
return 'audit';
|
|
66
|
+
if (tokens.some(t => ['research', 'explore', 'compare', 'investigate'].includes(t)))
|
|
67
|
+
return 'research';
|
|
68
|
+
if (tokens.some(t => ['configure', 'setup', 'integrate', 'wire'].includes(t)))
|
|
69
|
+
return 'configure';
|
|
70
|
+
if (tokens.some(t => ['refactor', 'fix', 'improve', 'clean'].includes(t)))
|
|
71
|
+
return 'refactor';
|
|
72
|
+
if (tokens.some(t => ['create', 'build', 'implement', 'ship'].includes(t)))
|
|
73
|
+
return currentState === 'new' ? 'full' : 'create';
|
|
74
|
+
if (tokens.some(t => ['init', 'scaffold', 'bootstrap'].includes(t)))
|
|
75
|
+
return 'init';
|
|
76
|
+
if (tokens.some(t => ['full', 'end-to-end', 'orchestrate', 'launch'].includes(t)))
|
|
77
|
+
return 'full';
|
|
78
|
+
return currentState === 'new' ? 'full' : undefined;
|
|
79
|
+
}
|
|
80
|
+
function inferAreas(request, explicitAreas, skills) {
|
|
81
|
+
const inferred = new Set((explicitAreas ?? []).map(a => a.toLowerCase()));
|
|
82
|
+
const tokens = tokenize(request);
|
|
83
|
+
for (const token of tokens) {
|
|
84
|
+
for (const area of AREA_ALIASES[token] ?? [])
|
|
85
|
+
inferred.add(area);
|
|
86
|
+
}
|
|
87
|
+
const allKnownAreas = new Set(skills.flatMap(skill => skill.areas.map(area => area.toLowerCase())));
|
|
88
|
+
for (const token of tokens) {
|
|
89
|
+
if (allKnownAreas.has(token))
|
|
90
|
+
inferred.add(token);
|
|
91
|
+
}
|
|
92
|
+
return [...inferred];
|
|
93
|
+
}
|
|
94
|
+
function containsText(haystack, terms) {
|
|
95
|
+
const lower = haystack.toLowerCase();
|
|
96
|
+
return terms.some(term => lower.includes(term));
|
|
97
|
+
}
|
|
98
|
+
function scoreSkill(skill, request, inferredAreas, desiredVerb, currentState, detectedNeeds, relatedNodes, requestedPlatform) {
|
|
99
|
+
let score = 0;
|
|
100
|
+
const reasons = [];
|
|
101
|
+
const requestTerms = tokenize(request);
|
|
102
|
+
const requestText = request.toLowerCase();
|
|
103
|
+
if (requestedPlatform && skill.platform === requestedPlatform) {
|
|
104
|
+
score += 30;
|
|
105
|
+
reasons.push(`exact ${requestedPlatform} platform match`);
|
|
106
|
+
}
|
|
107
|
+
else if (requestedPlatform && skill.platform === 'all') {
|
|
108
|
+
score += 12;
|
|
109
|
+
reasons.push('cross-platform workflow remains valid');
|
|
110
|
+
}
|
|
111
|
+
if (desiredVerb && skill.verb === desiredVerb) {
|
|
112
|
+
score += 28;
|
|
113
|
+
reasons.push(`verb matches requested outcome (${desiredVerb})`);
|
|
114
|
+
}
|
|
115
|
+
else if (desiredVerb === 'full' && skill.verb === 'full') {
|
|
116
|
+
score += 22;
|
|
117
|
+
reasons.push('orchestrator workflow fits end-to-end request');
|
|
118
|
+
}
|
|
119
|
+
const matchedAreas = inferredAreas.filter(area => skill.areas.map(a => a.toLowerCase()).includes(area));
|
|
120
|
+
if (matchedAreas.length > 0) {
|
|
121
|
+
score += matchedAreas.length * 9;
|
|
122
|
+
reasons.push(`covers requested areas: ${matchedAreas.join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
if (containsText(`${skill.invocation} ${skill.pluginName} ${skill.skillName}`, requestTerms)) {
|
|
125
|
+
score += 10;
|
|
126
|
+
reasons.push('name/invocation aligns with request wording');
|
|
127
|
+
}
|
|
128
|
+
const matchedNeeds = detectedNeeds.filter(need => skill.detects.includes(need));
|
|
129
|
+
if (matchedNeeds.length > 0) {
|
|
130
|
+
score += matchedNeeds.length * 10;
|
|
131
|
+
reasons.push(`detects requested state: ${matchedNeeds.join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
if (currentState === 'new' && skill.verb && ['full', 'research', 'init', 'create'].includes(skill.verb)) {
|
|
134
|
+
score += 8;
|
|
135
|
+
reasons.push('good fit for greenfield work');
|
|
136
|
+
}
|
|
137
|
+
if (currentState === 'existing' && skill.verb && ['full', 'audit', 'configure', 'refactor'].includes(skill.verb)) {
|
|
138
|
+
score += 8;
|
|
139
|
+
reasons.push('good fit for existing codebases');
|
|
140
|
+
}
|
|
141
|
+
if (skill.verb === 'full' && skill.invokes.length > 0) {
|
|
142
|
+
score += 4;
|
|
143
|
+
reasons.push('has an orchestrated skill chain');
|
|
144
|
+
}
|
|
145
|
+
if (relatedNodes.length > 0) {
|
|
146
|
+
const nodeText = relatedNodes.map(node => node.content.toLowerCase()).join(' ');
|
|
147
|
+
const priorAreaHits = skill.areas.filter(area => nodeText.includes(area.toLowerCase()));
|
|
148
|
+
if (priorAreaHits.length > 0) {
|
|
149
|
+
score += Math.min(priorAreaHits.length * 4, 12);
|
|
150
|
+
reasons.push(`prior graph context overlaps: ${priorAreaHits.slice(0, 3).join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
if (relatedNodes.some(node => ['detection', 'tech_debt'].includes(node.type)) && skill.detects.some(d => ['missing', 'needs-work'].includes(d))) {
|
|
153
|
+
score += 6;
|
|
154
|
+
reasons.push('prior detections/debt make remediation workflow relevant');
|
|
155
|
+
}
|
|
156
|
+
if (relatedNodes.some(node => node.type === 'decision') && skill.produces.includes('decision')) {
|
|
157
|
+
score += 3;
|
|
158
|
+
reasons.push('workflow commonly produces actionable decisions');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (skill.areas.some(area => requestText.includes(area.toLowerCase()))) {
|
|
162
|
+
score += 6;
|
|
163
|
+
reasons.push('request directly mentions skill coverage');
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
id: skill.id,
|
|
167
|
+
plugin: skill.pluginName,
|
|
168
|
+
skill: skill.skillName,
|
|
169
|
+
invocation: skill.invocation,
|
|
170
|
+
verb: skill.verb,
|
|
171
|
+
platform: skill.platform,
|
|
172
|
+
areas: skill.areas,
|
|
173
|
+
detects: skill.detects,
|
|
174
|
+
produces: skill.produces,
|
|
175
|
+
invokes: skill.invokes,
|
|
176
|
+
score,
|
|
177
|
+
reasons,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export async function routeSkills(graph, input) {
|
|
181
|
+
const desiredVerb = inferGoalVerb(input.request, input.currentState, input.goalVerb);
|
|
182
|
+
const skills = await graph.storage.querySkills(input.platform ? { platform: input.platform } : {});
|
|
183
|
+
const inferredAreas = inferAreas(input.request, input.areas, skills);
|
|
184
|
+
const relatedNodes = await graph.searchNodes(input.request, 12);
|
|
185
|
+
const detectedNeeds = input.detectedNeeds ?? [];
|
|
186
|
+
const candidates = skills
|
|
187
|
+
.map(skill => scoreSkill(skill, input.request, inferredAreas, desiredVerb, input.currentState, detectedNeeds, relatedNodes, input.platform))
|
|
188
|
+
.filter(candidate => candidate.score > 0)
|
|
189
|
+
.sort((a, b) => {
|
|
190
|
+
if (b.score !== a.score)
|
|
191
|
+
return b.score - a.score;
|
|
192
|
+
const aOrder = WORKFLOW_ORDER.indexOf(a.verb ?? 'audit');
|
|
193
|
+
const bOrder = WORKFLOW_ORDER.indexOf(b.verb ?? 'audit');
|
|
194
|
+
return aOrder - bOrder;
|
|
195
|
+
})
|
|
196
|
+
.slice(0, input.maxCandidates ?? 5);
|
|
197
|
+
const heuristicNotes = [
|
|
198
|
+
desiredVerb ? `desired verb inferred as \`${desiredVerb}\`` : 'no explicit verb inferred; ranking favors area/platform fit',
|
|
199
|
+
inferredAreas.length > 0 ? `areas inferred: ${inferredAreas.join(', ')}` : 'no strong area hints inferred from request',
|
|
200
|
+
relatedNodes.length > 0 ? `used ${relatedNodes.length} graph nodes as lightweight context` : 'no close graph context found for this request',
|
|
201
|
+
];
|
|
202
|
+
return { desiredVerb, inferredAreas, heuristicNotes, candidates };
|
|
203
|
+
}
|
|
204
|
+
function buildPurpose(skill) {
|
|
205
|
+
if (skill.verb === 'research')
|
|
206
|
+
return 'gather context and sharpen the implementation direction';
|
|
207
|
+
if (skill.verb === 'init')
|
|
208
|
+
return 'establish missing foundation and initial scaffolding';
|
|
209
|
+
if (skill.verb === 'configure')
|
|
210
|
+
return 'wire infrastructure, auth, data, security, or observability';
|
|
211
|
+
if (skill.verb === 'refactor')
|
|
212
|
+
return 'reshape architecture or state ownership in-place';
|
|
213
|
+
if (skill.verb === 'create')
|
|
214
|
+
return 'build product/UI surface and deliver the requested experience';
|
|
215
|
+
if (skill.verb === 'audit')
|
|
216
|
+
return 'verify quality and surface issues before completion';
|
|
217
|
+
if (skill.verb === 'growth')
|
|
218
|
+
return 'improve growth loops and conversion strategy';
|
|
219
|
+
if (skill.verb === 'humanize')
|
|
220
|
+
return 'improve product copy and tone';
|
|
221
|
+
return 'execute the selected workflow';
|
|
222
|
+
}
|
|
223
|
+
function dedupeSkills(skills) {
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
return skills.filter(skill => {
|
|
226
|
+
if (seen.has(skill.invocation))
|
|
227
|
+
return false;
|
|
228
|
+
seen.add(skill.invocation);
|
|
229
|
+
return true;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function selectWorkflowVerbs(currentState, desiredVerb, selected) {
|
|
233
|
+
if (desiredVerb && desiredVerb !== 'full')
|
|
234
|
+
return [desiredVerb];
|
|
235
|
+
if (selected.verb && ['research', 'init', 'configure', 'refactor', 'create', 'audit'].includes(selected.verb) && desiredVerb !== 'full') {
|
|
236
|
+
return [selected.verb];
|
|
237
|
+
}
|
|
238
|
+
return currentState === 'new'
|
|
239
|
+
? ['research', 'init', 'configure', 'create', 'audit']
|
|
240
|
+
: ['research', 'configure', 'refactor', 'create', 'audit'];
|
|
241
|
+
}
|
|
242
|
+
function buildApprovalGates(steps, requireApproval) {
|
|
243
|
+
if (!requireApproval || steps.length === 0)
|
|
244
|
+
return [];
|
|
245
|
+
const gates = [{
|
|
246
|
+
id: 'approve-plan',
|
|
247
|
+
title: 'Approve selected route and plan',
|
|
248
|
+
beforeStepNumber: 1,
|
|
249
|
+
reason: 'Confirms the chosen skill family, execution order, and intended scope before any workflow runs.',
|
|
250
|
+
prompt: 'Approve this execution plan before step 1.',
|
|
251
|
+
}];
|
|
252
|
+
const firstInit = steps.find(step => step.verb === 'init');
|
|
253
|
+
if (firstInit) {
|
|
254
|
+
gates.push({
|
|
255
|
+
id: 'approve-foundation',
|
|
256
|
+
title: 'Approve foundation and scaffolding choices',
|
|
257
|
+
beforeStepNumber: firstInit.stepNumber,
|
|
258
|
+
reason: 'Init steps usually lock in framework, structure, and default service choices.',
|
|
259
|
+
prompt: `Approve foundation choices before step ${firstInit.stepNumber}.`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const firstArchitecture = steps.find(step => ['configure', 'refactor'].includes(step.verb ?? ''));
|
|
263
|
+
if (firstArchitecture) {
|
|
264
|
+
gates.push({
|
|
265
|
+
id: 'approve-architecture',
|
|
266
|
+
title: 'Approve architecture and integration changes',
|
|
267
|
+
beforeStepNumber: firstArchitecture.stepNumber,
|
|
268
|
+
reason: 'Configure/refactor work can change auth ownership, data flow, state management, and infra boundaries.',
|
|
269
|
+
prompt: `Approve architecture/integration changes before step ${firstArchitecture.stepNumber}.`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const firstMessaging = steps.find(step => ['growth', 'humanize'].includes(step.verb ?? ''));
|
|
273
|
+
if (firstMessaging) {
|
|
274
|
+
gates.push({
|
|
275
|
+
id: 'approve-messaging',
|
|
276
|
+
title: 'Approve messaging or growth changes',
|
|
277
|
+
beforeStepNumber: firstMessaging.stepNumber,
|
|
278
|
+
reason: 'Growth and copy steps change user-facing messaging and should be explicitly approved.',
|
|
279
|
+
prompt: `Approve messaging/growth changes before step ${firstMessaging.stepNumber}.`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return gates;
|
|
283
|
+
}
|
|
284
|
+
export async function buildSkillPlan(graph, input) {
|
|
285
|
+
const routed = await routeSkills(graph, input);
|
|
286
|
+
const allSkills = await graph.storage.querySkills(input.platform ? { platform: input.platform } : {});
|
|
287
|
+
const byInvocation = new Map(allSkills.map(skill => [skill.invocation, skill]));
|
|
288
|
+
const selectedCandidate = input.selectedInvocation
|
|
289
|
+
? routed.candidates.find(candidate => candidate.invocation === input.selectedInvocation) ?? null
|
|
290
|
+
: routed.candidates[0] ?? null;
|
|
291
|
+
if (!selectedCandidate) {
|
|
292
|
+
return {
|
|
293
|
+
desiredVerb: routed.desiredVerb,
|
|
294
|
+
inferredAreas: routed.inferredAreas,
|
|
295
|
+
heuristicNotes: routed.heuristicNotes,
|
|
296
|
+
route: null,
|
|
297
|
+
plan: [],
|
|
298
|
+
approvalGates: [],
|
|
299
|
+
fallbacks: [],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const selectedSkill = byInvocation.get(selectedCandidate.invocation);
|
|
303
|
+
const family = allSkills.filter(skill => skill.pluginName === selectedSkill.pluginName && skill.skillName === selectedSkill.skillName);
|
|
304
|
+
let orderedSkills = [];
|
|
305
|
+
if (selectedSkill.verb === 'full' && selectedSkill.invokes.length > 0) {
|
|
306
|
+
orderedSkills = selectedSkill.invokes.map(invocation => byInvocation.get(invocation)).filter(Boolean);
|
|
307
|
+
}
|
|
308
|
+
else if (family.length > 1) {
|
|
309
|
+
const wantedVerbs = selectWorkflowVerbs(input.currentState, routed.desiredVerb, selectedSkill);
|
|
310
|
+
const familyMap = new Map(family.map(skill => [skill.verb ?? '', skill]));
|
|
311
|
+
orderedSkills = wantedVerbs.map(verb => familyMap.get(verb)).filter(Boolean);
|
|
312
|
+
if (orderedSkills.length === 0)
|
|
313
|
+
orderedSkills = [selectedSkill];
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
orderedSkills = [selectedSkill];
|
|
317
|
+
}
|
|
318
|
+
const dedupedSkills = dedupeSkills(orderedSkills);
|
|
319
|
+
const steps = dedupedSkills.map((skill, index) => ({
|
|
320
|
+
stepNumber: index + 1,
|
|
321
|
+
invocation: skill.invocation,
|
|
322
|
+
plugin: skill.pluginName,
|
|
323
|
+
skill: skill.skillName,
|
|
324
|
+
verb: skill.verb,
|
|
325
|
+
purpose: buildPurpose(skill),
|
|
326
|
+
approvalGateIds: [],
|
|
327
|
+
}));
|
|
328
|
+
const approvalGates = buildApprovalGates(steps, input.requireApproval ?? true);
|
|
329
|
+
for (const gate of approvalGates) {
|
|
330
|
+
const step = steps.find(candidate => candidate.stepNumber === gate.beforeStepNumber);
|
|
331
|
+
if (step)
|
|
332
|
+
step.approvalGateIds.push(gate.id);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
desiredVerb: routed.desiredVerb,
|
|
336
|
+
inferredAreas: routed.inferredAreas,
|
|
337
|
+
heuristicNotes: routed.heuristicNotes,
|
|
338
|
+
route: selectedCandidate,
|
|
339
|
+
plan: steps,
|
|
340
|
+
approvalGates,
|
|
341
|
+
fallbacks: routed.candidates.filter(candidate => candidate.invocation !== selectedCandidate.invocation).slice(0, 2),
|
|
342
|
+
};
|
|
343
|
+
}
|
package/dist/tools/think.js
CHANGED
|
@@ -82,7 +82,7 @@ function buildSuggestions(type, thoughtNumber, relatedCount, stats, matchedSkill
|
|
|
82
82
|
suggestions.push({
|
|
83
83
|
tool: 'skill',
|
|
84
84
|
when: 'Load the reasoning skill for structured thinking: recall → think → relate → research → decide → learn',
|
|
85
|
-
example: { skill: '
|
|
85
|
+
example: { skill: 'reasoning' },
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
// Append matched skills from the registry
|
|
@@ -155,8 +155,8 @@ export async function thinkHandler(graph, input) {
|
|
|
155
155
|
if (matches.length > 0) {
|
|
156
156
|
matchedSkills = matches.slice(0, 2).map(s => ({
|
|
157
157
|
tool: 'skill',
|
|
158
|
-
when:
|
|
159
|
-
example: { skill: `${s.pluginName}:${s.skillName}
|
|
158
|
+
when: `Run \`${s.invocation}\`${s.areas.length > 0 ? ` (covers: ${s.areas.join(', ')})` : ''}`,
|
|
159
|
+
example: { skill: `${s.pluginName}:${s.skillName}`, invocation: s.invocation },
|
|
160
160
|
}));
|
|
161
161
|
}
|
|
162
162
|
}
|
package/package.json
CHANGED