@feelingmindful/thinking-graph 1.10.0 → 1.11.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 +4 -1
- package/dist/tools/learn.js +6 -2
- 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 +1 -1
- package/dist/tools/research.js +2 -2
- 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/dist/vault/bridge.d.ts +5 -0
- package/dist/vault/bridge.js +27 -2
- package/package.json +1 -1
- package/seeds/skill-registry.json +144 -54
|
@@ -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/dist/vault/bridge.d.ts
CHANGED
|
@@ -29,6 +29,11 @@ export interface VaultWriteOpts {
|
|
|
29
29
|
projectSlug: string;
|
|
30
30
|
metadata?: Record<string, unknown>;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Derive a short, meaningful title from freeform content.
|
|
34
|
+
* Takes the first sentence (or first N words) and sanitizes it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function deriveTitle(content: string, fallback: string): string;
|
|
32
37
|
export declare class VaultBridge {
|
|
33
38
|
private vaultRoot;
|
|
34
39
|
constructor(vaultPath: string);
|
package/dist/vault/bridge.js
CHANGED
|
@@ -33,12 +33,37 @@ function expandHome(p) {
|
|
|
33
33
|
return join(homedir(), p.slice(2));
|
|
34
34
|
return p;
|
|
35
35
|
}
|
|
36
|
+
// Obsidian titles should stay under 60 chars for clean sidebar display.
|
|
37
|
+
// macOS allows 255 bytes but long titles break Obsidian search and links.
|
|
38
|
+
const MAX_TITLE_LENGTH = 60;
|
|
36
39
|
function sanitizeFilename(name) {
|
|
37
40
|
return name
|
|
38
|
-
.replace(/[<>:"
|
|
41
|
+
.replace(/[<>:"/\\|?*#^\[\]]/g, '') // filesystem + Obsidian-reserved chars
|
|
39
42
|
.replace(/\s+/g, ' ')
|
|
40
43
|
.trim()
|
|
41
|
-
.slice(0,
|
|
44
|
+
.slice(0, MAX_TITLE_LENGTH);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Derive a short, meaningful title from freeform content.
|
|
48
|
+
* Takes the first sentence (or first N words) and sanitizes it.
|
|
49
|
+
*/
|
|
50
|
+
export function deriveTitle(content, fallback) {
|
|
51
|
+
// Strip leading markdown headings/bullets
|
|
52
|
+
const cleaned = content.replace(/^[#\-*>\s]+/, '').trim();
|
|
53
|
+
if (!cleaned)
|
|
54
|
+
return sanitizeFilename(fallback);
|
|
55
|
+
// Take first sentence (up to period, newline, or em-dash)
|
|
56
|
+
const firstSentence = cleaned.split(/[.\n—]/, 1)[0].trim();
|
|
57
|
+
// If the sentence is short enough, use it
|
|
58
|
+
if (firstSentence.length > 0 && firstSentence.length <= MAX_TITLE_LENGTH) {
|
|
59
|
+
const title = firstSentence.replace(/[^a-zA-Z0-9 \-_]/g, '').trim();
|
|
60
|
+
if (title.length >= 5)
|
|
61
|
+
return sanitizeFilename(title);
|
|
62
|
+
}
|
|
63
|
+
// Otherwise take first ~8 words
|
|
64
|
+
const words = cleaned.replace(/[^a-zA-Z0-9 \-_]/g, '').split(/\s+/).filter(Boolean);
|
|
65
|
+
const shortTitle = words.slice(0, 8).join(' ');
|
|
66
|
+
return sanitizeFilename(shortTitle || fallback);
|
|
42
67
|
}
|
|
43
68
|
/**
|
|
44
69
|
* Walk a directory tree and yield .md files.
|
package/package.json
CHANGED