@soleri/core 9.3.0 → 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.
Files changed (177) hide show
  1. package/dist/brain/intelligence.d.ts +5 -0
  2. package/dist/brain/intelligence.d.ts.map +1 -1
  3. package/dist/brain/intelligence.js +115 -26
  4. package/dist/brain/intelligence.js.map +1 -1
  5. package/dist/brain/learning-radar.d.ts +3 -3
  6. package/dist/brain/learning-radar.d.ts.map +1 -1
  7. package/dist/brain/learning-radar.js +8 -4
  8. package/dist/brain/learning-radar.js.map +1 -1
  9. package/dist/control/intent-router.d.ts +2 -2
  10. package/dist/control/intent-router.d.ts.map +1 -1
  11. package/dist/control/intent-router.js +35 -1
  12. package/dist/control/intent-router.js.map +1 -1
  13. package/dist/control/types.d.ts +10 -2
  14. package/dist/control/types.d.ts.map +1 -1
  15. package/dist/curator/curator.d.ts +4 -0
  16. package/dist/curator/curator.d.ts.map +1 -1
  17. package/dist/curator/curator.js +23 -1
  18. package/dist/curator/curator.js.map +1 -1
  19. package/dist/curator/schema.d.ts +1 -1
  20. package/dist/curator/schema.d.ts.map +1 -1
  21. package/dist/curator/schema.js +8 -0
  22. package/dist/curator/schema.js.map +1 -1
  23. package/dist/domain-packs/types.d.ts +6 -0
  24. package/dist/domain-packs/types.d.ts.map +1 -1
  25. package/dist/domain-packs/types.js +1 -0
  26. package/dist/domain-packs/types.js.map +1 -1
  27. package/dist/engine/module-manifest.d.ts +2 -0
  28. package/dist/engine/module-manifest.d.ts.map +1 -1
  29. package/dist/engine/module-manifest.js +117 -2
  30. package/dist/engine/module-manifest.js.map +1 -1
  31. package/dist/engine/register-engine.d.ts +9 -0
  32. package/dist/engine/register-engine.d.ts.map +1 -1
  33. package/dist/engine/register-engine.js +59 -1
  34. package/dist/engine/register-engine.js.map +1 -1
  35. package/dist/facades/types.d.ts +5 -1
  36. package/dist/facades/types.d.ts.map +1 -1
  37. package/dist/facades/types.js.map +1 -1
  38. package/dist/index.d.ts +6 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +5 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/operator/operator-context-store.d.ts +54 -0
  43. package/dist/operator/operator-context-store.d.ts.map +1 -0
  44. package/dist/operator/operator-context-store.js +434 -0
  45. package/dist/operator/operator-context-store.js.map +1 -0
  46. package/dist/operator/operator-context-types.d.ts +101 -0
  47. package/dist/operator/operator-context-types.d.ts.map +1 -0
  48. package/dist/operator/operator-context-types.js +27 -0
  49. package/dist/operator/operator-context-types.js.map +1 -0
  50. package/dist/packs/index.d.ts +2 -2
  51. package/dist/packs/index.d.ts.map +1 -1
  52. package/dist/packs/index.js +1 -1
  53. package/dist/packs/index.js.map +1 -1
  54. package/dist/packs/lockfile.d.ts +3 -0
  55. package/dist/packs/lockfile.d.ts.map +1 -1
  56. package/dist/packs/lockfile.js.map +1 -1
  57. package/dist/packs/types.d.ts +8 -2
  58. package/dist/packs/types.d.ts.map +1 -1
  59. package/dist/packs/types.js +6 -0
  60. package/dist/packs/types.js.map +1 -1
  61. package/dist/planning/plan-lifecycle.d.ts +12 -1
  62. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  63. package/dist/planning/plan-lifecycle.js +52 -19
  64. package/dist/planning/plan-lifecycle.js.map +1 -1
  65. package/dist/planning/planner-types.d.ts +6 -0
  66. package/dist/planning/planner-types.d.ts.map +1 -1
  67. package/dist/planning/planner.d.ts +21 -1
  68. package/dist/planning/planner.d.ts.map +1 -1
  69. package/dist/planning/planner.js +62 -3
  70. package/dist/planning/planner.js.map +1 -1
  71. package/dist/planning/task-complexity-assessor.d.ts +42 -0
  72. package/dist/planning/task-complexity-assessor.d.ts.map +1 -0
  73. package/dist/planning/task-complexity-assessor.js +132 -0
  74. package/dist/planning/task-complexity-assessor.js.map +1 -0
  75. package/dist/plugins/types.d.ts +18 -18
  76. package/dist/runtime/admin-ops.d.ts +1 -1
  77. package/dist/runtime/admin-ops.d.ts.map +1 -1
  78. package/dist/runtime/admin-ops.js +118 -3
  79. package/dist/runtime/admin-ops.js.map +1 -1
  80. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  81. package/dist/runtime/admin-setup-ops.js +19 -9
  82. package/dist/runtime/admin-setup-ops.js.map +1 -1
  83. package/dist/runtime/capture-ops.d.ts.map +1 -1
  84. package/dist/runtime/capture-ops.js +35 -7
  85. package/dist/runtime/capture-ops.js.map +1 -1
  86. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  87. package/dist/runtime/facades/brain-facade.js +4 -2
  88. package/dist/runtime/facades/brain-facade.js.map +1 -1
  89. package/dist/runtime/facades/control-facade.d.ts.map +1 -1
  90. package/dist/runtime/facades/control-facade.js +8 -2
  91. package/dist/runtime/facades/control-facade.js.map +1 -1
  92. package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
  93. package/dist/runtime/facades/curator-facade.js +13 -0
  94. package/dist/runtime/facades/curator-facade.js.map +1 -1
  95. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  96. package/dist/runtime/facades/memory-facade.js +10 -12
  97. package/dist/runtime/facades/memory-facade.js.map +1 -1
  98. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  99. package/dist/runtime/facades/orchestrate-facade.js +36 -1
  100. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  101. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  102. package/dist/runtime/facades/plan-facade.js +20 -4
  103. package/dist/runtime/facades/plan-facade.js.map +1 -1
  104. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  105. package/dist/runtime/orchestrate-ops.js +109 -31
  106. package/dist/runtime/orchestrate-ops.js.map +1 -1
  107. package/dist/runtime/plan-feedback-helper.d.ts +21 -0
  108. package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
  109. package/dist/runtime/plan-feedback-helper.js +52 -0
  110. package/dist/runtime/plan-feedback-helper.js.map +1 -0
  111. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  112. package/dist/runtime/planning-extra-ops.js +73 -34
  113. package/dist/runtime/planning-extra-ops.js.map +1 -1
  114. package/dist/runtime/session-briefing.d.ts.map +1 -1
  115. package/dist/runtime/session-briefing.js +9 -1
  116. package/dist/runtime/session-briefing.js.map +1 -1
  117. package/dist/runtime/types.d.ts +3 -0
  118. package/dist/runtime/types.d.ts.map +1 -1
  119. package/dist/skills/sync-skills.d.ts.map +1 -1
  120. package/dist/skills/sync-skills.js +13 -7
  121. package/dist/skills/sync-skills.js.map +1 -1
  122. package/package.json +1 -1
  123. package/src/brain/brain-intelligence.test.ts +30 -0
  124. package/src/brain/extraction-quality.test.ts +323 -0
  125. package/src/brain/intelligence.ts +133 -30
  126. package/src/brain/learning-radar.ts +8 -5
  127. package/src/brain/second-brain-features.test.ts +1 -1
  128. package/src/control/intent-router.test.ts +73 -3
  129. package/src/control/intent-router.ts +38 -1
  130. package/src/control/types.ts +13 -2
  131. package/src/curator/curator.test.ts +92 -0
  132. package/src/curator/curator.ts +29 -1
  133. package/src/curator/schema.ts +8 -0
  134. package/src/domain-packs/types.ts +8 -0
  135. package/src/engine/module-manifest.test.ts +51 -2
  136. package/src/engine/module-manifest.ts +119 -2
  137. package/src/engine/register-engine.test.ts +73 -1
  138. package/src/engine/register-engine.ts +61 -1
  139. package/src/facades/types.ts +5 -0
  140. package/src/index.ts +30 -0
  141. package/src/operator/operator-context-store.test.ts +698 -0
  142. package/src/operator/operator-context-store.ts +569 -0
  143. package/src/operator/operator-context-types.ts +139 -0
  144. package/src/packs/index.ts +3 -1
  145. package/src/packs/lockfile.ts +3 -0
  146. package/src/packs/types.ts +9 -0
  147. package/src/planning/plan-lifecycle.ts +80 -22
  148. package/src/planning/planner-types.ts +6 -0
  149. package/src/planning/planner.ts +74 -4
  150. package/src/planning/task-complexity-assessor.test.ts +302 -0
  151. package/src/planning/task-complexity-assessor.ts +180 -0
  152. package/src/runtime/admin-ops.test.ts +159 -3
  153. package/src/runtime/admin-ops.ts +123 -3
  154. package/src/runtime/admin-setup-ops.ts +30 -10
  155. package/src/runtime/capture-ops.test.ts +84 -0
  156. package/src/runtime/capture-ops.ts +35 -7
  157. package/src/runtime/facades/admin-facade.test.ts +1 -1
  158. package/src/runtime/facades/brain-facade.ts +6 -3
  159. package/src/runtime/facades/control-facade.ts +10 -2
  160. package/src/runtime/facades/curator-facade.ts +18 -0
  161. package/src/runtime/facades/memory-facade.test.ts +14 -12
  162. package/src/runtime/facades/memory-facade.ts +10 -12
  163. package/src/runtime/facades/orchestrate-facade.ts +33 -1
  164. package/src/runtime/facades/plan-facade.test.ts +213 -0
  165. package/src/runtime/facades/plan-facade.ts +23 -4
  166. package/src/runtime/orchestrate-ops.test.ts +404 -0
  167. package/src/runtime/orchestrate-ops.ts +129 -37
  168. package/src/runtime/plan-feedback-helper.test.ts +173 -0
  169. package/src/runtime/plan-feedback-helper.ts +63 -0
  170. package/src/runtime/planning-extra-ops.test.ts +43 -1
  171. package/src/runtime/planning-extra-ops.ts +96 -33
  172. package/src/runtime/session-briefing.test.ts +1 -0
  173. package/src/runtime/session-briefing.ts +10 -1
  174. package/src/runtime/types.ts +3 -0
  175. package/src/skills/sync-skills.ts +14 -7
  176. package/src/vault/vault-scaling.test.ts +5 -5
  177. package/vitest.config.ts +1 -0
@@ -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: params.objective as string,
68
+ objective,
50
69
  scope: params.scope as string,
51
- decisions: (params.decisions as string[]) ?? [],
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,7 +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';
5
+ import { assessTaskComplexity } from '../planning/task-complexity-assessor.js';
3
6
  import type { AgentRuntime } from './types.js';
4
7
 
8
+ vi.mock('node:fs', () => ({
9
+ default: {
10
+ mkdirSync: vi.fn(),
11
+ writeFileSync: vi.fn(),
12
+ },
13
+ }));
14
+
5
15
  // ---------------------------------------------------------------------------
6
16
  // Mocks for external modules
7
17
  // ---------------------------------------------------------------------------
@@ -241,6 +251,73 @@ describe('createOrchestrateOps', () => {
241
251
  await op.handler({ planId: 'plan-1', sessionId: 'session-1' });
242
252
  expect(rt.brainIntelligence.extractKnowledge).toHaveBeenCalledWith('session-1');
243
253
  });
254
+
255
+ it('works without a preceding plan', async () => {
256
+ const op = findOp(ops, 'orchestrate_complete');
257
+ const result = (await op.handler({
258
+ sessionId: 'session-1',
259
+ outcome: 'completed',
260
+ summary: 'Fixed a typo in the README',
261
+ })) as Record<string, unknown>;
262
+
263
+ // Should not call planner.complete
264
+ expect(rt.planner.complete).not.toHaveBeenCalled();
265
+
266
+ // Should return a lightweight completion record
267
+ const plan = result.plan as Record<string, unknown>;
268
+ expect(plan.status).toBe('completed');
269
+ expect(plan.objective).toBe('Fixed a typo in the README');
270
+ });
271
+
272
+ it('captures knowledge even without plan', async () => {
273
+ const op = findOp(ops, 'orchestrate_complete');
274
+ await op.handler({
275
+ sessionId: 'session-1',
276
+ summary: 'Refactored utility function',
277
+ });
278
+
279
+ // Brain session end and knowledge extraction still run
280
+ expect(rt.brainIntelligence.lifecycle).toHaveBeenCalledWith(
281
+ expect.objectContaining({ action: 'end', sessionId: 'session-1' }),
282
+ );
283
+ expect(rt.brainIntelligence.extractKnowledge).toHaveBeenCalledWith('session-1');
284
+ });
285
+
286
+ it('skips anti-rationalization gate when no criteria', async () => {
287
+ const { detectRationalizations } = await import('../planning/rationalization-detector.js');
288
+ const op = findOp(ops, 'orchestrate_complete');
289
+
290
+ await op.handler({
291
+ sessionId: 'session-1',
292
+ outcome: 'completed',
293
+ summary: 'This was basically done already',
294
+ });
295
+
296
+ // detectRationalizations should never be called since there are no criteria
297
+ expect(detectRationalizations).not.toHaveBeenCalled();
298
+ // Should still complete successfully
299
+ expect(rt.brainIntelligence.lifecycle).toHaveBeenCalled();
300
+ });
301
+
302
+ it('still runs brain session end without plan', async () => {
303
+ const op = findOp(ops, 'orchestrate_complete');
304
+ const result = (await op.handler({
305
+ sessionId: 'session-1',
306
+ outcome: 'completed',
307
+ toolsUsed: ['grep', 'edit'],
308
+ filesModified: [],
309
+ })) as Record<string, unknown>;
310
+
311
+ expect(rt.brainIntelligence.lifecycle).toHaveBeenCalledWith(
312
+ expect.objectContaining({
313
+ action: 'end',
314
+ sessionId: 'session-1',
315
+ planOutcome: 'completed',
316
+ toolsUsed: ['grep', 'edit'],
317
+ }),
318
+ );
319
+ expect(result.session).toBeDefined();
320
+ });
244
321
  });
245
322
 
246
323
  // ─── orchestrate_status ───────────────────────────────────────
@@ -299,4 +376,331 @@ describe('createOrchestrateOps', () => {
299
376
  await expect(op.handler({ planId: 'missing' })).rejects.toThrow('not found');
300
377
  });
301
378
  });
379
+
380
+ // ─── task auto-assessment routing ────────────────────────────
381
+ //
382
+ // Integration-style tests that verify the full assess → route → complete flow:
383
+ // 1. Use TaskComplexityAssessor to classify the task
384
+ // 2. Route to direct execution (simple) or planning (complex)
385
+ // 3. Complete via orchestrate_complete in both paths
386
+
387
+ describe('task auto-assessment routing', () => {
388
+ it('simple task routes to direct execution + complete', async () => {
389
+ // Step 1: Assess — "fix typo in README" should be simple
390
+ const assessment = assessTaskComplexity({ prompt: 'fix typo in README' });
391
+ expect(assessment.classification).toBe('simple');
392
+
393
+ // Step 2: Skip planning, go straight to complete without a planId
394
+ const completeOp = findOp(ops, 'orchestrate_complete');
395
+ const result = (await completeOp.handler({
396
+ sessionId: 'session-simple',
397
+ outcome: 'completed',
398
+ summary: 'Fixed typo in README',
399
+ })) as Record<string, unknown>;
400
+
401
+ // Should not touch the planner at all
402
+ expect(rt.planner.complete).not.toHaveBeenCalled();
403
+
404
+ // Should still produce a valid completion record
405
+ const plan = result.plan as Record<string, unknown>;
406
+ expect(plan.status).toBe('completed');
407
+ expect(plan.objective).toBe('Fixed typo in README');
408
+
409
+ // Knowledge should still be captured
410
+ expect(rt.brainIntelligence.extractKnowledge).toHaveBeenCalledWith('session-simple');
411
+ });
412
+
413
+ it('complex task routes through planning + complete', async () => {
414
+ // Step 1: Assess — cross-cutting auth task should be complex
415
+ const assessment = assessTaskComplexity({
416
+ prompt: 'add authentication across all API routes',
417
+ filesEstimated: 8,
418
+ });
419
+ expect(assessment.classification).toBe('complex');
420
+
421
+ // Step 2: Create a plan via orchestrate_plan
422
+ const planOp = findOp(ops, 'orchestrate_plan');
423
+ const planResult = (await planOp.handler({
424
+ prompt: 'add authentication across all API routes',
425
+ })) as Record<string, unknown>;
426
+ expect(planResult).toHaveProperty('plan');
427
+ expect(planResult).toHaveProperty('flow');
428
+
429
+ // Step 3: Complete with the planId
430
+ const completeOp = findOp(ops, 'orchestrate_complete');
431
+ const result = (await completeOp.handler({
432
+ planId: 'plan-1',
433
+ sessionId: 'session-complex',
434
+ outcome: 'completed',
435
+ summary: 'Added authentication middleware to all API routes',
436
+ })) as Record<string, unknown>;
437
+
438
+ // Should complete via the planner lifecycle
439
+ expect(rt.planner.complete).toHaveBeenCalledWith('plan-1');
440
+
441
+ // Knowledge should be captured
442
+ expect(rt.brainIntelligence.lifecycle).toHaveBeenCalledWith(
443
+ expect.objectContaining({ action: 'end', sessionId: 'session-complex' }),
444
+ );
445
+ expect(rt.brainIntelligence.extractKnowledge).toHaveBeenCalledWith('session-complex');
446
+
447
+ // Plan should be marked completed
448
+ const completedPlan = result.plan as Record<string, unknown>;
449
+ expect(completedPlan.status).toBe('completed');
450
+ });
451
+
452
+ it('orchestrate_complete captures knowledge in both paths', async () => {
453
+ // ── Simple path (no planId) ──
454
+ vi.clearAllMocks();
455
+ rt = mockRuntime();
456
+ ops = createOrchestrateOps(rt);
457
+
458
+ await findOp(ops, 'orchestrate_complete').handler({
459
+ sessionId: 'session-simple',
460
+ outcome: 'completed',
461
+ summary: 'Renamed a variable',
462
+ });
463
+
464
+ // Brain session end called
465
+ expect(rt.brainIntelligence.lifecycle).toHaveBeenCalledWith(
466
+ expect.objectContaining({ action: 'end', sessionId: 'session-simple' }),
467
+ );
468
+ // Knowledge extraction called
469
+ expect(rt.brainIntelligence.extractKnowledge).toHaveBeenCalledWith('session-simple');
470
+ // Planner.complete NOT called (no plan)
471
+ expect(rt.planner.complete).not.toHaveBeenCalled();
472
+
473
+ // ── Complex path (with planId) ──
474
+ vi.clearAllMocks();
475
+ rt = mockRuntime();
476
+ ops = createOrchestrateOps(rt);
477
+
478
+ await findOp(ops, 'orchestrate_complete').handler({
479
+ planId: 'plan-1',
480
+ sessionId: 'session-complex',
481
+ outcome: 'completed',
482
+ summary: 'Implemented full auth layer',
483
+ });
484
+
485
+ // Brain session end called
486
+ expect(rt.brainIntelligence.lifecycle).toHaveBeenCalledWith(
487
+ expect.objectContaining({ action: 'end', sessionId: 'session-complex' }),
488
+ );
489
+ // Knowledge extraction called
490
+ expect(rt.brainIntelligence.extractKnowledge).toHaveBeenCalledWith('session-complex');
491
+ // Planner.complete IS called (has plan)
492
+ expect(rt.planner.complete).toHaveBeenCalledWith('plan-1');
493
+ });
494
+
495
+ it('assessment result includes non-empty reasoning for simple tasks', () => {
496
+ const result = assessTaskComplexity({ prompt: 'fix typo in README' });
497
+ expect(result.classification).toBe('simple');
498
+ expect(typeof result.reasoning).toBe('string');
499
+ expect(result.reasoning.length).toBeGreaterThan(0);
500
+ });
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
+
695
+ it('assessment result includes non-empty reasoning for complex tasks', () => {
696
+ const result = assessTaskComplexity({
697
+ prompt: 'add authentication across all API routes',
698
+ filesEstimated: 8,
699
+ domains: ['auth', 'api', 'middleware'],
700
+ });
701
+ expect(result.classification).toBe('complex');
702
+ expect(typeof result.reasoning).toBe('string');
703
+ expect(result.reasoning.length).toBeGreaterThan(0);
704
+ });
705
+ });
302
706
  });