@soleri/core 9.11.0 → 9.13.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 (234) hide show
  1. package/dist/adapters/types.d.ts +2 -0
  2. package/dist/adapters/types.d.ts.map +1 -1
  3. package/dist/brain/brain.d.ts +5 -1
  4. package/dist/brain/brain.d.ts.map +1 -1
  5. package/dist/brain/brain.js +97 -10
  6. package/dist/brain/brain.js.map +1 -1
  7. package/dist/dream/cron-manager.d.ts +10 -0
  8. package/dist/dream/cron-manager.d.ts.map +1 -0
  9. package/dist/dream/cron-manager.js +122 -0
  10. package/dist/dream/cron-manager.js.map +1 -0
  11. package/dist/dream/dream-engine.d.ts +34 -0
  12. package/dist/dream/dream-engine.d.ts.map +1 -0
  13. package/dist/dream/dream-engine.js +88 -0
  14. package/dist/dream/dream-engine.js.map +1 -0
  15. package/dist/dream/dream-ops.d.ts +8 -0
  16. package/dist/dream/dream-ops.d.ts.map +1 -0
  17. package/dist/dream/dream-ops.js +49 -0
  18. package/dist/dream/dream-ops.js.map +1 -0
  19. package/dist/dream/index.d.ts +7 -0
  20. package/dist/dream/index.d.ts.map +1 -0
  21. package/dist/dream/index.js +5 -0
  22. package/dist/dream/index.js.map +1 -0
  23. package/dist/dream/schema.d.ts +3 -0
  24. package/dist/dream/schema.d.ts.map +1 -0
  25. package/dist/dream/schema.js +16 -0
  26. package/dist/dream/schema.js.map +1 -0
  27. package/dist/embeddings/index.d.ts +5 -0
  28. package/dist/embeddings/index.d.ts.map +1 -0
  29. package/dist/embeddings/index.js +3 -0
  30. package/dist/embeddings/index.js.map +1 -0
  31. package/dist/embeddings/openai-provider.d.ts +31 -0
  32. package/dist/embeddings/openai-provider.d.ts.map +1 -0
  33. package/dist/embeddings/openai-provider.js +120 -0
  34. package/dist/embeddings/openai-provider.js.map +1 -0
  35. package/dist/embeddings/pipeline.d.ts +36 -0
  36. package/dist/embeddings/pipeline.d.ts.map +1 -0
  37. package/dist/embeddings/pipeline.js +78 -0
  38. package/dist/embeddings/pipeline.js.map +1 -0
  39. package/dist/embeddings/types.d.ts +62 -0
  40. package/dist/embeddings/types.d.ts.map +1 -0
  41. package/dist/embeddings/types.js +3 -0
  42. package/dist/embeddings/types.js.map +1 -0
  43. package/dist/engine/bin/soleri-engine.js +4 -1
  44. package/dist/engine/bin/soleri-engine.js.map +1 -1
  45. package/dist/engine/module-manifest.d.ts.map +1 -1
  46. package/dist/engine/module-manifest.js +20 -0
  47. package/dist/engine/module-manifest.js.map +1 -1
  48. package/dist/engine/register-engine.d.ts.map +1 -1
  49. package/dist/engine/register-engine.js +12 -0
  50. package/dist/engine/register-engine.js.map +1 -1
  51. package/dist/flows/chain-types.d.ts +8 -8
  52. package/dist/flows/dispatch-registry.d.ts +15 -1
  53. package/dist/flows/dispatch-registry.d.ts.map +1 -1
  54. package/dist/flows/dispatch-registry.js +28 -1
  55. package/dist/flows/dispatch-registry.js.map +1 -1
  56. package/dist/flows/executor.d.ts +20 -2
  57. package/dist/flows/executor.d.ts.map +1 -1
  58. package/dist/flows/executor.js +79 -1
  59. package/dist/flows/executor.js.map +1 -1
  60. package/dist/flows/index.d.ts +2 -1
  61. package/dist/flows/index.d.ts.map +1 -1
  62. package/dist/flows/index.js.map +1 -1
  63. package/dist/flows/types.d.ts +43 -21
  64. package/dist/flows/types.d.ts.map +1 -1
  65. package/dist/index.d.ts +6 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +4 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/persona/defaults.d.ts +8 -0
  70. package/dist/persona/defaults.d.ts.map +1 -1
  71. package/dist/persona/defaults.js +49 -0
  72. package/dist/persona/defaults.js.map +1 -1
  73. package/dist/plugins/types.d.ts +31 -31
  74. package/dist/runtime/admin-ops.d.ts.map +1 -1
  75. package/dist/runtime/admin-ops.js +15 -0
  76. package/dist/runtime/admin-ops.js.map +1 -1
  77. package/dist/runtime/admin-setup-ops.js +2 -2
  78. package/dist/runtime/admin-setup-ops.js.map +1 -1
  79. package/dist/runtime/embedding-ops.d.ts +12 -0
  80. package/dist/runtime/embedding-ops.d.ts.map +1 -0
  81. package/dist/runtime/embedding-ops.js +96 -0
  82. package/dist/runtime/embedding-ops.js.map +1 -0
  83. package/dist/runtime/facades/embedding-facade.d.ts +7 -0
  84. package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
  85. package/dist/runtime/facades/embedding-facade.js +8 -0
  86. package/dist/runtime/facades/embedding-facade.js.map +1 -0
  87. package/dist/runtime/facades/index.d.ts.map +1 -1
  88. package/dist/runtime/facades/index.js +12 -0
  89. package/dist/runtime/facades/index.js.map +1 -1
  90. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  91. package/dist/runtime/facades/orchestrate-facade.js +120 -0
  92. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  93. package/dist/runtime/feature-flags.d.ts.map +1 -1
  94. package/dist/runtime/feature-flags.js +4 -0
  95. package/dist/runtime/feature-flags.js.map +1 -1
  96. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  97. package/dist/runtime/orchestrate-ops.js +140 -9
  98. package/dist/runtime/orchestrate-ops.js.map +1 -1
  99. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  100. package/dist/runtime/planning-extra-ops.js +51 -0
  101. package/dist/runtime/planning-extra-ops.js.map +1 -1
  102. package/dist/runtime/preflight.d.ts +32 -0
  103. package/dist/runtime/preflight.d.ts.map +1 -0
  104. package/dist/runtime/preflight.js +29 -0
  105. package/dist/runtime/preflight.js.map +1 -0
  106. package/dist/runtime/runtime.d.ts.map +1 -1
  107. package/dist/runtime/runtime.js +33 -2
  108. package/dist/runtime/runtime.js.map +1 -1
  109. package/dist/runtime/types.d.ts +27 -0
  110. package/dist/runtime/types.d.ts.map +1 -1
  111. package/dist/skills/step-tracker.d.ts +39 -0
  112. package/dist/skills/step-tracker.d.ts.map +1 -0
  113. package/dist/skills/step-tracker.js +105 -0
  114. package/dist/skills/step-tracker.js.map +1 -0
  115. package/dist/skills/sync-skills.d.ts +3 -2
  116. package/dist/skills/sync-skills.d.ts.map +1 -1
  117. package/dist/skills/sync-skills.js +42 -8
  118. package/dist/skills/sync-skills.js.map +1 -1
  119. package/dist/subagent/dispatcher.d.ts +4 -3
  120. package/dist/subagent/dispatcher.d.ts.map +1 -1
  121. package/dist/subagent/dispatcher.js +57 -35
  122. package/dist/subagent/dispatcher.js.map +1 -1
  123. package/dist/subagent/index.d.ts +1 -0
  124. package/dist/subagent/index.d.ts.map +1 -1
  125. package/dist/subagent/index.js.map +1 -1
  126. package/dist/subagent/orphan-reaper.d.ts +51 -4
  127. package/dist/subagent/orphan-reaper.d.ts.map +1 -1
  128. package/dist/subagent/orphan-reaper.js +103 -3
  129. package/dist/subagent/orphan-reaper.js.map +1 -1
  130. package/dist/subagent/types.d.ts +7 -0
  131. package/dist/subagent/types.d.ts.map +1 -1
  132. package/dist/subagent/workspace-resolver.d.ts +2 -0
  133. package/dist/subagent/workspace-resolver.d.ts.map +1 -1
  134. package/dist/subagent/workspace-resolver.js +3 -1
  135. package/dist/subagent/workspace-resolver.js.map +1 -1
  136. package/dist/vault/vault-entries.d.ts +18 -0
  137. package/dist/vault/vault-entries.d.ts.map +1 -1
  138. package/dist/vault/vault-entries.js +73 -0
  139. package/dist/vault/vault-entries.js.map +1 -1
  140. package/dist/vault/vault-manager.d.ts.map +1 -1
  141. package/dist/vault/vault-manager.js +1 -0
  142. package/dist/vault/vault-manager.js.map +1 -1
  143. package/dist/vault/vault-schema.d.ts.map +1 -1
  144. package/dist/vault/vault-schema.js +14 -0
  145. package/dist/vault/vault-schema.js.map +1 -1
  146. package/dist/vault/vault.d.ts +1 -0
  147. package/dist/vault/vault.d.ts.map +1 -1
  148. package/dist/vault/vault.js.map +1 -1
  149. package/package.json +3 -5
  150. package/src/__tests__/cron-manager.test.ts +132 -0
  151. package/src/__tests__/deviation-detection.test.ts +234 -0
  152. package/src/__tests__/embeddings.test.ts +536 -0
  153. package/src/__tests__/preflight.test.ts +97 -0
  154. package/src/__tests__/step-persistence.test.ts +324 -0
  155. package/src/__tests__/step-tracker.test.ts +260 -0
  156. package/src/__tests__/subagent/dispatcher.test.ts +122 -4
  157. package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
  158. package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
  159. package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
  160. package/src/adapters/types.ts +2 -0
  161. package/src/brain/brain.ts +117 -9
  162. package/src/dream/cron-manager.ts +137 -0
  163. package/src/dream/dream-engine.ts +119 -0
  164. package/src/dream/dream-ops.ts +56 -0
  165. package/src/dream/dream.test.ts +182 -0
  166. package/src/dream/index.ts +6 -0
  167. package/src/dream/schema.ts +17 -0
  168. package/src/embeddings/openai-provider.ts +158 -0
  169. package/src/embeddings/pipeline.ts +126 -0
  170. package/src/embeddings/types.ts +67 -0
  171. package/src/engine/bin/soleri-engine.ts +4 -1
  172. package/src/engine/module-manifest.test.ts +4 -4
  173. package/src/engine/module-manifest.ts +20 -0
  174. package/src/engine/register-engine.ts +12 -0
  175. package/src/flows/dispatch-registry.ts +44 -1
  176. package/src/flows/executor.ts +93 -2
  177. package/src/flows/index.ts +2 -0
  178. package/src/flows/types.ts +39 -1
  179. package/src/index.ts +12 -0
  180. package/src/persona/defaults.test.ts +39 -1
  181. package/src/persona/defaults.ts +65 -0
  182. package/src/planning/goal-ancestry.test.ts +3 -5
  183. package/src/planning/planner.test.ts +2 -3
  184. package/src/runtime/admin-ops.test.ts +2 -2
  185. package/src/runtime/admin-ops.ts +17 -0
  186. package/src/runtime/admin-setup-ops.ts +2 -2
  187. package/src/runtime/embedding-ops.ts +116 -0
  188. package/src/runtime/facades/admin-facade.test.ts +31 -0
  189. package/src/runtime/facades/embedding-facade.ts +11 -0
  190. package/src/runtime/facades/index.ts +12 -0
  191. package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
  192. package/src/runtime/facades/orchestrate-facade.ts +146 -0
  193. package/src/runtime/feature-flags.ts +4 -0
  194. package/src/runtime/orchestrate-ops.test.ts +131 -0
  195. package/src/runtime/orchestrate-ops.ts +158 -10
  196. package/src/runtime/planning-extra-ops.ts +77 -0
  197. package/src/runtime/preflight.ts +53 -0
  198. package/src/runtime/runtime.ts +41 -2
  199. package/src/runtime/types.ts +20 -0
  200. package/src/skills/__tests__/sync-skills.test.ts +132 -0
  201. package/src/skills/step-tracker.ts +162 -0
  202. package/src/skills/sync-skills.ts +54 -9
  203. package/src/subagent/dispatcher.ts +62 -39
  204. package/src/subagent/index.ts +1 -0
  205. package/src/subagent/orphan-reaper.test.ts +135 -0
  206. package/src/subagent/orphan-reaper.ts +130 -7
  207. package/src/subagent/types.ts +10 -0
  208. package/src/subagent/workspace-resolver.ts +3 -1
  209. package/src/vault/vault-entries.ts +112 -0
  210. package/src/vault/vault-manager.ts +1 -0
  211. package/src/vault/vault-scaling.test.ts +3 -2
  212. package/src/vault/vault-schema.ts +15 -0
  213. package/src/vault/vault.ts +1 -0
  214. package/vitest.config.ts +2 -1
  215. package/dist/brain/strength-scorer.d.ts +0 -31
  216. package/dist/brain/strength-scorer.d.ts.map +0 -1
  217. package/dist/brain/strength-scorer.js +0 -264
  218. package/dist/brain/strength-scorer.js.map +0 -1
  219. package/dist/engine/index.d.ts +0 -21
  220. package/dist/engine/index.d.ts.map +0 -1
  221. package/dist/engine/index.js +0 -18
  222. package/dist/engine/index.js.map +0 -1
  223. package/dist/hooks/index.d.ts +0 -2
  224. package/dist/hooks/index.d.ts.map +0 -1
  225. package/dist/hooks/index.js +0 -2
  226. package/dist/hooks/index.js.map +0 -1
  227. package/dist/persona/index.d.ts +0 -5
  228. package/dist/persona/index.d.ts.map +0 -1
  229. package/dist/persona/index.js +0 -4
  230. package/dist/persona/index.js.map +0 -1
  231. package/dist/vault/vault-interfaces.d.ts +0 -153
  232. package/dist/vault/vault-interfaces.d.ts.map +0 -1
  233. package/dist/vault/vault-interfaces.js +0 -2
  234. package/dist/vault/vault-interfaces.js.map +0 -1
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { OrphanReaper } from './orphan-reaper.js';
3
+
4
+ describe('OrphanReaper', () => {
5
+ describe('killProcessGroup', () => {
6
+ let reaper: OrphanReaper;
7
+ let killSpy: ReturnType<typeof vi.spyOn>;
8
+
9
+ beforeEach(() => {
10
+ reaper = new OrphanReaper();
11
+ killSpy = vi.spyOn(process, 'kill');
12
+ });
13
+
14
+ afterEach(() => {
15
+ killSpy.mockRestore();
16
+ });
17
+
18
+ it('kills the entire process group with -pid', () => {
19
+ killSpy.mockImplementation(() => true);
20
+
21
+ const result = reaper.killProcessGroup(1234);
22
+
23
+ expect(killSpy).toHaveBeenCalledWith(-1234, 'SIGTERM');
24
+ expect(result).toEqual({ killed: true, method: 'group' });
25
+ });
26
+
27
+ it('accepts a custom signal', () => {
28
+ killSpy.mockImplementation(() => true);
29
+
30
+ const result = reaper.killProcessGroup(1234, 'SIGKILL');
31
+
32
+ expect(killSpy).toHaveBeenCalledWith(-1234, 'SIGKILL');
33
+ expect(result).toEqual({ killed: true, method: 'group' });
34
+ });
35
+
36
+ it('falls back to single-process kill when group ESRCH', () => {
37
+ killSpy.mockImplementation((pid: number) => {
38
+ if (pid < 0) {
39
+ const err = new Error('No such process') as NodeJS.ErrnoException;
40
+ err.code = 'ESRCH';
41
+ throw err;
42
+ }
43
+ return true;
44
+ });
45
+
46
+ const result = reaper.killProcessGroup(1234);
47
+
48
+ expect(killSpy).toHaveBeenCalledWith(-1234, 'SIGTERM');
49
+ expect(killSpy).toHaveBeenCalledWith(1234, 'SIGTERM');
50
+ expect(result).toEqual({ killed: true, method: 'single' });
51
+ });
52
+
53
+ it('returns killed:false when both group and single fail', () => {
54
+ killSpy.mockImplementation(() => {
55
+ const err = new Error('No such process') as NodeJS.ErrnoException;
56
+ err.code = 'ESRCH';
57
+ throw err;
58
+ });
59
+
60
+ const result = reaper.killProcessGroup(1234);
61
+
62
+ expect(result).toEqual({ killed: false, method: 'single' });
63
+ });
64
+
65
+ it('returns killed:false on EPERM for group kill', () => {
66
+ killSpy.mockImplementation(() => {
67
+ const err = new Error('Operation not permitted') as NodeJS.ErrnoException;
68
+ err.code = 'EPERM';
69
+ throw err;
70
+ });
71
+
72
+ const result = reaper.killProcessGroup(1234);
73
+
74
+ expect(result).toEqual({ killed: false, method: 'group' });
75
+ });
76
+ });
77
+
78
+ describe('killAll', () => {
79
+ let reaper: OrphanReaper;
80
+ let killSpy: ReturnType<typeof vi.spyOn>;
81
+
82
+ beforeEach(() => {
83
+ reaper = new OrphanReaper();
84
+ killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
85
+ });
86
+
87
+ afterEach(() => {
88
+ killSpy.mockRestore();
89
+ });
90
+
91
+ it('kills all tracked processes and clears tracking', () => {
92
+ reaper.register(100, 'task-a');
93
+ reaper.register(200, 'task-b');
94
+
95
+ const results = reaper.killAll();
96
+
97
+ expect(results.size).toBe(2);
98
+ expect(results.get(100)).toEqual({ killed: true, method: 'group' });
99
+ expect(results.get(200)).toEqual({ killed: true, method: 'group' });
100
+ // Tracking is cleared
101
+ expect(reaper.listTracked()).toEqual([]);
102
+ });
103
+
104
+ it('returns empty map when nothing is tracked', () => {
105
+ const results = reaper.killAll();
106
+ expect(results.size).toBe(0);
107
+ });
108
+
109
+ it('uses custom signal for all kills', () => {
110
+ reaper.register(100, 'task-a');
111
+
112
+ reaper.killAll('SIGKILL');
113
+
114
+ expect(killSpy).toHaveBeenCalledWith(-100, 'SIGKILL');
115
+ });
116
+
117
+ it('handles mixed success/failure', () => {
118
+ reaper.register(100, 'task-a');
119
+ reaper.register(200, 'task-b');
120
+
121
+ killSpy.mockImplementation((pid: number) => {
122
+ // PID 100 group kill works, PID 200 is already dead
123
+ if (pid === -100) return true;
124
+ const err = new Error('No such process') as NodeJS.ErrnoException;
125
+ err.code = 'ESRCH';
126
+ throw err;
127
+ });
128
+
129
+ const results = reaper.killAll();
130
+
131
+ expect(results.get(100)).toEqual({ killed: true, method: 'group' });
132
+ expect(results.get(200)).toEqual({ killed: false, method: 'single' });
133
+ });
134
+ });
135
+ });
@@ -5,9 +5,34 @@
5
5
  * - No error → process is alive
6
6
  * - ESRCH → process is dead (reap it)
7
7
  * - EPERM → process is alive but we lack permission to signal it
8
+ *
9
+ * Process group management:
10
+ * - `killProcessGroup()` sends a signal to the entire process group (-pid)
11
+ * - `killAll()` kills all tracked processes via process groups
12
+ * - Group kill only works if the child was spawned with `detached: true`
13
+ * or is otherwise a process group leader. Falls back to single-process
14
+ * kill when the group doesn't exist (ESRCH on -pid).
8
15
  */
9
16
 
10
- import type { TrackedProcess } from './types.js';
17
+ import type { TrackedProcess, KillResult } from './types.js';
18
+
19
+ const KILL_GRACE_PERIOD_MS = 5_000;
20
+
21
+ /** Result of a reap cycle. */
22
+ export interface ReapResult {
23
+ /** Task IDs of processes that were found dead and cleaned up. */
24
+ reaped: string[];
25
+ /** Task IDs of processes that are still alive. */
26
+ alive: string[];
27
+ }
28
+
29
+ /** Result of a process group kill attempt */
30
+ export interface ProcessGroupKillResult {
31
+ /** Whether the kill signal was delivered */
32
+ killed: boolean;
33
+ /** Whether the group or single-process kill path was used */
34
+ method: 'group' | 'single';
35
+ }
11
36
 
12
37
  export class OrphanReaper {
13
38
  private readonly tracked = new Map<number, TrackedProcess>();
@@ -29,20 +54,68 @@ export class OrphanReaper {
29
54
 
30
55
  /**
31
56
  * Check each tracked PID for liveness. Dead processes are removed from
32
- * tracking, the onOrphan callback is invoked, and they are returned.
57
+ * tracking, the onOrphan callback is invoked, and the result summarises
58
+ * which task IDs were reaped vs still alive.
33
59
  */
34
- reap(): TrackedProcess[] {
35
- const reaped: TrackedProcess[] = [];
60
+ reap(): ReapResult {
61
+ const reaped: string[] = [];
62
+ const alive: string[] = [];
36
63
 
37
64
  for (const [pid, entry] of this.tracked) {
38
65
  if (!this.isAlive(pid)) {
39
66
  this.tracked.delete(pid);
40
67
  this.onOrphan?.(entry.taskId, pid);
41
- reaped.push(entry);
68
+ reaped.push(entry.taskId);
69
+ } else {
70
+ alive.push(entry.taskId);
42
71
  }
43
72
  }
44
73
 
45
- return reaped;
74
+ return { reaped, alive };
75
+ }
76
+
77
+ /**
78
+ * Kill a process by PID. Sends SIGTERM first; if `escalate` is true
79
+ * (default), waits 5 seconds and sends SIGKILL if still alive.
80
+ *
81
+ * Handles gracefully: process already dead, EPERM, etc.
82
+ */
83
+ async killProcess(pid: number, escalate = true): Promise<KillResult> {
84
+ // If already dead, report as killed via SIGTERM (no-op)
85
+ if (!this.isAlive(pid)) {
86
+ return { killed: true, signal: 'SIGTERM' };
87
+ }
88
+
89
+ // Send SIGTERM
90
+ try {
91
+ process.kill(pid, 'SIGTERM');
92
+ } catch {
93
+ // ESRCH = already dead, EPERM = can't signal
94
+ return { killed: !this.isAlive(pid), signal: 'SIGTERM' };
95
+ }
96
+
97
+ // If not escalating, check once and return
98
+ if (!escalate) {
99
+ // Give a brief moment for the signal to take effect
100
+ return { killed: !this.isAlive(pid), signal: 'SIGTERM' };
101
+ }
102
+
103
+ // Wait grace period, then check if still alive
104
+ await new Promise<void>((resolve) => setTimeout(resolve, KILL_GRACE_PERIOD_MS));
105
+
106
+ if (!this.isAlive(pid)) {
107
+ return { killed: true, signal: 'SIGTERM' };
108
+ }
109
+
110
+ // Escalate to SIGKILL
111
+ try {
112
+ process.kill(pid, 'SIGKILL');
113
+ } catch {
114
+ // Process may have died between check and kill
115
+ return { killed: !this.isAlive(pid), signal: 'SIGKILL' };
116
+ }
117
+
118
+ return { killed: true, signal: 'SIGKILL' };
46
119
  }
47
120
 
48
121
  /** Return all currently tracked processes. */
@@ -60,6 +133,56 @@ export class OrphanReaper {
60
133
  this.tracked.clear();
61
134
  }
62
135
 
136
+ /**
137
+ * Kill an entire process group by negating the PID.
138
+ *
139
+ * Attempts `process.kill(-pid, signal)` first to kill the whole group.
140
+ * Falls back to `process.kill(pid, signal)` if the group kill fails
141
+ * (e.g., ESRCH when the process isn't a group leader).
142
+ *
143
+ * **Limitation:** Group kill only works if the child was spawned with
144
+ * `detached: true` or is otherwise a process group leader. On macOS,
145
+ * `process.kill(-pid)` works for process group leaders.
146
+ */
147
+ killProcessGroup(pid: number, signal: NodeJS.Signals = 'SIGTERM'): ProcessGroupKillResult {
148
+ // Try group kill first
149
+ try {
150
+ process.kill(-pid, signal);
151
+ return { killed: true, method: 'group' };
152
+ } catch (groupErr: unknown) {
153
+ const groupCode = (groupErr as NodeJS.ErrnoException).code;
154
+ // ESRCH on the group means it's not a group leader — fall back to single
155
+ if (groupCode === 'ESRCH') {
156
+ try {
157
+ process.kill(pid, signal);
158
+ return { killed: true, method: 'single' };
159
+ } catch {
160
+ // Process is already dead
161
+ return { killed: false, method: 'single' };
162
+ }
163
+ }
164
+ // EPERM means the process group exists but we can't signal it
165
+ // Any other error — process is dead or inaccessible
166
+ return { killed: false, method: 'group' };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Kill all tracked processes via process groups and clear tracking.
172
+ *
173
+ * Returns a summary of kill results keyed by PID.
174
+ */
175
+ killAll(signal: NodeJS.Signals = 'SIGTERM'): Map<number, ProcessGroupKillResult> {
176
+ const results = new Map<number, ProcessGroupKillResult>();
177
+
178
+ for (const [pid] of this.tracked) {
179
+ results.set(pid, this.killProcessGroup(pid, signal));
180
+ }
181
+
182
+ this.tracked.clear();
183
+ return results;
184
+ }
185
+
63
186
  // ── internals ──────────────────────────────────────────────────────
64
187
 
65
188
  /**
@@ -68,7 +191,7 @@ export class OrphanReaper {
68
191
  * - EPERM → alive (exists but we can't signal it)
69
192
  * - ESRCH → dead
70
193
  */
71
- private isAlive(pid: number): boolean {
194
+ isAlive(pid: number): boolean {
72
195
  try {
73
196
  process.kill(pid, 0);
74
197
  return true;
@@ -136,3 +136,13 @@ export interface TrackedProcess {
136
136
  /** When the process was registered */
137
137
  registeredAt: number;
138
138
  }
139
+
140
+ // ─── Kill Result ───────────────────────────────────────────────────
141
+
142
+ /** Result of attempting to kill a process */
143
+ export interface KillResult {
144
+ /** Whether the process was successfully killed */
145
+ killed: boolean;
146
+ /** Which signal ultimately killed the process */
147
+ signal: 'SIGTERM' | 'SIGKILL';
148
+ }
@@ -72,6 +72,8 @@ export class WorkspaceResolver {
72
72
 
73
73
  /**
74
74
  * Remove the worktree for a given task.
75
+ * Deletes the worktree directory and local branch.
76
+ * Worktree branches are local-only — never pushed to remote.
75
77
  * Silently handles errors (e.g., worktree already removed).
76
78
  */
77
79
  cleanup(taskId: string): void {
@@ -86,7 +88,7 @@ export class WorkspaceResolver {
86
88
  // Silently ignore — worktree may already be gone
87
89
  }
88
90
 
89
- // Clean up the branch as well
91
+ // Clean up the local branch (worktree branches are local-only)
90
92
  if (info.branch) {
91
93
  try {
92
94
  execSync(`git branch -D "${info.branch}"`, { ...EXEC_OPTS, cwd: this.baseDir });
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import type { PersistenceProvider } from '../persistence/types.js';
6
6
  import type { IntelligenceEntry } from '../intelligence/types.js';
7
+ import type { StoredVector } from '../embeddings/types.js';
7
8
  import type { LinkManager } from './linking.js';
8
9
  import { computeContentHash } from './content-hash.js';
9
10
  import type { SearchResult, VaultStats } from './vault.js';
@@ -377,6 +378,117 @@ export function contentHashStats(provider: PersistenceProvider): {
377
378
  return { total, hashed, uniqueHashes };
378
379
  }
379
380
 
381
+ // ── Vector Operations ────────────────────────────────────────────────────
382
+
383
+ /** Store a vector for an entry. Upserts — replaces if exists. */
384
+ export function storeVector(
385
+ provider: PersistenceProvider,
386
+ entryId: string,
387
+ vector: number[],
388
+ model: string,
389
+ dimensions: number,
390
+ ): void {
391
+ const blob = Buffer.from(new Float32Array(vector).buffer);
392
+ provider.run(
393
+ `INSERT INTO entry_vectors (entry_id, vector, model, dimensions, created_at)
394
+ VALUES (@entryId, @vector, @model, @dimensions, @createdAt)
395
+ ON CONFLICT(entry_id) DO UPDATE SET
396
+ vector = excluded.vector, model = excluded.model,
397
+ dimensions = excluded.dimensions, created_at = excluded.created_at`,
398
+ {
399
+ entryId,
400
+ vector: blob,
401
+ model,
402
+ dimensions,
403
+ createdAt: Date.now(),
404
+ },
405
+ );
406
+ }
407
+
408
+ /** Get the stored vector for an entry, or null. */
409
+ export function getVector(provider: PersistenceProvider, entryId: string): StoredVector | null {
410
+ const row = provider.get<{
411
+ entry_id: string;
412
+ vector: Buffer;
413
+ model: string;
414
+ dimensions: number;
415
+ created_at: number;
416
+ }>('SELECT * FROM entry_vectors WHERE entry_id = ?', [entryId]);
417
+ if (!row) return null;
418
+ return {
419
+ entryId: row.entry_id,
420
+ vector: Array.from(
421
+ new Float32Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength / 4),
422
+ ),
423
+ model: row.model,
424
+ dimensions: row.dimensions,
425
+ createdAt: row.created_at,
426
+ };
427
+ }
428
+
429
+ /** Delete the vector for an entry. */
430
+ export function deleteVector(provider: PersistenceProvider, entryId: string): void {
431
+ provider.run('DELETE FROM entry_vectors WHERE entry_id = ?', [entryId]);
432
+ }
433
+
434
+ /** Get IDs of entries that have no vector for the given model. */
435
+ export function getEntriesWithoutVectors(provider: PersistenceProvider, model: string): string[] {
436
+ const rows = provider.all<{ id: string }>(
437
+ `SELECT e.id FROM entries e
438
+ LEFT JOIN entry_vectors ev ON e.id = ev.entry_id AND ev.model = @model
439
+ WHERE ev.entry_id IS NULL`,
440
+ { model },
441
+ );
442
+ return rows.map((r) => r.id);
443
+ }
444
+
445
+ /**
446
+ * Brute-force cosine similarity search over all stored vectors.
447
+ * Returns top-K entries sorted by similarity descending.
448
+ * For <100K entries, brute-force is fast enough (~50ms).
449
+ */
450
+ export function cosineSearch(
451
+ provider: PersistenceProvider,
452
+ queryVector: number[],
453
+ topK: number,
454
+ ): Array<{ entryId: string; similarity: number }> {
455
+ const rows = provider.all<{
456
+ entry_id: string;
457
+ vector: Buffer;
458
+ }>('SELECT entry_id, vector FROM entry_vectors');
459
+
460
+ // Precompute query norm
461
+ let queryNorm = 0;
462
+ for (let i = 0; i < queryVector.length; i++) {
463
+ queryNorm += queryVector[i] * queryVector[i];
464
+ }
465
+ queryNorm = Math.sqrt(queryNorm);
466
+ if (queryNorm === 0) return [];
467
+
468
+ const results: Array<{ entryId: string; similarity: number }> = [];
469
+
470
+ for (const row of rows) {
471
+ const stored = new Float32Array(
472
+ row.vector.buffer,
473
+ row.vector.byteOffset,
474
+ row.vector.byteLength / 4,
475
+ );
476
+ let dot = 0;
477
+ let storedNorm = 0;
478
+ for (let i = 0; i < stored.length; i++) {
479
+ dot += queryVector[i] * stored[i];
480
+ storedNorm += stored[i] * stored[i];
481
+ }
482
+ storedNorm = Math.sqrt(storedNorm);
483
+ if (storedNorm === 0) continue;
484
+ const similarity = dot / (queryNorm * storedNorm);
485
+ results.push({ entryId: row.entry_id, similarity });
486
+ }
487
+
488
+ results.sort((a, b) => b.similarity - a.similarity);
489
+ return results.slice(0, topK);
490
+ }
491
+
380
492
  // ── Helpers ──────────────────────────────────────────────────────────────
381
493
 
382
494
  function gc(provider: PersistenceProvider, col: string): Record<string, number> {
@@ -188,6 +188,7 @@ export class VaultManager {
188
188
  return merged.slice(0, limit).map((r) => ({
189
189
  entry: r.entry,
190
190
  score: r.weightedScore,
191
+ source: r.source,
191
192
  }));
192
193
  }
193
194
 
@@ -11,6 +11,7 @@ import { Brain } from '../brain/brain.js';
11
11
  import type { IntelligenceEntry } from '../intelligence/types.js';
12
12
 
13
13
  const isCI = !!process.env.CI;
14
+ const isWindows = process.platform === 'win32';
14
15
  const DOMAINS = ['design', 'a11y', 'performance', 'security', 'architecture', 'testing', 'ux'];
15
16
  const TYPES: IntelligenceEntry['type'][] = ['pattern', 'anti-pattern', 'rule', 'playbook'];
16
17
  const SEVERITIES: IntelligenceEntry['severity'][] = ['critical', 'warning', 'suggestion'];
@@ -77,7 +78,7 @@ describe('Vault Scaling — 10K entries', () => {
77
78
  const elapsed = performance.now() - start;
78
79
 
79
80
  expect(results.length).toBeGreaterThan(0);
80
- expect(elapsed).toBeLessThan(isCI ? 500 : 50);
81
+ expect(elapsed).toBeLessThan(isCI ? (isWindows ? 2000 : 500) : 50);
81
82
  }
82
83
  });
83
84
 
@@ -90,7 +91,7 @@ describe('Vault Scaling — 10K entries', () => {
90
91
  const elapsed = performance.now() - start;
91
92
 
92
93
  expect(results.length).toBeGreaterThan(0);
93
- expect(elapsed).toBeLessThan(isCI ? 500 : 50);
94
+ expect(elapsed).toBeLessThan(isCI ? (isWindows ? 2000 : 1000) : 200);
94
95
  });
95
96
 
96
97
  test('list with filters under 200ms at 10K', () => {
@@ -28,6 +28,7 @@ export function initializeSchema(provider: PersistenceProvider): void {
28
28
  migrateContentHash(provider);
29
29
  migrateTierColumn(provider);
30
30
  migratePerformanceIndexes(provider);
31
+ migrateVectorStorage(provider);
31
32
  }
32
33
 
33
34
  function createCoreTables(provider: PersistenceProvider): void {
@@ -257,3 +258,17 @@ export function migratePerformanceIndexes(provider: PersistenceProvider): void {
257
258
  /* brain_sessions table doesn't exist yet — indexes will be created on next init */
258
259
  }
259
260
  }
261
+
262
+ function migrateVectorStorage(provider: PersistenceProvider): void {
263
+ provider.execSql(`
264
+ CREATE TABLE IF NOT EXISTS entry_vectors (
265
+ entry_id TEXT PRIMARY KEY,
266
+ vector BLOB NOT NULL,
267
+ model TEXT NOT NULL,
268
+ dimensions INTEGER NOT NULL,
269
+ created_at INTEGER NOT NULL,
270
+ FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
271
+ );
272
+ `);
273
+ provider.execSql('CREATE INDEX IF NOT EXISTS idx_entry_vectors_model ON entry_vectors(model)');
274
+ }
@@ -11,6 +11,7 @@ import type { AutoLinkConfig, EntryUpdateFields } from './vault-entries.js';
11
11
  export interface SearchResult {
12
12
  entry: IntelligenceEntry;
13
13
  score: number;
14
+ source?: string;
14
15
  }
15
16
  export interface VaultStats {
16
17
  totalEntries: number;
package/vitest.config.ts CHANGED
@@ -6,7 +6,8 @@ export default defineConfig({
6
6
  pool: 'forks',
7
7
  poolOptions: { forks: { singleFork: true } },
8
8
  testTimeout: 30_000,
9
- exclude: ['**/node_modules/**', '**/.claude/worktrees/**'],
9
+ include: ['src/**/*.test.ts'],
10
+ exclude: ['**/node_modules/**'],
10
11
  coverage: {
11
12
  provider: 'v8',
12
13
  include: ['src/**/*.ts'],
@@ -1,31 +0,0 @@
1
- /**
2
- * Strength Scorer — computes pattern strength scores from brain feedback data.
3
- *
4
- * Extracted from intelligence.ts (Phase 1, Wave 1A).
5
- * Responsible for: computeStrengths, getStrengths, recommend, buildGlobalRegistry, buildDomainProfiles.
6
- */
7
- import type { Vault } from '../vault/vault.js';
8
- import type { PatternStrength, StrengthsQuery, GlobalPattern, DomainProfile } from './types.js';
9
- export declare class StrengthScorer {
10
- private vault;
11
- private provider;
12
- constructor(vault: Vault);
13
- computeStrengths(): PatternStrength[];
14
- getStrengths(query?: StrengthsQuery): PatternStrength[];
15
- recommend(context: {
16
- domain?: string;
17
- task?: string;
18
- source?: string;
19
- limit?: number;
20
- }): PatternStrength[];
21
- buildGlobalRegistry(strengths: PatternStrength[]): number;
22
- buildDomainProfiles(strengths: PatternStrength[]): number;
23
- getGlobalPatterns(limit?: number): GlobalPattern[];
24
- getDomainProfile(domain: string): DomainProfile | null;
25
- private scoreFromFeedback;
26
- private persistStrength;
27
- private expandWithFallback;
28
- private boostByTaskContext;
29
- private boostBySource;
30
- }
31
- //# sourceMappingURL=strength-scorer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"strength-scorer.d.ts","sourceRoot":"","sources":["../../src/brain/strength-scorer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA2EhG,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAsB;gBAE1B,KAAK,EAAE,KAAK;IAKxB,gBAAgB,IAAI,eAAe,EAAE;IA8BrC,YAAY,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG,eAAe,EAAE;IAyBvD,SAAS,CAAC,OAAO,EAAE;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,eAAe,EAAE;IAyBrB,mBAAmB,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,MAAM;IA4BzD,mBAAmB,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,MAAM;IAuDzD,iBAAiB,CAAC,KAAK,SAAK,GAAG,aAAa,EAAE;IAkB9C,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAsBtD,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,eAAe;IAsBvB,OAAO,CAAC,kBAAkB;IAiB1B,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,aAAa;CAqBtB"}