@soleri/core 9.14.0 → 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/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +9 -3
- package/dist/skills/sync-skills.js.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 +6 -2
- 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/sync-skills.ts +9 -3
- 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/dist/knowledge-packs/knowledge-packs/community/.gitkeep +0 -0
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/soleri-pack.json +0 -10
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/accessibility.json +0 -53
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/design-tokens.json +0 -26
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/design.json +0 -33
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/styling.json +0 -44
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/ux-laws.json +0 -36
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/ux.json +0 -36
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/soleri-pack.json +0 -10
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/architecture.json +0 -143
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/commercial.json +0 -16
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/communication.json +0 -33
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/component.json +0 -16
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/express.json +0 -34
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/leadership.json +0 -33
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/methodology.json +0 -33
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/monorepo.json +0 -33
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/other.json +0 -73
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/performance.json +0 -35
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/prisma.json +0 -33
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/product-strategy.json +0 -42
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/react.json +0 -47
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/security.json +0 -34
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/testing.json +0 -33
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/tooling.json +0 -85
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/typescript.json +0 -34
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/workflow.json +0 -46
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-uipro/soleri-pack.json +0 -10
- package/dist/knowledge-packs/knowledge-packs/salvador/salvador-uipro/vault/design.json +0 -2589
- package/dist/knowledge-packs/knowledge-packs/starter/architecture/soleri-pack.json +0 -10
- package/dist/knowledge-packs/knowledge-packs/starter/architecture/vault/patterns.json +0 -137
- package/dist/knowledge-packs/knowledge-packs/starter/design/soleri-pack.json +0 -10
- package/dist/knowledge-packs/knowledge-packs/starter/design/vault/patterns.json +0 -137
- package/dist/knowledge-packs/knowledge-packs/starter/security/soleri-pack.json +0 -10
- package/dist/knowledge-packs/knowledge-packs/starter/security/vault/patterns.json +0 -137
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/api-design/soleri-pack.json +0 -0
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/api-design/vault/patterns.json +0 -0
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/nodejs/soleri-pack.json +0 -0
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/nodejs/vault/patterns.json +0 -0
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/react/soleri-pack.json +0 -0
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/react/vault/patterns.json +0 -0
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/testing/soleri-pack.json +0 -0
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/testing/vault/patterns.json +0 -0
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/typescript/soleri-pack.json +0 -0
- /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/typescript/vault/patterns.json +0 -0
|
@@ -74,6 +74,26 @@ describe('Planner', () => {
|
|
|
74
74
|
const planner2 = new Planner(join(tempDir, 'plans.json'));
|
|
75
75
|
expect(planner2.list()).toHaveLength(1);
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
it('should make newly created plans visible to other planner instances', () => {
|
|
79
|
+
const planner2 = new Planner(join(tempDir, 'plans.json'));
|
|
80
|
+
const plan = planner.create({ objective: 'Shared plan', scope: 'sync' });
|
|
81
|
+
|
|
82
|
+
expect(planner2.get(plan.id)?.objective).toBe('Shared plan');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should merge plans created by multiple planner instances', () => {
|
|
86
|
+
const planner2 = new Planner(join(tempDir, 'plans.json'));
|
|
87
|
+
const planA = planner.create({ objective: 'Plan A', scope: 'sync-a' });
|
|
88
|
+
const planB = planner2.create({ objective: 'Plan B', scope: 'sync-b' });
|
|
89
|
+
|
|
90
|
+
const reloaded = new Planner(join(tempDir, 'plans.json'));
|
|
91
|
+
const ids = reloaded.list().map((plan) => plan.id);
|
|
92
|
+
|
|
93
|
+
expect(ids).toContain(planA.id);
|
|
94
|
+
expect(ids).toContain(planB.id);
|
|
95
|
+
expect(ids).toHaveLength(2);
|
|
96
|
+
});
|
|
77
97
|
});
|
|
78
98
|
|
|
79
99
|
describe('get', () => {
|
|
@@ -120,26 +140,36 @@ describe('Planner', () => {
|
|
|
120
140
|
|
|
121
141
|
it('should approve a plan with A+ grade', () => {
|
|
122
142
|
const plan = planner.create({
|
|
123
|
-
objective: '
|
|
124
|
-
scope: '
|
|
143
|
+
objective: 'Implement a Redis caching layer for the API to reduce DB load by 50%',
|
|
144
|
+
scope: 'Backend API services only. Does not include frontend caching or CDN.',
|
|
125
145
|
tasks: [
|
|
126
|
-
{
|
|
127
|
-
|
|
146
|
+
{
|
|
147
|
+
title: 'Set up Redis client',
|
|
148
|
+
description: 'Install and configure Redis connection pool',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
title: 'Add cache middleware',
|
|
152
|
+
description: 'Express middleware for transparent caching',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
title: 'Add invalidation logic',
|
|
156
|
+
description: 'Purge cache on write operations to ensure consistency',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
title: 'Write integration tests',
|
|
160
|
+
description: 'Test cache hit/miss scenarios with Redis',
|
|
161
|
+
},
|
|
162
|
+
{ title: 'Add monitoring', description: 'Track and verify cache hit rate metrics' },
|
|
128
163
|
],
|
|
129
|
-
decisions: [
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
gaps: [],
|
|
139
|
-
iteration: 1,
|
|
140
|
-
checkedAt: Date.now(),
|
|
141
|
-
};
|
|
142
|
-
p.checks = [p.latestCheck];
|
|
164
|
+
decisions: [
|
|
165
|
+
'Use Redis because it provides sub-millisecond latency and supports TTL natively',
|
|
166
|
+
'Set TTL to 5 minutes since average data freshness requirement is 10 minutes',
|
|
167
|
+
],
|
|
168
|
+
alternatives: TWO_ALTERNATIVES,
|
|
169
|
+
});
|
|
170
|
+
const check = planner.grade(plan.id);
|
|
171
|
+
expect(check.score).toBeGreaterThanOrEqual(95);
|
|
172
|
+
expect(check.grade).toMatch(/^A/);
|
|
143
173
|
const approved = planner.approve(plan.id);
|
|
144
174
|
expect(approved.status).toBe('approved');
|
|
145
175
|
});
|
|
@@ -149,27 +179,8 @@ describe('Planner', () => {
|
|
|
149
179
|
objective: 'Bad plan',
|
|
150
180
|
scope: 'test',
|
|
151
181
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
p.latestCheck = {
|
|
155
|
-
checkId: 'chk-test',
|
|
156
|
-
planId: plan.id,
|
|
157
|
-
grade: 'B',
|
|
158
|
-
score: 82,
|
|
159
|
-
gaps: [
|
|
160
|
-
{
|
|
161
|
-
id: 'gap-1',
|
|
162
|
-
severity: 'major',
|
|
163
|
-
category: 'completeness',
|
|
164
|
-
description: 'Missing tasks',
|
|
165
|
-
recommendation: 'Add tasks',
|
|
166
|
-
location: 'tasks',
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
iteration: 1,
|
|
170
|
-
checkedAt: Date.now(),
|
|
171
|
-
};
|
|
172
|
-
p.checks = [p.latestCheck];
|
|
182
|
+
const check = planner.grade(plan.id);
|
|
183
|
+
expect(check.score).toBeLessThan(90);
|
|
173
184
|
expect(() => planner.approve(plan.id)).toThrow(PlanGradeRejectionError);
|
|
174
185
|
});
|
|
175
186
|
|
|
@@ -184,68 +195,52 @@ describe('Planner', () => {
|
|
|
184
195
|
const lenientPlanner = new Planner(join(tempDir, 'lenient-plans.json'), {
|
|
185
196
|
minGradeForApproval: 'B',
|
|
186
197
|
});
|
|
187
|
-
const plan = lenientPlanner.create({
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
};
|
|
199
|
-
p.checks = [p.latestCheck];
|
|
200
|
-
// B grade should pass with B threshold
|
|
198
|
+
const plan = lenientPlanner.create({
|
|
199
|
+
objective: 'B-grade plan with enough detail',
|
|
200
|
+
scope: 'Test scope with clear boundary',
|
|
201
|
+
decisions: ['Use TypeScript for compile-time safety'],
|
|
202
|
+
tasks: [
|
|
203
|
+
{ title: 'Task 1', description: 'Implement the first part of the change' },
|
|
204
|
+
{ title: 'Task 2', description: 'Validate the second part of the change' },
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
const check = lenientPlanner.grade(plan.id);
|
|
208
|
+
expect(check.grade).toBe('B');
|
|
201
209
|
const approved = lenientPlanner.approve(plan.id);
|
|
202
210
|
expect(approved.status).toBe('approved');
|
|
203
211
|
});
|
|
204
212
|
|
|
205
213
|
it('should reject with PlanGradeRejectionError containing gap details', () => {
|
|
206
|
-
const plan = planner.create({ objective: '
|
|
207
|
-
const
|
|
208
|
-
const testGaps = [
|
|
209
|
-
{
|
|
210
|
-
id: 'gap-crit',
|
|
211
|
-
severity: 'critical' as const,
|
|
212
|
-
category: 'structure',
|
|
213
|
-
description: 'Missing critical structure',
|
|
214
|
-
recommendation: 'Fix structure',
|
|
215
|
-
location: 'tasks',
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
id: 'gap-maj',
|
|
219
|
-
severity: 'major' as const,
|
|
220
|
-
category: 'completeness',
|
|
221
|
-
description: 'Incomplete scope',
|
|
222
|
-
recommendation: 'Add scope details',
|
|
223
|
-
location: 'scope',
|
|
224
|
-
},
|
|
225
|
-
];
|
|
226
|
-
p.latestCheck = {
|
|
227
|
-
checkId: 'chk-test-gaps',
|
|
228
|
-
planId: plan.id,
|
|
229
|
-
grade: 'C',
|
|
230
|
-
score: 65,
|
|
231
|
-
gaps: testGaps,
|
|
232
|
-
iteration: 1,
|
|
233
|
-
checkedAt: Date.now(),
|
|
234
|
-
};
|
|
235
|
-
p.checks = [p.latestCheck];
|
|
214
|
+
const plan = planner.create({ objective: '', scope: '' });
|
|
215
|
+
const check = planner.grade(plan.id);
|
|
236
216
|
try {
|
|
237
217
|
planner.approve(plan.id);
|
|
238
218
|
expect.unreachable('Should have thrown');
|
|
239
219
|
} catch (err) {
|
|
240
220
|
expect(err).toBeInstanceOf(PlanGradeRejectionError);
|
|
241
221
|
const rejection = err as PlanGradeRejectionError;
|
|
242
|
-
expect(rejection.grade).toBe(
|
|
243
|
-
expect(rejection.score).toBe(
|
|
222
|
+
expect(rejection.grade).toBe(check.grade);
|
|
223
|
+
expect(rejection.score).toBe(check.score);
|
|
244
224
|
expect(rejection.minGrade).toBe('A');
|
|
245
|
-
expect(rejection.gaps).
|
|
225
|
+
expect(rejection.gaps).toEqual(check.gaps);
|
|
246
226
|
expect(rejection.message).toContain('below the minimum required grade A');
|
|
247
227
|
}
|
|
248
228
|
});
|
|
229
|
+
|
|
230
|
+
it('should respect grading performed by another planner instance', () => {
|
|
231
|
+
const strictPlanner = new Planner(join(tempDir, 'shared-plans.json'), {
|
|
232
|
+
minGradeForApproval: 'A',
|
|
233
|
+
});
|
|
234
|
+
const gradingPlanner = new Planner(join(tempDir, 'shared-plans.json'), {
|
|
235
|
+
minGradeForApproval: 'A',
|
|
236
|
+
});
|
|
237
|
+
const plan = strictPlanner.create({ objective: '', scope: '' });
|
|
238
|
+
|
|
239
|
+
const check = gradingPlanner.grade(plan.id);
|
|
240
|
+
|
|
241
|
+
expect(check.grade).toBe('F');
|
|
242
|
+
expect(() => strictPlanner.approve(plan.id)).toThrow(PlanGradeRejectionError);
|
|
243
|
+
});
|
|
249
244
|
});
|
|
250
245
|
|
|
251
246
|
describe('startExecution', () => {
|
|
@@ -592,10 +587,11 @@ describe('Planner', () => {
|
|
|
592
587
|
{ title: 'Task C', description: 'Third task (not in cycle)' },
|
|
593
588
|
],
|
|
594
589
|
});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
590
|
+
planner.splitTasks(plan.id, [
|
|
591
|
+
{ title: 'Task A', description: 'First task in the cycle', dependsOn: ['task-2'] },
|
|
592
|
+
{ title: 'Task B', description: 'Second task in the cycle', dependsOn: ['task-1'] },
|
|
593
|
+
{ title: 'Task C', description: 'Third task (not in cycle)' },
|
|
594
|
+
]);
|
|
599
595
|
const check = planner.grade(plan.id);
|
|
600
596
|
const circGap = check.gaps.find((g) => g.description.includes('Circular'));
|
|
601
597
|
expect(circGap).toBeDefined();
|
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
|
/**
|
|
@@ -257,20 +257,26 @@ export function syncSkillsToClaudeCode(
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
/**
|
|
260
|
-
* Remove
|
|
260
|
+
* Remove ALL `{agent}-soleri-*` entries from ~/.claude/skills/
|
|
261
261
|
* to clean up duplicates left by the old global-install behavior.
|
|
262
|
+
*
|
|
263
|
+
* Cleans entries from ALL agents, not just the current one — any
|
|
264
|
+
* `*-soleri-*` entry in the global dir is a stale copy from a previous
|
|
265
|
+
* global install. Canonical skills now live in project-local .claude/skills/.
|
|
262
266
|
*/
|
|
263
267
|
function cleanStaleGlobalSkills(agentName: string, result: SyncResult): void {
|
|
264
268
|
const globalSkillsDir = join(homedir(), '.claude', 'skills');
|
|
265
269
|
if (!existsSync(globalSkillsDir)) return;
|
|
266
270
|
|
|
267
|
-
|
|
271
|
+
// Match any agent-prefixed soleri skill: <anything>-soleri-<skillname>
|
|
272
|
+
// Canonical project-local names look like "soleri-*" (no agent prefix).
|
|
273
|
+
const stalePattern = /^.+-soleri-.+$/;
|
|
268
274
|
|
|
269
275
|
try {
|
|
270
276
|
const entries = readdirSync(globalSkillsDir, { withFileTypes: true });
|
|
271
277
|
for (const entry of entries) {
|
|
272
278
|
if (!entry.isDirectory()) continue;
|
|
273
|
-
if (!entry.name
|
|
279
|
+
if (!stalePattern.test(entry.name)) continue;
|
|
274
280
|
|
|
275
281
|
const staleDir = join(globalSkillsDir, entry.name);
|
|
276
282
|
try {
|