@soleri/core 9.3.1 → 9.5.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 (205) 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.js +3 -3
  28. package/dist/engine/module-manifest.js.map +1 -1
  29. package/dist/engine/register-engine.d.ts +9 -0
  30. package/dist/engine/register-engine.d.ts.map +1 -1
  31. package/dist/engine/register-engine.js +59 -1
  32. package/dist/engine/register-engine.js.map +1 -1
  33. package/dist/facades/types.d.ts +5 -1
  34. package/dist/facades/types.d.ts.map +1 -1
  35. package/dist/facades/types.js.map +1 -1
  36. package/dist/hooks/candidate-scorer.d.ts +28 -0
  37. package/dist/hooks/candidate-scorer.d.ts.map +1 -0
  38. package/dist/hooks/candidate-scorer.js +20 -0
  39. package/dist/hooks/candidate-scorer.js.map +1 -0
  40. package/dist/hooks/index.d.ts +2 -0
  41. package/dist/hooks/index.d.ts.map +1 -0
  42. package/dist/hooks/index.js +2 -0
  43. package/dist/hooks/index.js.map +1 -0
  44. package/dist/index.d.ts +4 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +3 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/operator/operator-context-store.d.ts +54 -0
  49. package/dist/operator/operator-context-store.d.ts.map +1 -0
  50. package/dist/operator/operator-context-store.js +434 -0
  51. package/dist/operator/operator-context-store.js.map +1 -0
  52. package/dist/operator/operator-context-types.d.ts +101 -0
  53. package/dist/operator/operator-context-types.d.ts.map +1 -0
  54. package/dist/operator/operator-context-types.js +27 -0
  55. package/dist/operator/operator-context-types.js.map +1 -0
  56. package/dist/packs/index.d.ts +2 -2
  57. package/dist/packs/index.d.ts.map +1 -1
  58. package/dist/packs/index.js +1 -1
  59. package/dist/packs/index.js.map +1 -1
  60. package/dist/packs/lockfile.d.ts +3 -0
  61. package/dist/packs/lockfile.d.ts.map +1 -1
  62. package/dist/packs/lockfile.js.map +1 -1
  63. package/dist/packs/types.d.ts +8 -2
  64. package/dist/packs/types.d.ts.map +1 -1
  65. package/dist/packs/types.js +6 -0
  66. package/dist/packs/types.js.map +1 -1
  67. package/dist/planning/plan-lifecycle.d.ts +12 -1
  68. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  69. package/dist/planning/plan-lifecycle.js +54 -16
  70. package/dist/planning/plan-lifecycle.js.map +1 -1
  71. package/dist/planning/planner-types.d.ts +6 -0
  72. package/dist/planning/planner-types.d.ts.map +1 -1
  73. package/dist/planning/planner.d.ts +21 -1
  74. package/dist/planning/planner.d.ts.map +1 -1
  75. package/dist/planning/planner.js +62 -3
  76. package/dist/planning/planner.js.map +1 -1
  77. package/dist/planning/task-complexity-assessor.d.ts.map +1 -1
  78. package/dist/planning/task-complexity-assessor.js.map +1 -1
  79. package/dist/plugins/types.d.ts +18 -18
  80. package/dist/runtime/admin-ops.d.ts +1 -1
  81. package/dist/runtime/admin-ops.d.ts.map +1 -1
  82. package/dist/runtime/admin-ops.js +100 -3
  83. package/dist/runtime/admin-ops.js.map +1 -1
  84. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  85. package/dist/runtime/admin-setup-ops.js +19 -9
  86. package/dist/runtime/admin-setup-ops.js.map +1 -1
  87. package/dist/runtime/capture-ops.d.ts.map +1 -1
  88. package/dist/runtime/capture-ops.js +35 -7
  89. package/dist/runtime/capture-ops.js.map +1 -1
  90. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  91. package/dist/runtime/facades/brain-facade.js +4 -2
  92. package/dist/runtime/facades/brain-facade.js.map +1 -1
  93. package/dist/runtime/facades/control-facade.d.ts.map +1 -1
  94. package/dist/runtime/facades/control-facade.js +8 -2
  95. package/dist/runtime/facades/control-facade.js.map +1 -1
  96. package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
  97. package/dist/runtime/facades/curator-facade.js +13 -0
  98. package/dist/runtime/facades/curator-facade.js.map +1 -1
  99. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  100. package/dist/runtime/facades/memory-facade.js +10 -12
  101. package/dist/runtime/facades/memory-facade.js.map +1 -1
  102. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  103. package/dist/runtime/facades/orchestrate-facade.js +36 -1
  104. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  105. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  106. package/dist/runtime/facades/plan-facade.js +20 -4
  107. package/dist/runtime/facades/plan-facade.js.map +1 -1
  108. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  109. package/dist/runtime/orchestrate-ops.js +71 -4
  110. package/dist/runtime/orchestrate-ops.js.map +1 -1
  111. package/dist/runtime/plan-feedback-helper.d.ts +21 -0
  112. package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
  113. package/dist/runtime/plan-feedback-helper.js +52 -0
  114. package/dist/runtime/plan-feedback-helper.js.map +1 -0
  115. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  116. package/dist/runtime/planning-extra-ops.js +73 -34
  117. package/dist/runtime/planning-extra-ops.js.map +1 -1
  118. package/dist/runtime/session-briefing.d.ts.map +1 -1
  119. package/dist/runtime/session-briefing.js +9 -1
  120. package/dist/runtime/session-briefing.js.map +1 -1
  121. package/dist/runtime/types.d.ts +3 -0
  122. package/dist/runtime/types.d.ts.map +1 -1
  123. package/dist/skills/sync-skills.d.ts.map +1 -1
  124. package/dist/skills/sync-skills.js +13 -7
  125. package/dist/skills/sync-skills.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/brain/brain-intelligence.test.ts +30 -0
  128. package/src/brain/brain.ts +120 -46
  129. package/src/brain/extraction-quality.test.ts +323 -0
  130. package/src/brain/intelligence.ts +175 -64
  131. package/src/brain/learning-radar.ts +8 -5
  132. package/src/brain/second-brain-features.test.ts +1 -1
  133. package/src/chat/agent-loop.ts +1 -1
  134. package/src/chat/notifications.ts +4 -0
  135. package/src/control/intent-router.test.ts +73 -3
  136. package/src/control/intent-router.ts +48 -9
  137. package/src/control/types.ts +13 -2
  138. package/src/curator/curator.test.ts +92 -0
  139. package/src/curator/curator.ts +162 -18
  140. package/src/curator/schema.ts +8 -0
  141. package/src/domain-packs/types.ts +8 -0
  142. package/src/engine/module-manifest.test.ts +8 -2
  143. package/src/engine/module-manifest.ts +3 -3
  144. package/src/engine/register-engine.test.ts +73 -1
  145. package/src/engine/register-engine.ts +61 -1
  146. package/src/facades/types.ts +5 -0
  147. package/src/hooks/candidate-scorer.test.ts +76 -0
  148. package/src/hooks/candidate-scorer.ts +39 -0
  149. package/src/hooks/index.ts +6 -0
  150. package/src/index.ts +24 -0
  151. package/src/llm/llm-client.ts +1 -0
  152. package/src/operator/operator-context-store.test.ts +698 -0
  153. package/src/operator/operator-context-store.ts +569 -0
  154. package/src/operator/operator-context-types.ts +139 -0
  155. package/src/packs/index.ts +3 -1
  156. package/src/packs/lockfile.ts +3 -0
  157. package/src/packs/types.ts +9 -0
  158. package/src/persistence/sqlite-provider.ts +1 -0
  159. package/src/planning/github-projection.ts +48 -44
  160. package/src/planning/plan-lifecycle.ts +93 -22
  161. package/src/planning/planner-types.ts +6 -0
  162. package/src/planning/planner.ts +74 -4
  163. package/src/planning/task-complexity-assessor.test.ts +6 -2
  164. package/src/planning/task-complexity-assessor.ts +1 -4
  165. package/src/queue/pipeline-runner.ts +4 -0
  166. package/src/runtime/admin-ops.test.ts +139 -6
  167. package/src/runtime/admin-ops.ts +104 -3
  168. package/src/runtime/admin-setup-ops.ts +30 -10
  169. package/src/runtime/capture-ops.test.ts +84 -0
  170. package/src/runtime/capture-ops.ts +35 -7
  171. package/src/runtime/curator-extra-ops.test.ts +7 -0
  172. package/src/runtime/curator-extra-ops.ts +10 -1
  173. package/src/runtime/facades/admin-facade.test.ts +1 -1
  174. package/src/runtime/facades/brain-facade.ts +6 -3
  175. package/src/runtime/facades/control-facade.ts +10 -2
  176. package/src/runtime/facades/curator-facade.test.ts +7 -0
  177. package/src/runtime/facades/curator-facade.ts +18 -0
  178. package/src/runtime/facades/memory-facade.test.ts +14 -12
  179. package/src/runtime/facades/memory-facade.ts +197 -12
  180. package/src/runtime/facades/orchestrate-facade.ts +33 -1
  181. package/src/runtime/facades/plan-facade.test.ts +213 -0
  182. package/src/runtime/facades/plan-facade.ts +23 -4
  183. package/src/runtime/orchestrate-ops.test.ts +202 -2
  184. package/src/runtime/orchestrate-ops.ts +88 -7
  185. package/src/runtime/plan-feedback-helper.test.ts +173 -0
  186. package/src/runtime/plan-feedback-helper.ts +63 -0
  187. package/src/runtime/planning-extra-ops.test.ts +43 -1
  188. package/src/runtime/planning-extra-ops.ts +96 -33
  189. package/src/runtime/runtime.test.ts +50 -2
  190. package/src/runtime/runtime.ts +117 -89
  191. package/src/runtime/session-briefing.test.ts +1 -0
  192. package/src/runtime/session-briefing.ts +10 -1
  193. package/src/runtime/shutdown-registry.test.ts +151 -0
  194. package/src/runtime/shutdown-registry.ts +85 -0
  195. package/src/runtime/types.ts +7 -1
  196. package/src/skills/sync-skills.ts +14 -7
  197. package/src/transport/http-server.ts +50 -3
  198. package/src/transport/ws-server.ts +8 -0
  199. package/src/vault/linking.test.ts +12 -0
  200. package/src/vault/linking.ts +90 -44
  201. package/src/vault/vault-maintenance.ts +11 -18
  202. package/src/vault/vault-memories.ts +21 -13
  203. package/src/vault/vault-schema.ts +21 -0
  204. package/src/vault/vault.ts +8 -3
  205. package/vitest.config.ts +1 -0
@@ -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.string().optional().describe('ID of the executing plan to complete (optional for direct tasks)'),
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 (outcome === 'completed' && criteria.length > 0 && completionSummary && !overrideRationalization) {
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) — only if plan exists
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
- completedPlan = planner.complete(planId);
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
  },
@@ -766,7 +847,7 @@ export function createOrchestrateOps(
766
847
  }
767
848
 
768
849
  // 2. Detect GitHub context
769
- const ctx = detectGitHubContext(projectPath);
850
+ const ctx = await detectGitHubContext(projectPath);
770
851
  if (!ctx) {
771
852
  return {
772
853
  status: 'skipped',
@@ -804,7 +885,7 @@ export function createOrchestrateOps(
804
885
  };
805
886
  }
806
887
 
807
- const updated = updateGitHubIssueBody(ctx.repo, linkToIssue, body);
888
+ const updated = await updateGitHubIssueBody(ctx.repo, linkToIssue, body);
808
889
  if (!updated) {
809
890
  return {
810
891
  status: 'error',
@@ -868,7 +949,7 @@ export function createOrchestrateOps(
868
949
  continue;
869
950
  }
870
951
 
871
- const issueNumber = createGitHubIssue(ctx.repo, task.title, body, {
952
+ const issueNumber = await createGitHubIssue(ctx.repo, task.title, body, {
872
953
  milestone: milestoneNumber,
873
954
  labels: labels.length > 0 ? labels : undefined,
874
955
  });
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { recordPlanFeedback } from './plan-feedback-helper.js';
3
+
4
+ // ─── Mock Factories ───────────────────────────────────────────────────
5
+
6
+ function makeBrain() {
7
+ return {
8
+ recordFeedback: vi.fn(),
9
+ };
10
+ }
11
+
12
+ function makeBrainIntelligence() {
13
+ return {
14
+ maybeAutoBuildOnFeedback: vi.fn(),
15
+ };
16
+ }
17
+
18
+ function makePlan(decisions: (string | { decision: string })[] = []) {
19
+ return {
20
+ objective: 'Test objective',
21
+ decisions,
22
+ };
23
+ }
24
+
25
+ // ─── Tests ────────────────────────────────────────────────────────────
26
+
27
+ describe('recordPlanFeedback', () => {
28
+ it('should extract entryIds from decision strings and record feedback', () => {
29
+ const brain = makeBrain();
30
+ const intelligence = makeBrainIntelligence();
31
+ const plan = makePlan([
32
+ 'Brain pattern: TDD (strength: 52.5) [entryId:method-tdd-123]',
33
+ 'Brain pattern: Vault hooks (strength: 87.5) [entryId:arch-vault-456]',
34
+ ]);
35
+
36
+ const count = recordPlanFeedback(plan, brain as unknown, intelligence as unknown);
37
+
38
+ expect(count).toBe(2);
39
+ expect(brain.recordFeedback).toHaveBeenCalledTimes(2);
40
+ expect(brain.recordFeedback).toHaveBeenCalledWith(
41
+ 'Test objective',
42
+ 'method-tdd-123',
43
+ 'accepted',
44
+ );
45
+ expect(brain.recordFeedback).toHaveBeenCalledWith(
46
+ 'Test objective',
47
+ 'arch-vault-456',
48
+ 'accepted',
49
+ );
50
+ expect(intelligence.maybeAutoBuildOnFeedback).toHaveBeenCalledOnce();
51
+ });
52
+
53
+ it('should handle decision objects with .decision property', () => {
54
+ const brain = makeBrain();
55
+ const plan = makePlan([{ decision: 'Use vault pattern [entryId:obj-entry-1]' }]);
56
+
57
+ const count = recordPlanFeedback(plan, brain as unknown);
58
+
59
+ expect(count).toBe(1);
60
+ expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'obj-entry-1', 'accepted');
61
+ });
62
+
63
+ it('should skip decisions without entryId markers', () => {
64
+ const brain = makeBrain();
65
+ const plan = makePlan([
66
+ 'Brain pattern: TDD (strength: 52.5)',
67
+ 'Some decision without an entry ID',
68
+ 'Brain pattern: Vault hooks (strength: 87.5) [entryId:arch-vault-456]',
69
+ ]);
70
+
71
+ const count = recordPlanFeedback(plan, brain as unknown);
72
+
73
+ expect(count).toBe(1);
74
+ expect(brain.recordFeedback).toHaveBeenCalledTimes(1);
75
+ expect(brain.recordFeedback).toHaveBeenCalledWith(
76
+ 'Test objective',
77
+ 'arch-vault-456',
78
+ 'accepted',
79
+ );
80
+ });
81
+
82
+ it('should skip malformed entryId markers gracefully', () => {
83
+ const brain = makeBrain();
84
+ const plan = makePlan(['Brain pattern: X [entryId:]', 'Brain pattern: Y [entryId:valid-id]']);
85
+
86
+ const count = recordPlanFeedback(plan, brain as unknown);
87
+
88
+ // [entryId:] won't match because the regex requires at least one char after :
89
+ // Actually the regex [^\]]+ requires 1+ chars, so empty entryId won't match
90
+ expect(count).toBe(1);
91
+ expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'valid-id', 'accepted');
92
+ });
93
+
94
+ it('should not double-record duplicate entryIds', () => {
95
+ const brain = makeBrain();
96
+ const plan = makePlan([
97
+ 'Decision 1 [entryId:same-entry]',
98
+ 'Decision 2 [entryId:same-entry]',
99
+ 'Decision 3 [entryId:different-entry]',
100
+ ]);
101
+
102
+ const count = recordPlanFeedback(plan, brain as unknown);
103
+
104
+ expect(count).toBe(2);
105
+ expect(brain.recordFeedback).toHaveBeenCalledTimes(2);
106
+ expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'same-entry', 'accepted');
107
+ expect(brain.recordFeedback).toHaveBeenCalledWith(
108
+ 'Test objective',
109
+ 'different-entry',
110
+ 'accepted',
111
+ );
112
+ });
113
+
114
+ it('should gracefully handle recordFeedback throwing', () => {
115
+ const brain = makeBrain();
116
+ brain.recordFeedback.mockImplementationOnce(() => {
117
+ throw new Error('Entry not found');
118
+ });
119
+ const plan = makePlan([
120
+ 'Decision 1 [entryId:missing-entry]',
121
+ 'Decision 2 [entryId:valid-entry]',
122
+ ]);
123
+
124
+ const count = recordPlanFeedback(plan, brain as unknown);
125
+
126
+ // First one throws, second succeeds
127
+ expect(count).toBe(1);
128
+ expect(brain.recordFeedback).toHaveBeenCalledTimes(2);
129
+ });
130
+
131
+ it('should return 0 and not call maybeAutoBuild when no entryIds found', () => {
132
+ const brain = makeBrain();
133
+ const intelligence = makeBrainIntelligence();
134
+ const plan = makePlan(['Decision without markers', 'Another plain decision']);
135
+
136
+ const count = recordPlanFeedback(plan, brain as unknown, intelligence as unknown);
137
+
138
+ expect(count).toBe(0);
139
+ expect(brain.recordFeedback).not.toHaveBeenCalled();
140
+ expect(intelligence.maybeAutoBuildOnFeedback).not.toHaveBeenCalled();
141
+ });
142
+
143
+ it('should handle empty decisions array', () => {
144
+ const brain = makeBrain();
145
+ const plan = makePlan([]);
146
+
147
+ const count = recordPlanFeedback(plan, brain as unknown);
148
+
149
+ expect(count).toBe(0);
150
+ expect(brain.recordFeedback).not.toHaveBeenCalled();
151
+ });
152
+
153
+ it('should work without brainIntelligence (optional param)', () => {
154
+ const brain = makeBrain();
155
+ const plan = makePlan(['Decision [entryId:entry-1]']);
156
+
157
+ const count = recordPlanFeedback(plan, brain as unknown);
158
+
159
+ expect(count).toBe(1);
160
+ // No error thrown despite missing brainIntelligence
161
+ });
162
+
163
+ it('should extract multiple entryIds from a single decision string', () => {
164
+ const brain = makeBrain();
165
+ const plan = makePlan(['Combined: [entryId:first-entry] and also [entryId:second-entry]']);
166
+
167
+ const count = recordPlanFeedback(plan, brain as unknown);
168
+
169
+ expect(count).toBe(2);
170
+ expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'first-entry', 'accepted');
171
+ expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'second-entry', 'accepted');
172
+ });
173
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Shared helper for recording brain feedback from plan decisions and context.
3
+ *
4
+ * Used by both plan_complete_lifecycle (planning-extra-ops.ts) and
5
+ * orchestrate_complete (orchestrate-ops.ts) to close the brain learning loop.
6
+ */
7
+
8
+ import type { Brain } from '../brain/brain.js';
9
+ import type { BrainIntelligence } from '../brain/intelligence.js';
10
+
11
+ /** Regex to extract vault entry IDs embedded in decision/context strings. */
12
+ const ENTRY_ID_REGEX = /\[entryId:([^\]]+)\]/g;
13
+
14
+ /**
15
+ * Extract entry IDs from an array of decision or context strings,
16
+ * record positive feedback for each, and optionally trigger auto-rebuild.
17
+ *
18
+ * @returns Number of feedback entries recorded.
19
+ */
20
+ export function recordPlanFeedback(
21
+ plan: { objective: string; decisions: (string | { decision: string })[] },
22
+ brain: Brain,
23
+ brainIntelligence?: BrainIntelligence,
24
+ ): number {
25
+ let feedbackRecorded = 0;
26
+ const seen = new Set<string>();
27
+
28
+ // Collect all strings to scan: decisions + any context strings
29
+ const strings: string[] = [];
30
+
31
+ for (const d of plan.decisions) {
32
+ const str = typeof d === 'string' ? d : d.decision;
33
+ strings.push(str);
34
+ }
35
+
36
+ for (const str of strings) {
37
+ // Use matchAll to find all entryId markers in each string
38
+ for (const match of str.matchAll(ENTRY_ID_REGEX)) {
39
+ const entryId = match[1];
40
+ // Skip duplicates within the same plan
41
+ if (seen.has(entryId)) continue;
42
+ seen.add(entryId);
43
+
44
+ try {
45
+ brain.recordFeedback(plan.objective, entryId, 'accepted');
46
+ feedbackRecorded++;
47
+ } catch {
48
+ // Graceful degradation — skip if entry not found or already recorded
49
+ }
50
+ }
51
+ }
52
+
53
+ // Trigger auto-rebuild check after recording feedback
54
+ if (feedbackRecorded > 0 && brainIntelligence) {
55
+ try {
56
+ brainIntelligence.maybeAutoBuildOnFeedback();
57
+ } catch {
58
+ // Auto-rebuild is best-effort
59
+ }
60
+ }
61
+
62
+ return feedbackRecorded;
63
+ }
@@ -69,7 +69,7 @@ function createMockRuntime(): AgentRuntime {
69
69
 
70
70
  return {
71
71
  planner: {
72
- iterate: vi.fn(() => plan),
72
+ iterate: vi.fn(() => ({ plan, mutated: 1 })),
73
73
  splitTasks: vi.fn(() => ({
74
74
  ...plan,
75
75
  tasks: [plan.tasks[0], { id: 'task-2', title: 'Task 2' }],
@@ -159,6 +159,48 @@ describe('createPlanningExtraOps', () => {
159
159
  );
160
160
  });
161
161
 
162
+ it('returns iterated: false when no changes detected', async () => {
163
+ vi.mocked(runtime.planner.iterate).mockReturnValue({
164
+ plan: makePlan() as unknown,
165
+ mutated: 0,
166
+ } as unknown);
167
+ const result = (await findOp(ops, 'plan_iterate').handler({
168
+ planId: 'plan-1',
169
+ })) as Record<string, unknown>;
170
+ expect(result.iterated).toBe(false);
171
+ expect(result.reason).toBe('no changes detected');
172
+ });
173
+
174
+ it('passes alternatives to planner.iterate', async () => {
175
+ const result = (await findOp(ops, 'plan_iterate').handler({
176
+ planId: 'plan-1',
177
+ alternatives: [
178
+ { approach: 'Alt A', pros: ['fast'], cons: ['fragile'], rejected_reason: 'Too risky' },
179
+ ],
180
+ })) as Record<string, unknown>;
181
+ expect(result.iterated).toBe(true);
182
+ expect(runtime.planner.iterate).toHaveBeenCalledWith(
183
+ 'plan-1',
184
+ expect.objectContaining({
185
+ alternatives: [expect.objectContaining({ approach: 'Alt A' })],
186
+ }),
187
+ );
188
+ });
189
+
190
+ it('passes decisions to planner.iterate', async () => {
191
+ const result = (await findOp(ops, 'plan_iterate').handler({
192
+ planId: 'plan-1',
193
+ decisions: [{ decision: 'Use FTS5', rationale: 'Performance' }],
194
+ })) as Record<string, unknown>;
195
+ expect(result.iterated).toBe(true);
196
+ expect(runtime.planner.iterate).toHaveBeenCalledWith(
197
+ 'plan-1',
198
+ expect.objectContaining({
199
+ decisions: [{ decision: 'Use FTS5', rationale: 'Performance' }],
200
+ }),
201
+ );
202
+ });
203
+
162
204
  it('returns error on failure', async () => {
163
205
  vi.mocked(runtime.planner.iterate).mockImplementation(() => {
164
206
  throw new Error('Not a draft');