@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
@@ -38,7 +38,6 @@ const SPREAD_MAX = 5;
38
38
  const RECENCY_DECAY_DAYS = 30;
39
39
  const EXTRACTION_TOOL_THRESHOLD = 3;
40
40
  const EXTRACTION_FILE_THRESHOLD = 3;
41
- const EXTRACTION_LONG_SESSION_MINUTES = 30;
42
41
  const EXTRACTION_HIGH_FEEDBACK_RATIO = 0.8;
43
42
  const AUTO_PROMOTE_THRESHOLD = 0.8;
44
43
  const AUTO_PROMOTE_PENDING_MIN = 0.4;
@@ -500,7 +499,7 @@ export class BrainIntelligence {
500
499
  // ─── Strength Scoring ─────────────────────────────────────────────
501
500
 
502
501
  computeStrengths(): PatternStrength[] {
503
- // Gather feedback data grouped by entry_id
502
+ // Gather feedback data grouped by entry_id, JOIN with entries to avoid N+1 vault.get() calls
504
503
  const feedbackRows = this.provider.all<{
505
504
  entry_id: string;
506
505
  total: number;
@@ -509,16 +508,21 @@ export class BrainIntelligence {
509
508
  modified: number;
510
509
  failed: number;
511
510
  last_used: string;
511
+ entry_title: string | null;
512
+ entry_domain: string | null;
512
513
  }>(
513
- `SELECT entry_id,
514
+ `SELECT bf.entry_id,
514
515
  COUNT(*) as total,
515
- SUM(CASE WHEN action = 'accepted' THEN 1 ELSE 0 END) as accepted,
516
- SUM(CASE WHEN action = 'dismissed' THEN 1 ELSE 0 END) as dismissed,
517
- SUM(CASE WHEN action = 'modified' THEN 1 ELSE 0 END) as modified,
518
- SUM(CASE WHEN action = 'failed' THEN 1 ELSE 0 END) as failed,
519
- MAX(created_at) as last_used
520
- FROM brain_feedback
521
- GROUP BY entry_id`,
516
+ SUM(CASE WHEN bf.action = 'accepted' THEN 1 ELSE 0 END) as accepted,
517
+ SUM(CASE WHEN bf.action = 'dismissed' THEN 1 ELSE 0 END) as dismissed,
518
+ SUM(CASE WHEN bf.action = 'modified' THEN 1 ELSE 0 END) as modified,
519
+ SUM(CASE WHEN bf.action = 'failed' THEN 1 ELSE 0 END) as failed,
520
+ MAX(bf.created_at) as last_used,
521
+ e.title as entry_title,
522
+ e.domain as entry_domain
523
+ FROM brain_feedback bf
524
+ LEFT JOIN entries e ON e.id = bf.entry_id
525
+ GROUP BY bf.entry_id`,
522
526
  );
523
527
 
524
528
  // Count unique session domains as spread proxy
@@ -531,10 +535,9 @@ export class BrainIntelligence {
531
535
  const strengths: PatternStrength[] = [];
532
536
 
533
537
  for (const row of feedbackRows) {
534
- // Look up vault entry for domain info
535
- const entry = this.vault.get(row.entry_id);
536
- const domain = entry?.domain ?? 'unknown';
537
- const pattern = entry?.title ?? row.entry_id;
538
+ // Use JOINed entry data no per-row vault.get() needed
539
+ const domain = row.entry_domain ?? 'unknown';
540
+ const pattern = row.entry_title ?? row.entry_id;
538
541
 
539
542
  // Usage score: min(25, (count / USAGE_MAX) * 25)
540
543
  const usageScore = Math.min(25, (row.total / USAGE_MAX) * 25);
@@ -572,29 +575,33 @@ export class BrainIntelligence {
572
575
  };
573
576
 
574
577
  strengths.push(ps);
575
-
576
- // Persist
577
- this.provider.run(
578
- `INSERT OR REPLACE INTO brain_strengths
579
- (pattern, domain, strength, usage_score, spread_score, success_score, recency_score,
580
- usage_count, unique_contexts, success_rate, last_used, updated_at)
581
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
582
- [
583
- ps.pattern,
584
- ps.domain,
585
- ps.strength,
586
- ps.usageScore,
587
- ps.spreadScore,
588
- ps.successScore,
589
- ps.recencyScore,
590
- ps.usageCount,
591
- ps.uniqueContexts,
592
- ps.successRate,
593
- ps.lastUsed,
594
- ],
595
- );
596
578
  }
597
579
 
580
+ // Persist all strengths in a single transaction to avoid N fsync calls
581
+ this.provider.transaction(() => {
582
+ for (const ps of strengths) {
583
+ this.provider.run(
584
+ `INSERT OR REPLACE INTO brain_strengths
585
+ (pattern, domain, strength, usage_score, spread_score, success_score, recency_score,
586
+ usage_count, unique_contexts, success_rate, last_used, updated_at)
587
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
588
+ [
589
+ ps.pattern,
590
+ ps.domain,
591
+ ps.strength,
592
+ ps.usageScore,
593
+ ps.spreadScore,
594
+ ps.successScore,
595
+ ps.recencyScore,
596
+ ps.usageCount,
597
+ ps.uniqueContexts,
598
+ ps.successRate,
599
+ ps.lastUsed,
600
+ ],
601
+ );
602
+ }
603
+ });
604
+
598
605
  return strengths;
599
606
  }
600
607
 
@@ -739,10 +746,18 @@ export class BrainIntelligence {
739
746
  for (const [tool, count] of toolCounts) {
740
747
  if (count >= EXTRACTION_TOOL_THRESHOLD) {
741
748
  rulesApplied.push('repeated_tool_usage');
749
+ const ctx = session.context ?? '';
750
+ const objective = this.extractObjective(ctx);
751
+ const toolTitle = objective
752
+ ? `Tool pattern: ${tool} (${count}x) during ${objective.slice(0, 60)}`
753
+ : `Frequent use of ${tool} (${count}x)`;
754
+ const toolDescription = objective
755
+ ? `Tool ${tool} used ${count} times while working on: ${objective}. This tool-task pairing may indicate a reusable workflow.`
756
+ : `Tool ${tool} was used ${count} times in session. Consider automating or abstracting this workflow.`;
742
757
  proposals.push(
743
758
  this.createProposal(sessionId, 'repeated_tool_usage', 'pattern', {
744
- title: `Frequent use of ${tool}`,
745
- description: `Tool ${tool} was used ${count} times in session. Consider automating or abstracting this workflow.`,
759
+ title: toolTitle,
760
+ description: toolDescription,
746
761
  confidence: Math.min(0.9, 0.5 + count * 0.1),
747
762
  }),
748
763
  );
@@ -766,57 +781,107 @@ export class BrainIntelligence {
766
781
  if (significantDirs.length > 0) {
767
782
  const [topDir, topFiles] = significantDirs.sort((a, b) => b[1].length - a[1].length)[0];
768
783
  rulesApplied.push('multi_file_edit');
784
+ const ctx = session.context ?? '';
785
+ const objective = this.extractObjective(ctx);
786
+ const isRefactor = /refactor|rename|move|extract|consolidat/i.test(ctx);
787
+ const isFeature = /feat|add|implement|create|new/i.test(ctx);
788
+ const inferredPattern = isRefactor
789
+ ? 'Refactoring'
790
+ : isFeature
791
+ ? 'Feature'
792
+ : 'Cross-cutting change';
793
+ const mfeTitle = objective
794
+ ? `${inferredPattern}: ${objective.slice(0, 70)}`
795
+ : `${inferredPattern} in ${topDir} (${topFiles.length} files)`;
796
+ const mfeDescription = objective
797
+ ? `${inferredPattern} across ${topFiles.length} files in ${topDir}: ${objective}`
798
+ : `Session modified ${topFiles.length} files in ${topDir}: ${topFiles.slice(0, 5).join(', ')}${topFiles.length > 5 ? '...' : ''}.`;
769
799
  proposals.push(
770
800
  this.createProposal(sessionId, 'multi_file_edit', 'pattern', {
771
- title: `Multi-file change pattern in ${topDir} (${topFiles.length} files)`,
772
- description: `Session modified ${topFiles.length} files in ${topDir}: ${topFiles.slice(0, 5).join(', ')}${topFiles.length > 5 ? '...' : ''}. This may indicate an architectural pattern.`,
801
+ title: mfeTitle,
802
+ description: mfeDescription,
773
803
  confidence: Math.min(0.8, 0.4 + topFiles.length * 0.05),
774
804
  }),
775
805
  );
776
806
  }
777
807
  }
778
808
 
779
- // Rule 3: Long session (>30min) neutral observation, not anti-pattern
780
- if (session.endedAt && session.startedAt) {
781
- const durationMs =
782
- new Date(session.endedAt).getTime() - new Date(session.startedAt).getTime();
783
- const durationMin = durationMs / 60000;
784
- if (durationMin > EXTRACTION_LONG_SESSION_MINUTES) {
785
- rulesApplied.push('long_session');
786
- proposals.push(
787
- this.createProposal(sessionId, 'long_session', 'pattern', {
788
- title: `Long session (${Math.round(durationMin)} minutes)`,
789
- description: `Session lasted ${Math.round(durationMin)} minutes. Deep work session — review if this duration was productive or indicates a need for better tooling.`,
790
- confidence: 0.3,
791
- }),
792
- );
793
- }
794
- }
795
-
796
- // Rule 4: Plan completed — moderate confidence to avoid auto-promoting generic entries
809
+ // Rule 3: Plan completed — parse session.context for actionable title + dynamic confidence
797
810
  if (session.planId && session.planOutcome === 'completed') {
798
811
  rulesApplied.push('plan_completed');
812
+ const ctx = session.context ?? '';
813
+ const objective = this.extractObjective(ctx);
814
+ const hasScope = /scope|included|excluded/i.test(ctx);
815
+ const hasCriteria = /criteria|acceptance|verification/i.test(ctx);
816
+ const confidence =
817
+ ctx.length > 0
818
+ ? hasScope && hasCriteria
819
+ ? 0.85
820
+ : hasScope || hasCriteria
821
+ ? 0.8
822
+ : 0.75
823
+ : 0.5;
824
+ const title = objective
825
+ ? `Workflow: ${objective.slice(0, 80)}`
826
+ : `Successful plan: ${session.planId}`;
827
+ const description = objective
828
+ ? `Completed: ${objective}${hasScope ? '. Scope and constraints documented in session context.' : ''}`
829
+ : `Plan ${session.planId} completed successfully. This workflow can be reused for similar tasks.`;
799
830
  proposals.push(
800
831
  this.createProposal(sessionId, 'plan_completed', 'workflow', {
801
- title: `Successful plan: ${session.planId}`,
802
- description: `Plan ${session.planId} completed successfully. This workflow can be reused for similar tasks.`,
803
- confidence: 0.65,
832
+ title,
833
+ description,
834
+ confidence,
804
835
  }),
805
836
  );
806
837
  }
807
838
 
808
- // Rule 5: Plan abandoned
839
+ // Rule 4: Plan abandoned — parse context for failure reason
809
840
  if (session.planId && session.planOutcome === 'abandoned') {
810
841
  rulesApplied.push('plan_abandoned');
842
+ const ctx = session.context ?? '';
843
+ const objective = this.extractObjective(ctx);
844
+ const hasFailureReason = /blocked|failed|wrong|mistake|abandoned|reverted|conflict/i.test(
845
+ ctx,
846
+ );
847
+ const confidence = ctx.length > 0 ? (hasFailureReason ? 0.85 : 0.75) : 0.5;
848
+ const title = objective
849
+ ? `Anti-pattern: ${objective.slice(0, 80)}`
850
+ : `Abandoned plan: ${session.planId}`;
851
+ const description = objective
852
+ ? `Abandoned: ${objective}${hasFailureReason ? '. Failure indicators found in session context — review for root cause.' : '. Review what went wrong to avoid repeating.'}`
853
+ : `Plan ${session.planId} was abandoned. Review what went wrong to avoid repeating in future sessions.`;
811
854
  proposals.push(
812
855
  this.createProposal(sessionId, 'plan_abandoned', 'anti-pattern', {
813
- title: `Abandoned plan: ${session.planId}`,
814
- description: `Plan ${session.planId} was abandoned. Review what went wrong to avoid repeating in future sessions.`,
815
- confidence: 0.7,
856
+ title,
857
+ description,
858
+ confidence,
816
859
  }),
817
860
  );
818
861
  }
819
862
 
863
+ // Rule 5: Drift detected — fires when plan completed but context contains drift indicators
864
+ if (session.planId && session.planOutcome === 'completed' && session.context) {
865
+ const driftPattern =
866
+ /drift|skipped|added.*unplanned|changed scope|out of scope|deviat|unplanned/i;
867
+ if (driftPattern.test(session.context)) {
868
+ rulesApplied.push('drift_detected');
869
+ const objective = this.extractObjective(session.context);
870
+ const driftMatch =
871
+ session.context.match(/drift[:\s]+(.{1,120})/i) ??
872
+ session.context.match(/skipped[:\s]+(.{1,120})/i) ??
873
+ session.context.match(/unplanned[:\s]+(.{1,120})/i);
874
+ const driftDetail = driftMatch ? driftMatch[1].trim() : 'scope changed during execution';
875
+ proposals.push(
876
+ this.createProposal(sessionId, 'drift_detected', 'anti-pattern', {
877
+ title: `Plan drift: ${objective ? objective.slice(0, 60) : session.planId} — ${driftDetail.slice(0, 40)}`,
878
+ description: `Plan ${objective ?? session.planId} completed with drift: ${driftDetail}. Review scope controls for future planning.`,
879
+ confidence: 0.8,
880
+ }),
881
+ );
882
+ }
883
+ }
884
+
820
885
  // Rule 6: High feedback ratio (>80% accept or dismiss)
821
886
  const feedbackRow = this.provider.get<{
822
887
  total: number;
@@ -1369,12 +1434,58 @@ export class BrainIntelligence {
1369
1434
  };
1370
1435
  }
1371
1436
 
1437
+ /**
1438
+ * Extract the objective from session context — first meaningful sentence or line.
1439
+ * Returns empty string if context is empty or unparseable.
1440
+ */
1441
+ private extractObjective(context: string): string {
1442
+ if (!context || context.trim().length === 0) return '';
1443
+ // Try to find an "Objective:" line
1444
+ const objMatch = context.match(/objective[:\s]+(.+)/i);
1445
+ if (objMatch) return objMatch[1].trim().replace(/\s+/g, ' ');
1446
+ // Fall back to first non-empty line
1447
+ const firstLine = context
1448
+ .split('\n')
1449
+ .map((l) => l.trim())
1450
+ .find((l) => l.length > 0);
1451
+ return firstLine ? firstLine.replace(/\s+/g, ' ') : '';
1452
+ }
1453
+
1372
1454
  private createProposal(
1373
1455
  sessionId: string,
1374
1456
  rule: string,
1375
1457
  type: 'pattern' | 'anti-pattern' | 'workflow',
1376
1458
  data: { title: string; description: string; confidence: number },
1377
1459
  ): KnowledgeProposal {
1460
+ // Dedup guard: skip if a proposal with the same rule + sessionId already exists
1461
+ const existing = this.provider.get<{
1462
+ id: string;
1463
+ session_id: string;
1464
+ rule: string;
1465
+ type: string;
1466
+ title: string;
1467
+ description: string;
1468
+ confidence: number;
1469
+ promoted: number;
1470
+ created_at: string;
1471
+ }>('SELECT * FROM brain_proposals WHERE session_id = ? AND rule = ? LIMIT 1', [
1472
+ sessionId,
1473
+ rule,
1474
+ ]);
1475
+ if (existing) {
1476
+ return {
1477
+ id: existing.id,
1478
+ sessionId: existing.session_id,
1479
+ rule: existing.rule,
1480
+ type: existing.type as 'pattern' | 'anti-pattern' | 'workflow',
1481
+ title: existing.title,
1482
+ description: existing.description,
1483
+ confidence: existing.confidence,
1484
+ promoted: existing.promoted === 1,
1485
+ createdAt: existing.created_at,
1486
+ };
1487
+ }
1488
+
1378
1489
  const id = randomUUID();
1379
1490
  this.provider.run(
1380
1491
  `INSERT INTO brain_proposals (id, session_id, rule, type, title, description, confidence)
@@ -229,14 +229,17 @@ export class LearningRadar {
229
229
  }
230
230
 
231
231
  /**
232
- * Dismiss a pending candidate — mark it as not worth capturing.
232
+ * Dismiss one or more pending candidates — mark them as not worth capturing.
233
233
  */
234
- dismiss(candidateId: number): { dismissed: boolean } {
234
+ dismiss(candidateIds: number | number[]): { dismissed: number } {
235
+ const ids = Array.isArray(candidateIds) ? candidateIds : [candidateIds];
236
+ if (ids.length === 0) return { dismissed: 0 };
237
+ const placeholders = ids.map(() => '?').join(',');
235
238
  const result = this.provider.run(
236
- "UPDATE radar_candidates SET status = 'dismissed' WHERE id = ? AND status = 'pending'",
237
- [candidateId],
239
+ `UPDATE radar_candidates SET status = 'dismissed' WHERE id IN (${placeholders}) AND status = 'pending'`,
240
+ ids,
238
241
  );
239
- return { dismissed: result.changes > 0 };
242
+ return { dismissed: result.changes };
240
243
  }
241
244
 
242
245
  /**
@@ -335,7 +335,7 @@ describe('Ambient learning radar (#208)', () => {
335
335
  const pending = candidates.find((c) => c.title.includes('stale cache'));
336
336
  expect(pending).toBeDefined();
337
337
  const result = learningRadar.dismiss(pending!.id);
338
- expect(result.dismissed).toBe(true);
338
+ expect(result.dismissed).toBe(1);
339
339
  });
340
340
 
341
341
  it('getStats returns radar statistics', () => {
@@ -292,7 +292,7 @@ async function callAnthropic(params: AnthropicCallParams): Promise<AnthropicResp
292
292
  'anthropic-version': API_VERSION,
293
293
  },
294
294
  body: JSON.stringify(body),
295
- signal: params.signal,
295
+ signal: params.signal ?? AbortSignal.timeout(120_000),
296
296
  });
297
297
 
298
298
  if (!response.ok) {
@@ -88,6 +88,10 @@ export class NotificationEngine {
88
88
  setTimeout(() => this.poll(), 10_000);
89
89
 
90
90
  this.timer = setInterval(() => this.poll(), this.intervalMs);
91
+ // Don't prevent process exit for background notification polling
92
+ if (this.timer && typeof this.timer === 'object' && 'unref' in this.timer) {
93
+ (this.timer as NodeJS.Timeout).unref();
94
+ }
91
95
  }
92
96
 
93
97
  /**
@@ -35,7 +35,7 @@ describe('IntentRouter', () => {
35
35
  describe('construction', () => {
36
36
  it('seeds 10 default modes on first creation', () => {
37
37
  const modes = router.getModes();
38
- expect(modes.length).toBe(10);
38
+ expect(modes.length).toBe(11);
39
39
  });
40
40
 
41
41
  it('starts in GENERAL-MODE', () => {
@@ -44,7 +44,7 @@ describe('IntentRouter', () => {
44
44
 
45
45
  it('is idempotent — second instance does not duplicate modes', () => {
46
46
  const router2 = new IntentRouter(vault);
47
- expect(router2.getModes().length).toBe(10);
47
+ expect(router2.getModes().length).toBe(11);
48
48
  });
49
49
  });
50
50
 
@@ -199,7 +199,7 @@ describe('IntentRouter', () => {
199
199
  it('adds a new mode to the database', () => {
200
200
  router.registerMode(customMode);
201
201
  const modes = router.getModes();
202
- expect(modes.length).toBe(11);
202
+ expect(modes.length).toBe(12);
203
203
  const found = modes.find((m) => m.mode === 'CUSTOM-MODE');
204
204
  expect(found).toBeDefined();
205
205
  expect(found!.keywords).toEqual(['custom', 'special']);
@@ -338,6 +338,76 @@ describe('IntentRouter', () => {
338
338
  });
339
339
  });
340
340
 
341
+ // ─── YOLO-MODE ───────────────────────────────────────────────
342
+
343
+ describe('YOLO-MODE', () => {
344
+ it('route_intent with "yolo" returns YOLO-MODE', () => {
345
+ const result = router.routeIntent('go yolo on this task');
346
+ expect(result.intent).toBe('yolo');
347
+ expect(result.mode).toBe('YOLO-MODE');
348
+ expect(result.matchedKeywords).toContain('yolo');
349
+ });
350
+
351
+ it('morph to YOLO-MODE succeeds when hook pack is installed', () => {
352
+ const result = router.morph('YOLO-MODE', { hookPackInstalled: true });
353
+ expect(result.previousMode).toBe('GENERAL-MODE');
354
+ expect(result.currentMode).toBe('YOLO-MODE');
355
+ expect(result.behaviorRules.length).toBe(5);
356
+ expect(result.blocked).toBeUndefined();
357
+ expect(result.error).toBeUndefined();
358
+ });
359
+
360
+ it('morph to YOLO-MODE fails when hook pack is missing', () => {
361
+ const result = router.morph('YOLO-MODE');
362
+ expect(result.blocked).toBe(true);
363
+ expect(result.error).toContain('yolo-safety hook pack');
364
+ expect(result.error).toContain('soleri hooks add-pack yolo-safety');
365
+ expect(result.currentMode).toBe('GENERAL-MODE'); // unchanged
366
+ expect(router.getCurrentMode()).toBe('GENERAL-MODE'); // not switched
367
+ });
368
+
369
+ it('morph to YOLO-MODE fails when hookPackInstalled is explicitly false', () => {
370
+ const result = router.morph('YOLO-MODE', { hookPackInstalled: false });
371
+ expect(result.blocked).toBe(true);
372
+ expect(result.error).toContain('yolo-safety hook pack');
373
+ expect(router.getCurrentMode()).toBe('GENERAL-MODE');
374
+ });
375
+
376
+ it('morph to other modes is unaffected by the gate', () => {
377
+ const result = router.morph('BUILD-MODE');
378
+ expect(result.currentMode).toBe('BUILD-MODE');
379
+ expect(result.blocked).toBeUndefined();
380
+ expect(result.error).toBeUndefined();
381
+ expect(router.getCurrentMode()).toBe('BUILD-MODE');
382
+ });
383
+
384
+ it('get_behavior_rules returns 5 rules', () => {
385
+ const rules = router.getBehaviorRules('YOLO-MODE');
386
+ expect(rules).toHaveLength(5);
387
+ expect(rules[0]).toContain('Skip plan approval gates');
388
+ expect(rules[1]).toContain('orchestrate_complete');
389
+ expect(rules[2]).toContain('vault gather-before-execute');
390
+ expect(rules[3]).toContain('Hook pack must be installed');
391
+ expect(rules[4]).toContain('exit YOLO');
392
+ });
393
+
394
+ it('all keywords route to YOLO-MODE', () => {
395
+ const keywords = [
396
+ 'yolo',
397
+ 'autonomous',
398
+ 'fire-and-forget',
399
+ 'hands-off',
400
+ 'no-approval',
401
+ 'skip-gates',
402
+ 'full-auto',
403
+ ];
404
+ for (const kw of keywords) {
405
+ const result = router.routeIntent(kw);
406
+ expect(result.mode).toBe('YOLO-MODE');
407
+ }
408
+ });
409
+ });
410
+
341
411
  // ─── getModes ─────────────────────────────────────────────────
342
412
 
343
413
  describe('getModes', () => {
@@ -15,6 +15,7 @@ import type {
15
15
  OperationalMode,
16
16
  IntentClassification,
17
17
  ModeConfig,
18
+ MorphOptions,
18
19
  MorphResult,
19
20
  RoutingAccuracyReport,
20
21
  } from './types.js';
@@ -132,6 +133,27 @@ const DEFAULT_MODES: ModeConfig[] = [
132
133
  behaviorRules: ['Be helpful', 'Ask clarifying questions when needed'],
133
134
  keywords: [],
134
135
  },
136
+ {
137
+ mode: 'YOLO-MODE',
138
+ intent: 'yolo',
139
+ description: 'Autonomous execution — skip approval gates, execute directly',
140
+ behaviorRules: [
141
+ 'Skip plan approval gates — execute tasks directly',
142
+ 'Still run orchestrate_complete — knowledge capture is non-negotiable',
143
+ 'Still run vault gather-before-execute — decisions must be informed',
144
+ 'Hook pack must be installed — refuse to activate without yolo-safety hooks',
145
+ 'User can exit with "exit YOLO" or session end',
146
+ ],
147
+ keywords: [
148
+ 'yolo',
149
+ 'autonomous',
150
+ 'fire-and-forget',
151
+ 'hands-off',
152
+ 'no-approval',
153
+ 'skip-gates',
154
+ 'full-auto',
155
+ ],
156
+ },
135
157
  ];
136
158
 
137
159
  // ─── Class ──────────────────────────────────────────────────────────
@@ -140,6 +162,7 @@ export class IntentRouter {
140
162
  private vault: Vault;
141
163
  private provider: PersistenceProvider;
142
164
  private currentMode: OperationalMode = 'GENERAL-MODE';
165
+ private modesCache: ModeConfig[] | null = null;
143
166
 
144
167
  constructor(vault: Vault) {
145
168
  this.vault = vault;
@@ -267,7 +290,7 @@ export class IntentRouter {
267
290
 
268
291
  // ─── Mode Management ───────────────────────────────────────────────
269
292
 
270
- morph(mode: OperationalMode): MorphResult {
293
+ morph(mode: OperationalMode, options?: MorphOptions): MorphResult {
271
294
  // Handle "reset" as a built-in alias for GENERAL-MODE
272
295
  const resolvedMode: OperationalMode = (mode as string) === 'reset' ? 'GENERAL-MODE' : mode;
273
296
 
@@ -283,8 +306,24 @@ export class IntentRouter {
283
306
  throw new Error(`Unknown mode: ${mode}. Available: ${available}`);
284
307
  }
285
308
 
309
+ // ─── YOLO-MODE activation gate ────────────────────────────────
310
+ // YOLO-MODE requires the yolo-safety hook pack to be installed.
311
+ // The CLI/facade layer provides hookPackInstalled based on filesystem check.
312
+ if (resolvedMode === 'YOLO-MODE' && !options?.hookPackInstalled) {
313
+ return {
314
+ previousMode: this.currentMode,
315
+ currentMode: this.currentMode, // unchanged — mode switch blocked
316
+ behaviorRules: this.getBehaviorRules(),
317
+ blocked: true,
318
+ error:
319
+ 'YOLO-MODE requires the yolo-safety hook pack. ' +
320
+ 'Install it with: soleri hooks add-pack yolo-safety',
321
+ };
322
+ }
323
+
286
324
  const previousMode = this.currentMode;
287
325
  this.currentMode = resolvedMode;
326
+ this.modesCache = null; // Invalidate cache on mode change
288
327
  const behaviorRules = JSON.parse(row.behavior_rules) as string[];
289
328
 
290
329
  return { previousMode, currentMode: resolvedMode, behaviorRules };
@@ -296,18 +335,16 @@ export class IntentRouter {
296
335
 
297
336
  getBehaviorRules(mode?: OperationalMode): string[] {
298
337
  const target = mode ?? this.currentMode;
299
- const row = this.provider.get<{ behavior_rules: string }>(
300
- 'SELECT behavior_rules FROM agent_modes WHERE mode = ?',
301
- [target],
302
- );
303
-
304
- if (!row) return [];
305
- return JSON.parse(row.behavior_rules) as string[];
338
+ const modes = this.getModes();
339
+ const found = modes.find((m) => m.mode === target);
340
+ return found ? found.behaviorRules : [];
306
341
  }
307
342
 
308
343
  getModes(): ModeConfig[] {
344
+ if (this.modesCache) return this.modesCache;
309
345
  const rows = this.provider.all<ModeRow>('SELECT * FROM agent_modes ORDER BY mode');
310
- return rows.map(rowToModeConfig);
346
+ this.modesCache = rows.map(rowToModeConfig);
347
+ return this.modesCache;
311
348
  }
312
349
 
313
350
  registerMode(config: ModeConfig): void {
@@ -322,6 +359,7 @@ export class IntentRouter {
322
359
  JSON.stringify(config.keywords),
323
360
  ],
324
361
  );
362
+ this.modesCache = null;
325
363
  }
326
364
 
327
365
  updateModeRules(mode: OperationalMode, rules: string[]): void {
@@ -332,6 +370,7 @@ export class IntentRouter {
332
370
  if (result.changes === 0) {
333
371
  throw new Error(`Unknown mode: ${mode}`);
334
372
  }
373
+ this.modesCache = null;
335
374
  }
336
375
 
337
376
  // ─── Routing Feedback ─────────────────────────────────────────────
@@ -65,7 +65,8 @@ export type IntentType =
65
65
  | 'explore'
66
66
  | 'plan'
67
67
  | 'review'
68
- | 'general';
68
+ | 'general'
69
+ | 'yolo';
69
70
 
70
71
  export type OperationalMode =
71
72
  | 'BUILD-MODE'
@@ -77,7 +78,8 @@ export type OperationalMode =
77
78
  | 'EXPLORE-MODE'
78
79
  | 'PLAN-MODE'
79
80
  | 'REVIEW-MODE'
80
- | 'GENERAL-MODE';
81
+ | 'GENERAL-MODE'
82
+ | 'YOLO-MODE';
81
83
 
82
84
  export interface IntentClassification {
83
85
  intent: IntentType;
@@ -95,10 +97,19 @@ export interface ModeConfig {
95
97
  keywords: string[];
96
98
  }
97
99
 
100
+ export interface MorphOptions {
101
+ /** Whether the yolo-safety hook pack is installed. Required for YOLO-MODE activation. */
102
+ hookPackInstalled?: boolean;
103
+ }
104
+
98
105
  export interface MorphResult {
99
106
  previousMode: OperationalMode;
100
107
  currentMode: OperationalMode;
101
108
  behaviorRules: string[];
109
+ /** Present when activation is refused (e.g. missing hook pack). */
110
+ error?: string;
111
+ /** When true, the mode switch was blocked. */
112
+ blocked?: boolean;
102
113
  }
103
114
 
104
115
  export interface RoutingAccuracyReport {