@soleri/core 9.14.4 → 9.15.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/brain/brain.d.ts +9 -0
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +11 -1
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +24 -0
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/chat/chat-session.d.ts +6 -0
- package/dist/chat/chat-session.d.ts.map +1 -1
- package/dist/chat/chat-session.js +68 -17
- package/dist/chat/chat-session.js.map +1 -1
- package/dist/curator/curator.d.ts +6 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +138 -0
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/types.d.ts +10 -0
- package/dist/curator/types.d.ts.map +1 -1
- package/dist/engine/bin/soleri-engine.js +0 -0
- package/dist/flows/types.d.ts +16 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/intake/content-classifier.d.ts +10 -4
- package/dist/intake/content-classifier.d.ts.map +1 -1
- package/dist/intake/content-classifier.js +19 -5
- package/dist/intake/content-classifier.js.map +1 -1
- package/dist/intake/text-ingester.d.ts +18 -0
- package/dist/intake/text-ingester.d.ts.map +1 -1
- package/dist/intake/text-ingester.js +37 -13
- package/dist/intake/text-ingester.js.map +1 -1
- package/dist/planning/planner.d.ts +3 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +43 -4
- package/dist/planning/planner.js.map +1 -1
- package/dist/plugins/types.d.ts +2 -2
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +59 -20
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +28 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +16 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +19 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/validate-skills.d.ts +32 -0
- package/dist/skills/validate-skills.d.ts.map +1 -0
- package/dist/skills/validate-skills.js +396 -0
- package/dist/skills/validate-skills.js.map +1 -0
- package/dist/vault/default-canonical-tags.d.ts +15 -0
- package/dist/vault/default-canonical-tags.d.ts.map +1 -0
- package/dist/vault/default-canonical-tags.js +65 -0
- package/dist/vault/default-canonical-tags.js.map +1 -0
- package/dist/vault/tag-normalizer.d.ts +42 -0
- package/dist/vault/tag-normalizer.d.ts.map +1 -0
- package/dist/vault/tag-normalizer.js +157 -0
- package/dist/vault/tag-normalizer.js.map +1 -0
- package/package.json +5 -1
- package/src/__tests__/embeddings.test.ts +3 -3
- package/src/brain/brain.ts +25 -1
- package/src/brain/intelligence.ts +25 -0
- package/src/brain/types.ts +1 -0
- package/src/chat/chat-session.ts +75 -17
- package/src/chat/chat-transport.test.ts +31 -1
- package/src/curator/curator.ts +180 -0
- package/src/curator/types.ts +10 -0
- package/src/index.ts +7 -0
- package/src/intake/content-classifier.ts +22 -4
- package/src/intake/text-ingester.ts +61 -12
- package/src/planning/planner.test.ts +86 -90
- package/src/planning/planner.ts +48 -4
- package/src/runtime/admin-setup-ops.test.ts +44 -0
- package/src/runtime/admin-setup-ops.ts +59 -20
- package/src/runtime/facades/orchestrate-facade.ts +27 -1
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/types.ts +19 -0
- package/src/skills/validate-skills.test.ts +205 -0
- package/src/skills/validate-skills.ts +470 -0
- package/src/vault/default-canonical-tags.ts +64 -0
- package/src/vault/tag-normalizer.test.ts +214 -0
- package/src/vault/tag-normalizer.ts +188 -0
- package/dist/embeddings/index.d.ts +0 -5
- package/dist/embeddings/index.d.ts.map +0 -1
- package/dist/embeddings/index.js +0 -3
- package/dist/embeddings/index.js.map +0 -1
package/src/planning/planner.ts
CHANGED
|
@@ -77,8 +77,38 @@ export class Planner {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
private
|
|
80
|
+
private refresh(): void {
|
|
81
|
+
this.store = this.load();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private mergeLatestStore(deletedPlanIds: string[] = []): void {
|
|
85
|
+
const deleted = new Set(deletedPlanIds);
|
|
86
|
+
const latest = this.load();
|
|
87
|
+
const merged = new Map<string, Plan>();
|
|
88
|
+
|
|
89
|
+
for (const plan of latest.plans) {
|
|
90
|
+
if (!deleted.has(plan.id)) {
|
|
91
|
+
merged.set(plan.id, plan);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const plan of this.store.plans) {
|
|
96
|
+
if (deleted.has(plan.id)) continue;
|
|
97
|
+
const existing = merged.get(plan.id);
|
|
98
|
+
if (!existing || plan.updatedAt >= existing.updatedAt) {
|
|
99
|
+
merged.set(plan.id, plan);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.store = {
|
|
104
|
+
version: latest.version ?? this.store.version ?? '1.0',
|
|
105
|
+
plans: [...merged.values()],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private save(deletedPlanIds: string[] = []): void {
|
|
81
110
|
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
111
|
+
this.mergeLatestStore(deletedPlanIds);
|
|
82
112
|
writeFileSync(this.filePath, JSON.stringify(this.store, null, 2), 'utf-8');
|
|
83
113
|
}
|
|
84
114
|
|
|
@@ -88,8 +118,13 @@ export class Planner {
|
|
|
88
118
|
plan.updatedAt = r.updatedAt;
|
|
89
119
|
}
|
|
90
120
|
|
|
121
|
+
private findPlan(planId: string): Plan | null {
|
|
122
|
+
return this.store.plans.find((p) => p.id === planId) ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
private requirePlan(planId: string): Plan {
|
|
92
|
-
|
|
126
|
+
this.refresh();
|
|
127
|
+
const plan = this.findPlan(planId);
|
|
93
128
|
if (!plan) throw new Error(`Plan not found: ${planId}`);
|
|
94
129
|
return plan;
|
|
95
130
|
}
|
|
@@ -101,6 +136,7 @@ export class Planner {
|
|
|
101
136
|
}
|
|
102
137
|
|
|
103
138
|
create(params: Parameters<typeof createPlanObject>[0]): Plan {
|
|
139
|
+
this.refresh();
|
|
104
140
|
const plan = createPlanObject(params);
|
|
105
141
|
this.store.plans.push(plan);
|
|
106
142
|
this.save();
|
|
@@ -108,18 +144,21 @@ export class Planner {
|
|
|
108
144
|
}
|
|
109
145
|
|
|
110
146
|
get(planId: string): Plan | null {
|
|
111
|
-
|
|
147
|
+
this.refresh();
|
|
148
|
+
return this.findPlan(planId);
|
|
112
149
|
}
|
|
113
150
|
|
|
114
151
|
list(): Plan[] {
|
|
152
|
+
this.refresh();
|
|
115
153
|
return [...this.store.plans];
|
|
116
154
|
}
|
|
117
155
|
|
|
118
156
|
remove(planId: string): boolean {
|
|
157
|
+
this.refresh();
|
|
119
158
|
const idx = this.store.plans.findIndex((p) => p.id === planId);
|
|
120
159
|
if (idx < 0) return false;
|
|
121
160
|
this.store.plans.splice(idx, 1);
|
|
122
|
-
this.save();
|
|
161
|
+
this.save([planId]);
|
|
123
162
|
return true;
|
|
124
163
|
}
|
|
125
164
|
|
|
@@ -220,10 +259,12 @@ export class Planner {
|
|
|
220
259
|
}
|
|
221
260
|
|
|
222
261
|
getExecuting(): Plan[] {
|
|
262
|
+
this.refresh();
|
|
223
263
|
return this.store.plans.filter((p) => p.status === 'executing' || p.status === 'validating');
|
|
224
264
|
}
|
|
225
265
|
|
|
226
266
|
getActive(): Plan[] {
|
|
267
|
+
this.refresh();
|
|
227
268
|
return this.store.plans.filter(
|
|
228
269
|
(p) =>
|
|
229
270
|
p.status === 'brainstorming' ||
|
|
@@ -435,6 +476,7 @@ export class Planner {
|
|
|
435
476
|
}
|
|
436
477
|
|
|
437
478
|
archive(olderThanDays?: number): Plan[] {
|
|
479
|
+
this.refresh();
|
|
438
480
|
const cutoff =
|
|
439
481
|
olderThanDays !== undefined
|
|
440
482
|
? Date.now() - olderThanDays * 24 * 60 * 60 * 1000
|
|
@@ -460,6 +502,7 @@ export class Planner {
|
|
|
460
502
|
closedIds: string[];
|
|
461
503
|
closedPlans: Array<{ id: string; previousStatus: string; reason: string }>;
|
|
462
504
|
} {
|
|
505
|
+
this.refresh();
|
|
463
506
|
const now = Date.now();
|
|
464
507
|
const forceAll = olderThanMs === 0;
|
|
465
508
|
const defaultTtl = forceAll ? 0 : 30 * 60 * 1000; // 30 minutes for draft/approved
|
|
@@ -522,6 +565,7 @@ export class Planner {
|
|
|
522
565
|
totalTasks: number;
|
|
523
566
|
tasksByStatus: Record<TaskStatus, number>;
|
|
524
567
|
} {
|
|
568
|
+
this.refresh();
|
|
525
569
|
const plans = this.store.plans;
|
|
526
570
|
const byStatus = {
|
|
527
571
|
brainstorming: 0,
|
|
@@ -46,6 +46,11 @@ vi.mock('./claude-md-helpers.js', () => ({
|
|
|
46
46
|
injectEngineRulesBlock: vi.fn((content: string) => content),
|
|
47
47
|
}));
|
|
48
48
|
|
|
49
|
+
vi.mock('../paths.js', () => ({
|
|
50
|
+
agentPlansPath: vi.fn(() => '/mock-home/.soleri/test-agent/plans.json'),
|
|
51
|
+
agentVaultPath: vi.fn(() => '/mock-home/.soleri/test-agent/vault.db'),
|
|
52
|
+
}));
|
|
53
|
+
|
|
49
54
|
vi.mock('../skills/sync-skills.js', () => ({
|
|
50
55
|
discoverSkills: vi.fn(() => [{ name: 'skill-1', path: '/mock/skills/skill-1' }]),
|
|
51
56
|
syncSkillsToClaudeCode: vi.fn(() => ({
|
|
@@ -330,5 +335,44 @@ describe('createAdminSetupOps', () => {
|
|
|
330
335
|
expect(activePlans[0].status).toBe('executing');
|
|
331
336
|
expect(result.recommendation).toContain('need attention');
|
|
332
337
|
});
|
|
338
|
+
|
|
339
|
+
it('uses configured or resolved .soleri plan paths and understands planner stores', async () => {
|
|
340
|
+
runtime = {
|
|
341
|
+
...createMockRuntime(),
|
|
342
|
+
config: {
|
|
343
|
+
agentId: 'test-agent',
|
|
344
|
+
dataDir: '/mock/agent-data',
|
|
345
|
+
agentDir: '/mock/agent-dir',
|
|
346
|
+
},
|
|
347
|
+
} as unknown as AgentRuntime;
|
|
348
|
+
ops = createAdminSetupOps(runtime);
|
|
349
|
+
|
|
350
|
+
mockDirs.add('/mock-home/.soleri/test-agent');
|
|
351
|
+
mockFs['/mock-home/.soleri/test-agent/vault.db'] = 'binary';
|
|
352
|
+
mockFs['/mock-home/.soleri/test-agent/plans.json'] = JSON.stringify({
|
|
353
|
+
version: '1.0',
|
|
354
|
+
plans: [
|
|
355
|
+
{ id: 'plan-1', status: 'executing' },
|
|
356
|
+
{ id: 'plan-2', status: 'completed' },
|
|
357
|
+
],
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const result = (await findOp(ops, 'admin_check_persistence').handler({})) as Record<
|
|
361
|
+
string,
|
|
362
|
+
unknown
|
|
363
|
+
>;
|
|
364
|
+
|
|
365
|
+
expect((result.storageDirectory as Record<string, unknown>).path).toBe(
|
|
366
|
+
'/mock-home/.soleri/test-agent',
|
|
367
|
+
);
|
|
368
|
+
expect(
|
|
369
|
+
((result.files as Record<string, unknown>).plans as Record<string, unknown>).path,
|
|
370
|
+
).toBe('/mock-home/.soleri/test-agent/plans.json');
|
|
371
|
+
expect(
|
|
372
|
+
((result.files as Record<string, unknown>).plans as Record<string, unknown>).items,
|
|
373
|
+
).toBe(2);
|
|
374
|
+
expect(result.status).toBe('PERSISTENCE_ACTIVE');
|
|
375
|
+
expect(result.activePlans).toEqual([{ id: 'plan-1', status: 'executing' }]);
|
|
376
|
+
});
|
|
333
377
|
});
|
|
334
378
|
});
|
|
@@ -27,6 +27,10 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
27
27
|
import { homedir } from 'node:os';
|
|
28
28
|
import type { OpDefinition } from '../facades/types.js';
|
|
29
29
|
import type { AgentRuntime } from './types.js';
|
|
30
|
+
import {
|
|
31
|
+
agentPlansPath as getAgentPlansPath,
|
|
32
|
+
agentVaultPath as getAgentVaultPath,
|
|
33
|
+
} from '../paths.js';
|
|
30
34
|
import {
|
|
31
35
|
hasSections,
|
|
32
36
|
removeSections,
|
|
@@ -74,19 +78,63 @@ function getFileInfo(path: string): { exists: boolean; size: number; items: numb
|
|
|
74
78
|
try {
|
|
75
79
|
const stat = statSync(path);
|
|
76
80
|
const content = JSON.parse(readFileSync(path, 'utf-8'));
|
|
77
|
-
const items = content
|
|
78
|
-
? Object.keys(content.items).length
|
|
79
|
-
: content.contexts
|
|
80
|
-
? content.contexts.length
|
|
81
|
-
: Array.isArray(content)
|
|
82
|
-
? content.length
|
|
83
|
-
: 0;
|
|
81
|
+
const items = countPersistedItems(content);
|
|
84
82
|
return { exists: true, size: stat.size, items };
|
|
85
83
|
} catch {
|
|
86
84
|
return { exists: true, size: 0, items: -1 };
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
|
|
88
|
+
function countPersistedItems(content: unknown): number {
|
|
89
|
+
if (Array.isArray(content)) return content.length;
|
|
90
|
+
if (!content || typeof content !== 'object') return 0;
|
|
91
|
+
|
|
92
|
+
const data = content as Record<string, unknown>;
|
|
93
|
+
if (Array.isArray(data.plans)) return data.plans.length;
|
|
94
|
+
if (data.items && typeof data.items === 'object') return Object.keys(data.items).length;
|
|
95
|
+
if (Array.isArray(data.contexts)) return data.contexts.length;
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function extractActivePlans(content: unknown): Array<{ id: string; status: string }> {
|
|
100
|
+
if (!content || typeof content !== 'object') return [];
|
|
101
|
+
|
|
102
|
+
const plans = Array.isArray((content as Record<string, unknown>).plans)
|
|
103
|
+
? ((content as Record<string, unknown>).plans as unknown[])
|
|
104
|
+
: null;
|
|
105
|
+
if (plans) {
|
|
106
|
+
return plans.flatMap((plan) => {
|
|
107
|
+
if (!plan || typeof plan !== 'object') return [];
|
|
108
|
+
const p = plan as Record<string, unknown>;
|
|
109
|
+
const id = typeof p.id === 'string' ? p.id : null;
|
|
110
|
+
const lifecycle =
|
|
111
|
+
typeof p.lifecycleStatus === 'string'
|
|
112
|
+
? p.lifecycleStatus
|
|
113
|
+
: typeof p.status === 'string'
|
|
114
|
+
? p.status
|
|
115
|
+
: null;
|
|
116
|
+
if (!id || (lifecycle !== 'executing' && lifecycle !== 'reconciling')) return [];
|
|
117
|
+
return [{ id, status: lifecycle }];
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const items = (content as Record<string, unknown>).items;
|
|
122
|
+
if (!items || typeof items !== 'object') return [];
|
|
123
|
+
|
|
124
|
+
return Object.entries(items).flatMap(([id, plan]) => {
|
|
125
|
+
if (!plan || typeof plan !== 'object') return [];
|
|
126
|
+
const p = plan as Record<string, unknown>;
|
|
127
|
+
const lifecycle =
|
|
128
|
+
typeof p.lifecycleStatus === 'string'
|
|
129
|
+
? p.lifecycleStatus
|
|
130
|
+
: typeof p.status === 'string'
|
|
131
|
+
? p.status
|
|
132
|
+
: null;
|
|
133
|
+
if (lifecycle !== 'executing' && lifecycle !== 'reconciling') return [];
|
|
134
|
+
return [{ id, status: lifecycle }];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
90
138
|
/** Discover hookify rule files in a directory */
|
|
91
139
|
function discoverHookifyFiles(dir: string): Array<{ name: string; path: string }> {
|
|
92
140
|
if (!existsSync(dir)) return [];
|
|
@@ -621,15 +669,15 @@ export function createAdminSetupOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
621
669
|
auth: 'read',
|
|
622
670
|
handler: async () => {
|
|
623
671
|
const { agentId, plansPath, vaultPath } = config;
|
|
624
|
-
const
|
|
672
|
+
const plansFile = plansPath ?? getAgentPlansPath(agentId);
|
|
673
|
+
const vaultFile = vaultPath ?? getAgentVaultPath(agentId);
|
|
674
|
+
const storageDir = dirname(plansFile);
|
|
625
675
|
const storageDirExists = existsSync(storageDir);
|
|
626
676
|
|
|
627
677
|
// Check plan storage
|
|
628
|
-
const plansFile = plansPath ?? join(storageDir, 'plans.json');
|
|
629
678
|
const plansInfo = getFileInfo(plansFile);
|
|
630
679
|
|
|
631
680
|
// Check vault
|
|
632
|
-
const vaultFile = vaultPath ?? join(storageDir, 'vault.db');
|
|
633
681
|
const vaultExists = existsSync(vaultFile);
|
|
634
682
|
let vaultSize = 0;
|
|
635
683
|
if (vaultExists) {
|
|
@@ -655,16 +703,7 @@ export function createAdminSetupOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
655
703
|
if (plansInfo.exists) {
|
|
656
704
|
try {
|
|
657
705
|
const plansData = JSON.parse(readFileSync(plansFile, 'utf-8'));
|
|
658
|
-
|
|
659
|
-
if (typeof items === 'object' && items !== null) {
|
|
660
|
-
for (const [id, plan] of Object.entries(items)) {
|
|
661
|
-
const p = plan as Record<string, unknown>;
|
|
662
|
-
const lifecycle = (p.lifecycleStatus ?? p.status) as string | undefined;
|
|
663
|
-
if (lifecycle === 'executing' || lifecycle === 'reconciling') {
|
|
664
|
-
activePlans.push({ id, status: lifecycle });
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
706
|
+
activePlans.push(...extractActivePlans(plansData));
|
|
668
707
|
} catch {
|
|
669
708
|
// Parse error — not critical
|
|
670
709
|
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import type { SkillStep, EvidenceType } from '../../skills/step-tracker.js';
|
|
25
25
|
|
|
26
26
|
export function createOrchestrateFacadeOps(runtime: AgentRuntime): OpDefinition[] {
|
|
27
|
-
const { vault, governance, projectRegistry } = runtime;
|
|
27
|
+
const { vault, governance, projectRegistry, brainIntelligence } = runtime;
|
|
28
28
|
|
|
29
29
|
return [
|
|
30
30
|
// ─── Session Start (inline from core-ops.ts) ─────────────────────
|
|
@@ -150,6 +150,31 @@ export function createOrchestrateFacadeOps(runtime: AgentRuntime): OpDefinition[
|
|
|
150
150
|
vaultStats: stats,
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
// Auto-close orphaned brain sessions (endedAt IS NULL, startedAt < now - 2h)
|
|
154
|
+
let orphansClosed = 0;
|
|
155
|
+
try {
|
|
156
|
+
const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
|
|
157
|
+
const cutoff = new Date(Date.now() - TWO_HOURS_MS);
|
|
158
|
+
const activeSessions = brainIntelligence.listSessions({ active: true, limit: 1000 });
|
|
159
|
+
for (const s of activeSessions) {
|
|
160
|
+
if (new Date(s.startedAt) < cutoff) {
|
|
161
|
+
try {
|
|
162
|
+
brainIntelligence.lifecycle({
|
|
163
|
+
action: 'end',
|
|
164
|
+
sessionId: s.id,
|
|
165
|
+
planOutcome: 'abandoned',
|
|
166
|
+
context: 'auto-closed: orphan from previous conversation',
|
|
167
|
+
});
|
|
168
|
+
orphansClosed++;
|
|
169
|
+
} catch {
|
|
170
|
+
// Best-effort per session — never let one failure abort the rest
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Non-critical — don't fail session start over orphan cleanup
|
|
176
|
+
}
|
|
177
|
+
|
|
153
178
|
return {
|
|
154
179
|
project,
|
|
155
180
|
is_new: isNew,
|
|
@@ -167,6 +192,7 @@ export function createOrchestrateFacadeOps(runtime: AgentRuntime): OpDefinition[
|
|
|
167
192
|
expiredThisSession: expired,
|
|
168
193
|
},
|
|
169
194
|
preflight,
|
|
195
|
+
orphansClosed,
|
|
170
196
|
...(stagingWarning ? { stagingWarning } : {}),
|
|
171
197
|
...(dreamInfo ? { dream: dreamInfo } : {}),
|
|
172
198
|
};
|
package/src/runtime/runtime.ts
CHANGED
|
@@ -148,6 +148,15 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
|
|
|
148
148
|
// Pass embeddingProvider for hybrid FTS5+vector search when available
|
|
149
149
|
const brain = new Brain(vault, vaultManager, embeddingProvider);
|
|
150
150
|
|
|
151
|
+
// Wire canonical tag config if provided
|
|
152
|
+
if (config.canonicalTags && config.canonicalTags.length > 0) {
|
|
153
|
+
brain.setCanonicalTagConfig({
|
|
154
|
+
canonicalTags: config.canonicalTags,
|
|
155
|
+
tagConstraintMode: config.tagConstraintMode ?? 'suggest',
|
|
156
|
+
metadataTagPrefixes: config.metadataTagPrefixes ?? ['source:'],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
// Brain Intelligence — pattern strengths, session knowledge, intelligence pipeline
|
|
152
161
|
const brainIntelligence = new BrainIntelligence(vault, brain);
|
|
153
162
|
|
|
@@ -199,6 +208,15 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
|
|
|
199
208
|
const intakePipeline = new IntakePipeline(vault.getProvider(), vault, llmClient);
|
|
200
209
|
const textIngester = new TextIngester(vault, llmClient);
|
|
201
210
|
|
|
211
|
+
// Wire canonical tag config into TextIngester if provided
|
|
212
|
+
if (config.canonicalTags && config.canonicalTags.length > 0) {
|
|
213
|
+
textIngester.setCanonicalTagConfig({
|
|
214
|
+
canonicalTags: config.canonicalTags,
|
|
215
|
+
tagConstraintMode: config.tagConstraintMode ?? 'suggest',
|
|
216
|
+
metadataTagPrefixes: config.metadataTagPrefixes ?? ['source:'],
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
// Playbook Executor — in-memory step-by-step workflow sessions
|
|
203
221
|
const playbookExecutor = new PlaybookExecutor();
|
|
204
222
|
|
package/src/runtime/types.ts
CHANGED
|
@@ -79,6 +79,25 @@ export interface AgentRuntimeConfig {
|
|
|
79
79
|
persona?: Partial<import('../persona/types.js').PersonaConfig>;
|
|
80
80
|
/** Embedding provider configuration. If omitted, embeddings are disabled. */
|
|
81
81
|
embedding?: EmbeddingConfig;
|
|
82
|
+
/**
|
|
83
|
+
* Canonical tag taxonomy configuration.
|
|
84
|
+
* When set, tags are normalized against this list during capture and ingestion.
|
|
85
|
+
*/
|
|
86
|
+
canonicalTags?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* Tag constraint mode.
|
|
89
|
+
* - 'enforce': tags not matching canonical list are dropped (unless within edit-distance 3).
|
|
90
|
+
* - 'suggest': tags are mapped to nearest canonical if within edit-distance 2 (default).
|
|
91
|
+
* - 'off': no normalization — behavior unchanged from pre-taxonomy.
|
|
92
|
+
* Default: 'suggest'
|
|
93
|
+
*/
|
|
94
|
+
tagConstraintMode?: 'enforce' | 'suggest' | 'off';
|
|
95
|
+
/**
|
|
96
|
+
* Metadata tag prefixes — tags with these prefixes (e.g. 'source:') are treated as metadata
|
|
97
|
+
* and are exempt from canonical normalization.
|
|
98
|
+
* Default: ['source:']
|
|
99
|
+
*/
|
|
100
|
+
metadataTagPrefixes?: string[];
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
/**
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for validate-skills — the user-installed SKILL.md validator.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { validateSkillDocs } from './validate-skills.js';
|
|
10
|
+
|
|
11
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function createSkillsDir(): string {
|
|
14
|
+
return mkdtempSync(join(tmpdir(), 'soleri-validate-skills-test-'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function addSkill(skillsDir: string, skillName: string, content: string): void {
|
|
18
|
+
const skillDir = join(skillsDir, skillName);
|
|
19
|
+
mkdirSync(skillDir, { recursive: true });
|
|
20
|
+
writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Tests ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
describe('validateSkillDocs', () => {
|
|
26
|
+
let skillsDir: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
skillsDir = createSkillsDir();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
rmSync(skillsDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns valid=true and no errors when the skills directory is empty', () => {
|
|
37
|
+
const result = validateSkillDocs(skillsDir);
|
|
38
|
+
expect(result.valid).toBe(true);
|
|
39
|
+
expect(result.errors).toHaveLength(0);
|
|
40
|
+
expect(result.totalFiles).toBe(0);
|
|
41
|
+
expect(result.totalExamples).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns valid=true for a SKILL.md with no op-call examples', () => {
|
|
45
|
+
addSkill(
|
|
46
|
+
skillsDir,
|
|
47
|
+
'my-skill',
|
|
48
|
+
`# My Skill
|
|
49
|
+
|
|
50
|
+
This skill does something useful.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Just invoke it.
|
|
55
|
+
`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const result = validateSkillDocs(skillsDir);
|
|
59
|
+
expect(result.valid).toBe(true);
|
|
60
|
+
expect(result.errors).toHaveLength(0);
|
|
61
|
+
expect(result.totalFiles).toBe(1);
|
|
62
|
+
expect(result.totalExamples).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns valid=true when op-call params match the schema', () => {
|
|
66
|
+
addSkill(
|
|
67
|
+
skillsDir,
|
|
68
|
+
'capture-skill',
|
|
69
|
+
`# Capture Skill
|
|
70
|
+
|
|
71
|
+
Captures knowledge to the vault.
|
|
72
|
+
|
|
73
|
+
\`\`\`
|
|
74
|
+
YOUR_AGENT_core op:capture_knowledge params: { projectPath: ".", entries: [{ type: "pattern", domain: "testing", title: "Use vitest", description: "Prefer vitest for unit tests", severity: "info" }] }
|
|
75
|
+
\`\`\`
|
|
76
|
+
`,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const result = validateSkillDocs(skillsDir);
|
|
80
|
+
expect(result.valid).toBe(true);
|
|
81
|
+
expect(result.errors).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('reports an error when severity has an invalid enum value', () => {
|
|
85
|
+
// "suggestion" is not in the capture_knowledge severity enum (valid: critical, warning, info)
|
|
86
|
+
addSkill(
|
|
87
|
+
skillsDir,
|
|
88
|
+
'bad-severity-skill',
|
|
89
|
+
`# Bad Skill
|
|
90
|
+
|
|
91
|
+
Example with wrong severity enum:
|
|
92
|
+
|
|
93
|
+
\`\`\`
|
|
94
|
+
YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
|
|
95
|
+
\`\`\`
|
|
96
|
+
`,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const result = validateSkillDocs(skillsDir);
|
|
100
|
+
expect(result.valid).toBe(false);
|
|
101
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
102
|
+
|
|
103
|
+
const severityError = result.errors.find(
|
|
104
|
+
(e) => e.op === 'capture_knowledge' && e.message.toLowerCase().includes('invalid'),
|
|
105
|
+
);
|
|
106
|
+
expect(severityError).toBeDefined();
|
|
107
|
+
expect(severityError!.file).toContain('bad-severity-skill');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('reports an error when scope receives an object instead of a string', () => {
|
|
111
|
+
// create_plan scope expects z.string() but we pass an object
|
|
112
|
+
addSkill(
|
|
113
|
+
skillsDir,
|
|
114
|
+
'bad-scope-skill',
|
|
115
|
+
`# Bad Scope Skill
|
|
116
|
+
|
|
117
|
+
Example with wrong scope type:
|
|
118
|
+
|
|
119
|
+
\`\`\`
|
|
120
|
+
YOUR_AGENT_core op:create_plan params: { title: "My Plan", objective: "Do something", scope: { included: [] } }
|
|
121
|
+
\`\`\`
|
|
122
|
+
`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const result = validateSkillDocs(skillsDir);
|
|
126
|
+
expect(result.valid).toBe(false);
|
|
127
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
128
|
+
|
|
129
|
+
const scopeError = result.errors.find(
|
|
130
|
+
(e) => e.op === 'create_plan' && e.message.includes('scope'),
|
|
131
|
+
);
|
|
132
|
+
expect(scopeError).toBeDefined();
|
|
133
|
+
expect(scopeError!.message).toContain('Expected string');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns structured error objects with required fields', () => {
|
|
137
|
+
addSkill(
|
|
138
|
+
skillsDir,
|
|
139
|
+
'structured-error-skill',
|
|
140
|
+
`# Structured Error Skill
|
|
141
|
+
|
|
142
|
+
\`\`\`
|
|
143
|
+
YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
|
|
144
|
+
\`\`\`
|
|
145
|
+
`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const result = validateSkillDocs(skillsDir);
|
|
149
|
+
|
|
150
|
+
if (result.errors.length > 0) {
|
|
151
|
+
const err = result.errors[0];
|
|
152
|
+
expect(err).toHaveProperty('file');
|
|
153
|
+
expect(err).toHaveProperty('op');
|
|
154
|
+
expect(err).toHaveProperty('message');
|
|
155
|
+
expect(typeof err.file).toBe('string');
|
|
156
|
+
expect(typeof err.op).toBe('string');
|
|
157
|
+
expect(typeof err.message).toBe('string');
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('includes the file path and op name in each error', () => {
|
|
162
|
+
addSkill(
|
|
163
|
+
skillsDir,
|
|
164
|
+
'named-skill',
|
|
165
|
+
`# Named Skill
|
|
166
|
+
|
|
167
|
+
\`\`\`
|
|
168
|
+
YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
|
|
169
|
+
\`\`\`
|
|
170
|
+
`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const result = validateSkillDocs(skillsDir);
|
|
174
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
175
|
+
|
|
176
|
+
const err = result.errors[0];
|
|
177
|
+
expect(err.file).toContain('named-skill');
|
|
178
|
+
expect(err.op).toBe('capture_knowledge');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('builds a non-empty schema registry', () => {
|
|
182
|
+
const result = validateSkillDocs(skillsDir);
|
|
183
|
+
expect(result.registrySize).toBeGreaterThan(50);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('handles a skills directory that does not exist', () => {
|
|
187
|
+
const nonExistentDir = join(skillsDir, 'does-not-exist');
|
|
188
|
+
const result = validateSkillDocs(nonExistentDir);
|
|
189
|
+
expect(result.valid).toBe(true);
|
|
190
|
+
expect(result.totalFiles).toBe(0);
|
|
191
|
+
expect(result.errors).toHaveLength(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('counts multiple skill files correctly', () => {
|
|
195
|
+
addSkill(
|
|
196
|
+
skillsDir,
|
|
197
|
+
'skill-one',
|
|
198
|
+
`# Skill One\n\n\`\`\`\nYOUR_AGENT_core op:capture_quick params: { title: "Test", content: "Content" }\n\`\`\`\n`,
|
|
199
|
+
);
|
|
200
|
+
addSkill(skillsDir, 'skill-two', `# Skill Two\n\nNo examples here.\n`);
|
|
201
|
+
|
|
202
|
+
const result = validateSkillDocs(skillsDir);
|
|
203
|
+
expect(result.totalFiles).toBe(2);
|
|
204
|
+
});
|
|
205
|
+
});
|