@soleri/core 9.3.1 → 9.4.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/intelligence.d.ts +5 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +115 -26
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/learning-radar.d.ts +3 -3
- package/dist/brain/learning-radar.d.ts.map +1 -1
- package/dist/brain/learning-radar.js +8 -4
- package/dist/brain/learning-radar.js.map +1 -1
- package/dist/control/intent-router.d.ts +2 -2
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +35 -1
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +10 -2
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +23 -1
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/schema.d.ts +1 -1
- package/dist/curator/schema.d.ts.map +1 -1
- package/dist/curator/schema.js +8 -0
- package/dist/curator/schema.js.map +1 -1
- package/dist/domain-packs/types.d.ts +6 -0
- package/dist/domain-packs/types.d.ts.map +1 -1
- package/dist/domain-packs/types.js +1 -0
- package/dist/domain-packs/types.js.map +1 -1
- package/dist/engine/module-manifest.js +3 -3
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts +9 -0
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +59 -1
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/facades/types.d.ts +5 -1
- package/dist/facades/types.d.ts.map +1 -1
- package/dist/facades/types.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/operator/operator-context-store.d.ts +54 -0
- package/dist/operator/operator-context-store.d.ts.map +1 -0
- package/dist/operator/operator-context-store.js +434 -0
- package/dist/operator/operator-context-store.js.map +1 -0
- package/dist/operator/operator-context-types.d.ts +101 -0
- package/dist/operator/operator-context-types.d.ts.map +1 -0
- package/dist/operator/operator-context-types.js +27 -0
- package/dist/operator/operator-context-types.js.map +1 -0
- package/dist/packs/index.d.ts +2 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +3 -0
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/types.d.ts +8 -2
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +6 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts +12 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +52 -19
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +6 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +21 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +62 -3
- package/dist/planning/planner.js.map +1 -1
- package/dist/planning/task-complexity-assessor.d.ts.map +1 -1
- package/dist/planning/task-complexity-assessor.js.map +1 -1
- package/dist/plugins/types.d.ts +18 -18
- package/dist/runtime/admin-ops.d.ts +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +100 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +19 -9
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +35 -7
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +4 -2
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +8 -2
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +13 -0
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +10 -12
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +36 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +20 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +71 -4
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/plan-feedback-helper.d.ts +21 -0
- package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
- package/dist/runtime/plan-feedback-helper.js +52 -0
- package/dist/runtime/plan-feedback-helper.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +73 -34
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +9 -1
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -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 +13 -7
- package/dist/skills/sync-skills.js.map +1 -1
- package/package.json +1 -1
- package/src/brain/brain-intelligence.test.ts +30 -0
- package/src/brain/extraction-quality.test.ts +323 -0
- package/src/brain/intelligence.ts +133 -30
- package/src/brain/learning-radar.ts +8 -5
- package/src/brain/second-brain-features.test.ts +1 -1
- package/src/control/intent-router.test.ts +73 -3
- package/src/control/intent-router.ts +38 -1
- package/src/control/types.ts +13 -2
- package/src/curator/curator.test.ts +92 -0
- package/src/curator/curator.ts +29 -1
- package/src/curator/schema.ts +8 -0
- package/src/domain-packs/types.ts +8 -0
- package/src/engine/module-manifest.test.ts +8 -2
- package/src/engine/module-manifest.ts +3 -3
- package/src/engine/register-engine.test.ts +73 -1
- package/src/engine/register-engine.ts +61 -1
- package/src/facades/types.ts +5 -0
- package/src/index.ts +22 -0
- package/src/operator/operator-context-store.test.ts +698 -0
- package/src/operator/operator-context-store.ts +569 -0
- package/src/operator/operator-context-types.ts +139 -0
- package/src/packs/index.ts +3 -1
- package/src/packs/lockfile.ts +3 -0
- package/src/packs/types.ts +9 -0
- package/src/planning/plan-lifecycle.ts +80 -22
- package/src/planning/planner-types.ts +6 -0
- package/src/planning/planner.ts +74 -4
- package/src/planning/task-complexity-assessor.test.ts +6 -2
- package/src/planning/task-complexity-assessor.ts +1 -4
- package/src/runtime/admin-ops.test.ts +139 -6
- package/src/runtime/admin-ops.ts +104 -3
- package/src/runtime/admin-setup-ops.ts +30 -10
- package/src/runtime/capture-ops.test.ts +84 -0
- package/src/runtime/capture-ops.ts +35 -7
- package/src/runtime/facades/admin-facade.test.ts +1 -1
- package/src/runtime/facades/brain-facade.ts +6 -3
- package/src/runtime/facades/control-facade.ts +10 -2
- package/src/runtime/facades/curator-facade.ts +18 -0
- package/src/runtime/facades/memory-facade.test.ts +14 -12
- package/src/runtime/facades/memory-facade.ts +10 -12
- package/src/runtime/facades/orchestrate-facade.ts +33 -1
- package/src/runtime/facades/plan-facade.test.ts +213 -0
- package/src/runtime/facades/plan-facade.ts +23 -4
- package/src/runtime/orchestrate-ops.test.ts +202 -2
- package/src/runtime/orchestrate-ops.ts +85 -4
- package/src/runtime/plan-feedback-helper.test.ts +173 -0
- package/src/runtime/plan-feedback-helper.ts +63 -0
- package/src/runtime/planning-extra-ops.test.ts +43 -1
- package/src/runtime/planning-extra-ops.ts +96 -33
- package/src/runtime/session-briefing.test.ts +1 -0
- package/src/runtime/session-briefing.ts +10 -1
- package/src/runtime/types.ts +3 -0
- package/src/skills/sync-skills.ts +14 -7
- package/vitest.config.ts +1 -0
|
@@ -239,6 +239,66 @@ describe('plan-facade', () => {
|
|
|
239
239
|
expect(data.iterated).toBe(true);
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
+
it('plan_iterate with decisions persists them', async () => {
|
|
243
|
+
const createResult = await executeOp(ops, 'create_plan', {
|
|
244
|
+
objective: 'Decisions test',
|
|
245
|
+
scope: 'test',
|
|
246
|
+
});
|
|
247
|
+
const planId = ((createResult.data as Record<string, unknown>).plan as Record<string, unknown>)
|
|
248
|
+
.id as string;
|
|
249
|
+
|
|
250
|
+
const result = await executeOp(ops, 'plan_iterate', {
|
|
251
|
+
planId,
|
|
252
|
+
decisions: [{ decision: 'Use FTS5', rationale: 'Performance' }],
|
|
253
|
+
});
|
|
254
|
+
expect(result.success).toBe(true);
|
|
255
|
+
const data = result.data as { iterated: boolean; plan: Record<string, unknown> };
|
|
256
|
+
expect(data.iterated).toBe(true);
|
|
257
|
+
const decisions = data.plan.decisions as Array<Record<string, string>>;
|
|
258
|
+
expect(decisions).toHaveLength(1);
|
|
259
|
+
expect(decisions[0].decision).toBe('Use FTS5');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('plan_iterate with alternatives persists them', async () => {
|
|
263
|
+
const createResult = await executeOp(ops, 'create_plan', {
|
|
264
|
+
objective: 'Alternatives test',
|
|
265
|
+
scope: 'test',
|
|
266
|
+
});
|
|
267
|
+
const planId = ((createResult.data as Record<string, unknown>).plan as Record<string, unknown>)
|
|
268
|
+
.id as string;
|
|
269
|
+
|
|
270
|
+
const result = await executeOp(ops, 'plan_iterate', {
|
|
271
|
+
planId,
|
|
272
|
+
alternatives: [
|
|
273
|
+
{ approach: 'Alt A', pros: ['fast'], cons: ['fragile'], rejected_reason: 'Too risky' },
|
|
274
|
+
{ approach: 'Alt B', pros: ['safe'], cons: ['slow'], rejected_reason: 'Too slow' },
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
expect(result.success).toBe(true);
|
|
278
|
+
const data = result.data as { iterated: boolean; plan: Record<string, unknown> };
|
|
279
|
+
expect(data.iterated).toBe(true);
|
|
280
|
+
const alternatives = data.plan.alternatives as Array<Record<string, unknown>>;
|
|
281
|
+
expect(alternatives).toHaveLength(2);
|
|
282
|
+
expect(alternatives[0].approach).toBe('Alt A');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('plan_iterate with no effective changes returns iterated: false', async () => {
|
|
286
|
+
const createResult = await executeOp(ops, 'create_plan', {
|
|
287
|
+
objective: 'No-op test',
|
|
288
|
+
scope: 'test',
|
|
289
|
+
});
|
|
290
|
+
const planId = ((createResult.data as Record<string, unknown>).plan as Record<string, unknown>)
|
|
291
|
+
.id as string;
|
|
292
|
+
|
|
293
|
+
const result = await executeOp(ops, 'plan_iterate', {
|
|
294
|
+
planId,
|
|
295
|
+
});
|
|
296
|
+
expect(result.success).toBe(true);
|
|
297
|
+
const data = result.data as { iterated: boolean; reason?: string };
|
|
298
|
+
expect(data.iterated).toBe(false);
|
|
299
|
+
expect(data.reason).toBe('no changes detected');
|
|
300
|
+
});
|
|
301
|
+
|
|
242
302
|
// ─── plan_stats ────────────────────────────────────────────────
|
|
243
303
|
|
|
244
304
|
it('plan_stats returns statistics', async () => {
|
|
@@ -280,4 +340,157 @@ describe('plan-facade', () => {
|
|
|
280
340
|
expect(result.success).toBe(true);
|
|
281
341
|
expect((result.data as Record<string, unknown>).error).toContain('not found');
|
|
282
342
|
});
|
|
343
|
+
|
|
344
|
+
// ─── create_plan vault enrichment ─────────────────────────────
|
|
345
|
+
|
|
346
|
+
it('create_plan enriches decisions with vault patterns when matches exist', async () => {
|
|
347
|
+
// Seed vault with a relevant entry
|
|
348
|
+
vault.add({
|
|
349
|
+
title: 'SQLite FTS5 search pattern',
|
|
350
|
+
description: 'Use FTS5 with porter tokenizer for all text search',
|
|
351
|
+
content: 'Use FTS5 with porter tokenizer for all text search',
|
|
352
|
+
type: 'pattern',
|
|
353
|
+
domain: 'architecture',
|
|
354
|
+
severity: 'suggestion',
|
|
355
|
+
tags: ['sqlite', 'search'],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const result = await executeOp(ops, 'create_plan', {
|
|
359
|
+
objective: 'Implement text search with SQLite FTS5',
|
|
360
|
+
scope: 'packages/core/src/vault',
|
|
361
|
+
});
|
|
362
|
+
expect(result.success).toBe(true);
|
|
363
|
+
const data = result.data as {
|
|
364
|
+
created: boolean;
|
|
365
|
+
plan: Record<string, unknown>;
|
|
366
|
+
vaultEntryIds: string[];
|
|
367
|
+
};
|
|
368
|
+
expect(data.created).toBe(true);
|
|
369
|
+
expect(data.vaultEntryIds.length).toBeGreaterThan(0);
|
|
370
|
+
// Decisions should contain vault pattern references with entryId markers
|
|
371
|
+
const decisions = data.plan.decisions as string[];
|
|
372
|
+
const vaultDecisions = decisions.filter((d) => d.startsWith('Vault pattern:'));
|
|
373
|
+
expect(vaultDecisions.length).toBeGreaterThan(0);
|
|
374
|
+
// Each vault decision should have an [entryId:...] marker for brain feedback
|
|
375
|
+
for (const vd of vaultDecisions) {
|
|
376
|
+
expect(vd).toMatch(/\[entryId:[^\]]+\]/);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('create_plan works without vault matches (empty vault)', async () => {
|
|
381
|
+
// Fresh vault, no entries
|
|
382
|
+
const result = await executeOp(ops, 'create_plan', {
|
|
383
|
+
objective: 'Something completely unrelated xyz123',
|
|
384
|
+
scope: 'test',
|
|
385
|
+
});
|
|
386
|
+
expect(result.success).toBe(true);
|
|
387
|
+
const data = result.data as {
|
|
388
|
+
created: boolean;
|
|
389
|
+
vaultEntryIds: string[];
|
|
390
|
+
};
|
|
391
|
+
expect(data.created).toBe(true);
|
|
392
|
+
expect(data.vaultEntryIds).toEqual([]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('create_plan preserves user decisions alongside vault enrichment', async () => {
|
|
396
|
+
vault.add({
|
|
397
|
+
title: 'Testing pattern',
|
|
398
|
+
description: 'Always write tests before implementation',
|
|
399
|
+
content: 'Always write tests before implementation',
|
|
400
|
+
type: 'pattern',
|
|
401
|
+
domain: 'testing',
|
|
402
|
+
severity: 'suggestion',
|
|
403
|
+
tags: ['tdd'],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const result = await executeOp(ops, 'create_plan', {
|
|
407
|
+
objective: 'Add testing patterns to the project',
|
|
408
|
+
scope: 'packages/core',
|
|
409
|
+
decisions: ['Use vitest as test runner'],
|
|
410
|
+
});
|
|
411
|
+
expect(result.success).toBe(true);
|
|
412
|
+
const data = result.data as { plan: Record<string, unknown>; vaultEntryIds: string[] };
|
|
413
|
+
const decisions = data.plan.decisions as string[];
|
|
414
|
+
// User decision preserved
|
|
415
|
+
expect(decisions).toContain('Use vitest as test runner');
|
|
416
|
+
// Vault enrichment added
|
|
417
|
+
if (data.vaultEntryIds.length > 0) {
|
|
418
|
+
const vaultDecisions = decisions.filter((d) => d.startsWith('Vault pattern:'));
|
|
419
|
+
expect(vaultDecisions.length).toBeGreaterThan(0);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// ─── plan_close_stale ─────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
it('plan_close_stale op is registered', () => {
|
|
426
|
+
expect([...ops.keys()]).toContain('plan_close_stale');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('plan_close_stale returns no plans when none are stale', async () => {
|
|
430
|
+
const result = await executeOp(ops, 'plan_close_stale', {});
|
|
431
|
+
expect(result.success).toBe(true);
|
|
432
|
+
const data = result.data as { closed: number; plans: unknown[] };
|
|
433
|
+
expect(data.closed).toBe(0);
|
|
434
|
+
expect(data.plans).toHaveLength(0);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('plan_close_stale with olderThanMs: 0 closes all non-terminal plans', async () => {
|
|
438
|
+
// Create a draft plan
|
|
439
|
+
await executeOp(ops, 'create_plan', { objective: 'Stale test', scope: 'test' });
|
|
440
|
+
|
|
441
|
+
// Close immediately (olderThanMs: 0 means close everything)
|
|
442
|
+
const result = await executeOp(ops, 'plan_close_stale', { olderThanMs: 0 });
|
|
443
|
+
expect(result.success).toBe(true);
|
|
444
|
+
const data = result.data as { closed: number; plans: Array<{ id: string; reason: string }> };
|
|
445
|
+
expect(data.closed).toBeGreaterThanOrEqual(1);
|
|
446
|
+
expect(data.plans[0].reason).toContain('ttl-expired');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// ─── Planner.closeStale() ─────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
it('closeStale closes draft plans older than TTL', () => {
|
|
452
|
+
const runtime = makeRuntime(vault);
|
|
453
|
+
const planner = runtime.planner;
|
|
454
|
+
|
|
455
|
+
// Create a plan — it's immediately a draft
|
|
456
|
+
planner.create({ objective: 'Old draft', scope: 'test' });
|
|
457
|
+
|
|
458
|
+
// Close with olderThanMs: 0 to force-close regardless of age
|
|
459
|
+
const result = planner.closeStale(0);
|
|
460
|
+
expect(result.closedPlans.length).toBeGreaterThanOrEqual(1);
|
|
461
|
+
expect(result.closedPlans[0].previousStatus).toBe('draft');
|
|
462
|
+
expect(result.closedPlans[0].reason).toContain('ttl-expired');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('closeStale does not close completed plans', () => {
|
|
466
|
+
const runtime = makeRuntime(vault);
|
|
467
|
+
const planner = runtime.planner;
|
|
468
|
+
|
|
469
|
+
// Create and complete a plan
|
|
470
|
+
const plan = planner.create({
|
|
471
|
+
objective: 'Completed plan',
|
|
472
|
+
scope: 'test',
|
|
473
|
+
decisions: ['d1', 'd2'],
|
|
474
|
+
tasks: [{ title: 'T1', description: 'd1' }],
|
|
475
|
+
});
|
|
476
|
+
planner.approve(plan.id);
|
|
477
|
+
planner.startExecution(plan.id);
|
|
478
|
+
planner.complete(plan.id);
|
|
479
|
+
|
|
480
|
+
// closeStale should not touch it
|
|
481
|
+
const result = planner.closeStale(0);
|
|
482
|
+
expect(result.closedPlans.filter((p) => p.id === plan.id)).toHaveLength(0);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('closeStale respects default TTL — does not close fresh drafts', () => {
|
|
486
|
+
const runtime = makeRuntime(vault);
|
|
487
|
+
const planner = runtime.planner;
|
|
488
|
+
|
|
489
|
+
// Create a fresh plan
|
|
490
|
+
planner.create({ objective: 'Fresh draft', scope: 'test' });
|
|
491
|
+
|
|
492
|
+
// Close with default TTL (30 min) — fresh plan should NOT be closed
|
|
493
|
+
const result = planner.closeStale();
|
|
494
|
+
expect(result.closedPlans).toHaveLength(0);
|
|
495
|
+
});
|
|
283
496
|
});
|
|
@@ -12,7 +12,7 @@ import { createChainOps } from '../chain-ops.js';
|
|
|
12
12
|
import { PlanGradeRejectionError } from '../../planning/planner.js';
|
|
13
13
|
|
|
14
14
|
export function createPlanFacadeOps(runtime: AgentRuntime): OpDefinition[] {
|
|
15
|
-
const { planner } = runtime;
|
|
15
|
+
const { planner, vault } = runtime;
|
|
16
16
|
|
|
17
17
|
return [
|
|
18
18
|
// ─── Planning (inline from core-ops.ts) ─────────────────────
|
|
@@ -45,16 +45,35 @@ export function createPlanFacadeOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
45
45
|
.describe('Rejected alternative approaches — plans with 2+ alternatives score higher'),
|
|
46
46
|
}),
|
|
47
47
|
handler: async (params) => {
|
|
48
|
+
const objective = params.objective as string;
|
|
49
|
+
const decisions = ((params.decisions as string[]) ?? []).slice();
|
|
50
|
+
|
|
51
|
+
// Vault enrichment: search for patterns matching the objective
|
|
52
|
+
let vaultEntryIds: string[] = [];
|
|
53
|
+
try {
|
|
54
|
+
const results = vault.search(objective, { limit: 5 });
|
|
55
|
+
if (results.length > 0) {
|
|
56
|
+
vaultEntryIds = results.map((r) => r.entry.id);
|
|
57
|
+
for (const r of results) {
|
|
58
|
+
decisions.push(
|
|
59
|
+
`Vault pattern: ${r.entry.title ?? r.entry.id} (score: ${r.score.toFixed(2)}) [entryId:${r.entry.id}]`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Vault search failed — proceed without enrichment
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
const plan = planner.create({
|
|
49
|
-
objective
|
|
68
|
+
objective,
|
|
50
69
|
scope: params.scope as string,
|
|
51
|
-
decisions
|
|
70
|
+
decisions,
|
|
52
71
|
tasks: (params.tasks as Array<{ title: string; description: string }>) ?? [],
|
|
53
72
|
alternatives: params.alternatives as
|
|
54
73
|
| Array<{ approach: string; pros: string[]; cons: string[]; rejected_reason: string }>
|
|
55
74
|
| undefined,
|
|
56
75
|
});
|
|
57
|
-
return { created: true, plan };
|
|
76
|
+
return { created: true, plan, vaultEntryIds };
|
|
58
77
|
},
|
|
59
78
|
},
|
|
60
79
|
{
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
4
|
import { createOrchestrateOps } from './orchestrate-ops.js';
|
|
3
5
|
import { assessTaskComplexity } from '../planning/task-complexity-assessor.js';
|
|
4
6
|
import type { AgentRuntime } from './types.js';
|
|
5
7
|
|
|
8
|
+
vi.mock('node:fs', () => ({
|
|
9
|
+
default: {
|
|
10
|
+
mkdirSync: vi.fn(),
|
|
11
|
+
writeFileSync: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
6
15
|
// ---------------------------------------------------------------------------
|
|
7
16
|
// Mocks for external modules
|
|
8
17
|
// ---------------------------------------------------------------------------
|
|
@@ -441,8 +450,6 @@ describe('createOrchestrateOps', () => {
|
|
|
441
450
|
});
|
|
442
451
|
|
|
443
452
|
it('orchestrate_complete captures knowledge in both paths', async () => {
|
|
444
|
-
const completeOp = findOp(ops, 'orchestrate_complete');
|
|
445
|
-
|
|
446
453
|
// ── Simple path (no planId) ──
|
|
447
454
|
vi.clearAllMocks();
|
|
448
455
|
rt = mockRuntime();
|
|
@@ -492,6 +499,199 @@ describe('createOrchestrateOps', () => {
|
|
|
492
499
|
expect(result.reasoning.length).toBeGreaterThan(0);
|
|
493
500
|
});
|
|
494
501
|
|
|
502
|
+
it('orchestrate_complete compounds operator signals when provided', async () => {
|
|
503
|
+
const compoundSignalsMock = vi.fn();
|
|
504
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
505
|
+
compoundSignals: compoundSignalsMock,
|
|
506
|
+
hasDrifted: vi.fn().mockReturnValue(false),
|
|
507
|
+
renderContextFile: vi.fn(),
|
|
508
|
+
};
|
|
509
|
+
ops = createOrchestrateOps(rt);
|
|
510
|
+
|
|
511
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
512
|
+
await op.handler({
|
|
513
|
+
sessionId: 'session-1',
|
|
514
|
+
outcome: 'completed',
|
|
515
|
+
operatorSignals: {
|
|
516
|
+
expertise: [{ topic: 'typescript', level: 'expert', confidence: 0.9 }],
|
|
517
|
+
corrections: [{ rule: 'use conventional commits', scope: 'global' }],
|
|
518
|
+
interests: [{ tag: 'coffee' }],
|
|
519
|
+
patterns: [{ pattern: 'prefers small PRs', frequency: 'frequent' }],
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
expect(compoundSignalsMock).toHaveBeenCalledWith(
|
|
524
|
+
{
|
|
525
|
+
expertise: [{ topic: 'typescript', level: 'expert', confidence: 0.9 }],
|
|
526
|
+
corrections: [{ rule: 'use conventional commits', scope: 'global' }],
|
|
527
|
+
interests: [{ tag: 'coffee' }],
|
|
528
|
+
patterns: [{ pattern: 'prefers small PRs', frequency: 'frequent' }],
|
|
529
|
+
},
|
|
530
|
+
'session-1',
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('orchestrate_complete handles empty operator signals gracefully', async () => {
|
|
535
|
+
const compoundSignalsMock = vi.fn();
|
|
536
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
537
|
+
compoundSignals: compoundSignalsMock,
|
|
538
|
+
hasDrifted: vi.fn().mockReturnValue(false),
|
|
539
|
+
renderContextFile: vi.fn(),
|
|
540
|
+
};
|
|
541
|
+
ops = createOrchestrateOps(rt);
|
|
542
|
+
|
|
543
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
544
|
+
await op.handler({
|
|
545
|
+
sessionId: 'session-1',
|
|
546
|
+
outcome: 'completed',
|
|
547
|
+
operatorSignals: {},
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Empty object with default arrays should be passed through
|
|
551
|
+
expect(compoundSignalsMock).toHaveBeenCalledTimes(1);
|
|
552
|
+
const [passedSignals, passedSessionId] = compoundSignalsMock.mock.calls[0];
|
|
553
|
+
expect(passedSessionId).toBe('session-1');
|
|
554
|
+
// Zod defaults produce empty arrays for each field
|
|
555
|
+
expect(passedSignals).toBeDefined();
|
|
556
|
+
expect(Array.isArray(passedSignals.expertise ?? [])).toBe(true);
|
|
557
|
+
expect(Array.isArray(passedSignals.corrections ?? [])).toBe(true);
|
|
558
|
+
expect(Array.isArray(passedSignals.interests ?? [])).toBe(true);
|
|
559
|
+
expect(Array.isArray(passedSignals.patterns ?? [])).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('orchestrate_complete works when operatorContextStore not available', async () => {
|
|
563
|
+
// Ensure no operatorContextStore on runtime (backward compat)
|
|
564
|
+
delete (rt as Record<string, unknown>).operatorContextStore;
|
|
565
|
+
ops = createOrchestrateOps(rt);
|
|
566
|
+
|
|
567
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
568
|
+
const result = (await op.handler({
|
|
569
|
+
sessionId: 'session-1',
|
|
570
|
+
outcome: 'completed',
|
|
571
|
+
operatorSignals: {
|
|
572
|
+
expertise: [{ topic: 'react', level: 'intermediate' }],
|
|
573
|
+
corrections: [],
|
|
574
|
+
interests: [],
|
|
575
|
+
patterns: [],
|
|
576
|
+
},
|
|
577
|
+
})) as Record<string, unknown>;
|
|
578
|
+
|
|
579
|
+
// Should complete normally without errors
|
|
580
|
+
expect(result).toHaveProperty('plan');
|
|
581
|
+
expect(result).toHaveProperty('session');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('orchestrate_complete re-renders context file when drift detected', async () => {
|
|
585
|
+
const compoundSignalsMock = vi.fn();
|
|
586
|
+
const hasDriftedMock = vi.fn().mockReturnValue(true);
|
|
587
|
+
const renderContextFileMock = vi
|
|
588
|
+
.fn()
|
|
589
|
+
.mockReturnValue(
|
|
590
|
+
'# Operator Context\n\n**Expertise:** typescript (expert, 1 sessions, confidence 0.90).',
|
|
591
|
+
);
|
|
592
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
593
|
+
compoundSignals: compoundSignalsMock,
|
|
594
|
+
hasDrifted: hasDriftedMock,
|
|
595
|
+
renderContextFile: renderContextFileMock,
|
|
596
|
+
};
|
|
597
|
+
rt.config.agentDir = '/tmp/test-agent';
|
|
598
|
+
ops = createOrchestrateOps(rt);
|
|
599
|
+
|
|
600
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
601
|
+
await op.handler({
|
|
602
|
+
sessionId: 'session-1',
|
|
603
|
+
outcome: 'completed',
|
|
604
|
+
operatorSignals: {
|
|
605
|
+
expertise: [{ topic: 'typescript', level: 'expert', confidence: 0.9 }],
|
|
606
|
+
corrections: [],
|
|
607
|
+
interests: [],
|
|
608
|
+
patterns: [],
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
expect(compoundSignalsMock).toHaveBeenCalled();
|
|
613
|
+
expect(hasDriftedMock).toHaveBeenCalled();
|
|
614
|
+
expect(renderContextFileMock).toHaveBeenCalled();
|
|
615
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join('/tmp/test-agent', 'instructions'), {
|
|
616
|
+
recursive: true,
|
|
617
|
+
});
|
|
618
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
619
|
+
path.join('/tmp/test-agent', 'instructions', 'operator-context.md'),
|
|
620
|
+
'# Operator Context\n\n**Expertise:** typescript (expert, 1 sessions, confidence 0.90).',
|
|
621
|
+
'utf-8',
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('orchestrate_complete skips file write when no agentDir', async () => {
|
|
626
|
+
const compoundSignalsMock = vi.fn();
|
|
627
|
+
const hasDriftedMock = vi.fn().mockReturnValue(true);
|
|
628
|
+
const renderContextFileMock = vi.fn().mockReturnValue('# Operator Context');
|
|
629
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
630
|
+
compoundSignals: compoundSignalsMock,
|
|
631
|
+
hasDrifted: hasDriftedMock,
|
|
632
|
+
renderContextFile: renderContextFileMock,
|
|
633
|
+
};
|
|
634
|
+
// agentDir is NOT set
|
|
635
|
+
delete (rt.config as Record<string, unknown>).agentDir;
|
|
636
|
+
ops = createOrchestrateOps(rt);
|
|
637
|
+
|
|
638
|
+
vi.mocked(fs.mkdirSync).mockClear();
|
|
639
|
+
vi.mocked(fs.writeFileSync).mockClear();
|
|
640
|
+
|
|
641
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
642
|
+
await op.handler({
|
|
643
|
+
sessionId: 'session-1',
|
|
644
|
+
outcome: 'completed',
|
|
645
|
+
operatorSignals: {
|
|
646
|
+
expertise: [{ topic: 'react', level: 'intermediate' }],
|
|
647
|
+
corrections: [],
|
|
648
|
+
interests: [],
|
|
649
|
+
patterns: [],
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
expect(compoundSignalsMock).toHaveBeenCalled();
|
|
654
|
+
expect(hasDriftedMock).toHaveBeenCalled();
|
|
655
|
+
// Should NOT write to disk since agentDir is missing
|
|
656
|
+
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
|
657
|
+
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('orchestrate_complete skips file write when no drift', async () => {
|
|
661
|
+
const compoundSignalsMock = vi.fn();
|
|
662
|
+
const hasDriftedMock = vi.fn().mockReturnValue(false);
|
|
663
|
+
const renderContextFileMock = vi.fn();
|
|
664
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
665
|
+
compoundSignals: compoundSignalsMock,
|
|
666
|
+
hasDrifted: hasDriftedMock,
|
|
667
|
+
renderContextFile: renderContextFileMock,
|
|
668
|
+
};
|
|
669
|
+
rt.config.agentDir = '/tmp/test-agent';
|
|
670
|
+
ops = createOrchestrateOps(rt);
|
|
671
|
+
|
|
672
|
+
vi.mocked(fs.mkdirSync).mockClear();
|
|
673
|
+
vi.mocked(fs.writeFileSync).mockClear();
|
|
674
|
+
|
|
675
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
676
|
+
await op.handler({
|
|
677
|
+
sessionId: 'session-1',
|
|
678
|
+
outcome: 'completed',
|
|
679
|
+
operatorSignals: {
|
|
680
|
+
expertise: [],
|
|
681
|
+
corrections: [],
|
|
682
|
+
interests: [],
|
|
683
|
+
patterns: [],
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
expect(compoundSignalsMock).toHaveBeenCalled();
|
|
688
|
+
expect(hasDriftedMock).toHaveBeenCalled();
|
|
689
|
+
// No drift means no file write
|
|
690
|
+
expect(renderContextFileMock).not.toHaveBeenCalled();
|
|
691
|
+
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
|
692
|
+
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
693
|
+
});
|
|
694
|
+
|
|
495
695
|
it('assessment result includes non-empty reasoning for complex tasks', () => {
|
|
496
696
|
const result = assessTaskComplexity({
|
|
497
697
|
prompt: 'add authentication across all API routes',
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - orchestrate_quick_capture: one-call knowledge capture without full planning
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
12
14
|
import { z } from 'zod';
|
|
13
15
|
import type { OpDefinition, FacadeConfig } from '../facades/types.js';
|
|
14
16
|
import type { AgentRuntime } from './types.js';
|
|
@@ -18,6 +20,7 @@ import { createDispatcher } from '../flows/dispatch-registry.js';
|
|
|
18
20
|
import { runEpilogue } from '../flows/epilogue.js';
|
|
19
21
|
import type { OrchestrationPlan, ExecutionResult } from '../flows/types.js';
|
|
20
22
|
import type { ContextHealthStatus } from './context-health.js';
|
|
23
|
+
import type { OperatorSignals } from '../operator/operator-context-types.js';
|
|
21
24
|
import {
|
|
22
25
|
detectGitHubContext,
|
|
23
26
|
findMatchingMilestone,
|
|
@@ -472,7 +475,10 @@ export function createOrchestrateOps(
|
|
|
472
475
|
'end brain session, and clean up.',
|
|
473
476
|
auth: 'write',
|
|
474
477
|
schema: z.object({
|
|
475
|
-
planId: z
|
|
478
|
+
planId: z
|
|
479
|
+
.string()
|
|
480
|
+
.optional()
|
|
481
|
+
.describe('ID of the executing plan to complete (optional for direct tasks)'),
|
|
476
482
|
sessionId: z.string().describe('ID of the brain session to end'),
|
|
477
483
|
outcome: z
|
|
478
484
|
.enum(['completed', 'abandoned', 'partial'])
|
|
@@ -495,6 +501,48 @@ export function createOrchestrateOps(
|
|
|
495
501
|
.optional()
|
|
496
502
|
.default(false)
|
|
497
503
|
.describe('Set true to bypass rationalization gate and impact warnings after review'),
|
|
504
|
+
operatorSignals: z
|
|
505
|
+
.object({
|
|
506
|
+
expertise: z
|
|
507
|
+
.array(
|
|
508
|
+
z.object({
|
|
509
|
+
topic: z.string(),
|
|
510
|
+
level: z.enum(['learning', 'intermediate', 'expert']),
|
|
511
|
+
evidence: z.string().optional(),
|
|
512
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
513
|
+
}),
|
|
514
|
+
)
|
|
515
|
+
.default([]),
|
|
516
|
+
corrections: z
|
|
517
|
+
.array(
|
|
518
|
+
z.object({
|
|
519
|
+
rule: z.string(),
|
|
520
|
+
quote: z.string().optional(),
|
|
521
|
+
scope: z.enum(['global', 'project']).default('global'),
|
|
522
|
+
}),
|
|
523
|
+
)
|
|
524
|
+
.default([]),
|
|
525
|
+
interests: z
|
|
526
|
+
.array(
|
|
527
|
+
z.object({
|
|
528
|
+
tag: z.string(),
|
|
529
|
+
context: z.string().optional(),
|
|
530
|
+
}),
|
|
531
|
+
)
|
|
532
|
+
.default([]),
|
|
533
|
+
patterns: z
|
|
534
|
+
.array(
|
|
535
|
+
z.object({
|
|
536
|
+
pattern: z.string(),
|
|
537
|
+
frequency: z.enum(['once', 'occasional', 'frequent']).optional(),
|
|
538
|
+
}),
|
|
539
|
+
)
|
|
540
|
+
.default([]),
|
|
541
|
+
})
|
|
542
|
+
.default({})
|
|
543
|
+
.describe(
|
|
544
|
+
'Your silent assessment of the operator this session. Fill what you observed, empty arrays for what you did not. Never announce this to the operator.',
|
|
545
|
+
),
|
|
498
546
|
}),
|
|
499
547
|
handler: async (params) => {
|
|
500
548
|
const planId = params.planId as string | undefined;
|
|
@@ -510,7 +558,12 @@ export function createOrchestrateOps(
|
|
|
510
558
|
|
|
511
559
|
// Anti-rationalization gate: only if we have acceptance criteria from a plan
|
|
512
560
|
const criteria = planObj && planId ? collectAcceptanceCriteria(planner, planId) : [];
|
|
513
|
-
if (
|
|
561
|
+
if (
|
|
562
|
+
outcome === 'completed' &&
|
|
563
|
+
criteria.length > 0 &&
|
|
564
|
+
completionSummary &&
|
|
565
|
+
!overrideRationalization
|
|
566
|
+
) {
|
|
514
567
|
const report = detectRationalizations(criteria, completionSummary);
|
|
515
568
|
if (report.detected) {
|
|
516
569
|
captureRationalizationAntiPattern(vault, report);
|
|
@@ -549,10 +602,22 @@ export function createOrchestrateOps(
|
|
|
549
602
|
}
|
|
550
603
|
}
|
|
551
604
|
|
|
552
|
-
// Complete the planner plan (legacy lifecycle) —
|
|
605
|
+
// Complete the planner plan (legacy lifecycle) — best-effort
|
|
606
|
+
// The epilogue (brain session, knowledge extraction, flow epilogue) MUST run
|
|
607
|
+
// even if plan transition fails (e.g. already completed, missing, invalid state).
|
|
608
|
+
const warnings: string[] = [];
|
|
553
609
|
let completedPlan;
|
|
554
610
|
if (planObj && planId) {
|
|
555
|
-
|
|
611
|
+
try {
|
|
612
|
+
completedPlan = planner.complete(planId);
|
|
613
|
+
} catch (err) {
|
|
614
|
+
warnings.push(`Plan transition skipped: ${(err as Error).message}`);
|
|
615
|
+
completedPlan = {
|
|
616
|
+
id: planId,
|
|
617
|
+
status: planObj.status ?? 'completed',
|
|
618
|
+
objective: planObj.objective ?? (completionSummary || 'Direct execution'),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
556
621
|
} else {
|
|
557
622
|
completedPlan = {
|
|
558
623
|
id: planId ?? `direct-${Date.now()}`,
|
|
@@ -602,12 +667,28 @@ export function createOrchestrateOps(
|
|
|
602
667
|
}
|
|
603
668
|
}
|
|
604
669
|
|
|
670
|
+
// Compound operator signals (silent learning)
|
|
671
|
+
const signals = params.operatorSignals as OperatorSignals | undefined;
|
|
672
|
+
if (signals && runtime.operatorContextStore) {
|
|
673
|
+
runtime.operatorContextStore.compoundSignals(signals, sessionId);
|
|
674
|
+
|
|
675
|
+
// Re-render operator context file if profile drifted
|
|
676
|
+
const agentDir = runtime.config.agentDir;
|
|
677
|
+
if (runtime.operatorContextStore.hasDrifted() && agentDir) {
|
|
678
|
+
const content = runtime.operatorContextStore.renderContextFile();
|
|
679
|
+
const contextPath = path.join(agentDir, 'instructions', 'operator-context.md');
|
|
680
|
+
fs.mkdirSync(path.dirname(contextPath), { recursive: true });
|
|
681
|
+
fs.writeFileSync(contextPath, content, 'utf-8');
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
605
685
|
return {
|
|
606
686
|
plan: completedPlan,
|
|
607
687
|
session,
|
|
608
688
|
extraction,
|
|
609
689
|
epilogue: epilogueResult,
|
|
610
690
|
...(impactReport ? { impactAnalysis: impactReport } : {}),
|
|
691
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
611
692
|
};
|
|
612
693
|
},
|
|
613
694
|
},
|