@soleri/core 9.5.0 → 9.6.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 (192) hide show
  1. package/dist/adapters/claude-code-adapter.d.ts +27 -0
  2. package/dist/adapters/claude-code-adapter.d.ts.map +1 -0
  3. package/dist/adapters/claude-code-adapter.js +111 -0
  4. package/dist/adapters/claude-code-adapter.js.map +1 -0
  5. package/dist/adapters/index.d.ts +9 -0
  6. package/dist/adapters/index.d.ts.map +1 -0
  7. package/dist/adapters/index.js +10 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/registry.d.ts +21 -0
  10. package/dist/adapters/registry.d.ts.map +1 -0
  11. package/dist/adapters/registry.js +44 -0
  12. package/dist/adapters/registry.js.map +1 -0
  13. package/dist/adapters/types.d.ts +93 -0
  14. package/dist/adapters/types.d.ts.map +1 -0
  15. package/dist/adapters/types.js +10 -0
  16. package/dist/adapters/types.js.map +1 -0
  17. package/dist/brain/brain.d.ts +12 -1
  18. package/dist/brain/brain.d.ts.map +1 -1
  19. package/dist/brain/brain.js +106 -44
  20. package/dist/brain/brain.js.map +1 -1
  21. package/dist/brain/intelligence.d.ts.map +1 -1
  22. package/dist/brain/intelligence.js +36 -30
  23. package/dist/brain/intelligence.js.map +1 -1
  24. package/dist/chat/agent-loop.js +1 -1
  25. package/dist/chat/agent-loop.js.map +1 -1
  26. package/dist/chat/notifications.d.ts.map +1 -1
  27. package/dist/chat/notifications.js +4 -0
  28. package/dist/chat/notifications.js.map +1 -1
  29. package/dist/control/intent-router.d.ts +1 -0
  30. package/dist/control/intent-router.d.ts.map +1 -1
  31. package/dist/control/intent-router.js +11 -5
  32. package/dist/control/intent-router.js.map +1 -1
  33. package/dist/curator/curator.d.ts +4 -0
  34. package/dist/curator/curator.d.ts.map +1 -1
  35. package/dist/curator/curator.js +141 -27
  36. package/dist/curator/curator.js.map +1 -1
  37. package/dist/index.d.ts +14 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +12 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/llm/llm-client.d.ts.map +1 -1
  42. package/dist/llm/llm-client.js +1 -0
  43. package/dist/llm/llm-client.js.map +1 -1
  44. package/dist/packs/index.d.ts +3 -2
  45. package/dist/packs/index.d.ts.map +1 -1
  46. package/dist/packs/index.js +3 -2
  47. package/dist/packs/index.js.map +1 -1
  48. package/dist/packs/lockfile.d.ts +23 -1
  49. package/dist/packs/lockfile.d.ts.map +1 -1
  50. package/dist/packs/lockfile.js +50 -4
  51. package/dist/packs/lockfile.js.map +1 -1
  52. package/dist/packs/pack-installer.d.ts +10 -0
  53. package/dist/packs/pack-installer.d.ts.map +1 -1
  54. package/dist/packs/pack-installer.js +69 -2
  55. package/dist/packs/pack-installer.js.map +1 -1
  56. package/dist/packs/pack-lifecycle.d.ts +50 -0
  57. package/dist/packs/pack-lifecycle.d.ts.map +1 -0
  58. package/dist/packs/pack-lifecycle.js +91 -0
  59. package/dist/packs/pack-lifecycle.js.map +1 -0
  60. package/dist/packs/types.d.ts +64 -44
  61. package/dist/packs/types.d.ts.map +1 -1
  62. package/dist/packs/types.js +9 -0
  63. package/dist/packs/types.js.map +1 -1
  64. package/dist/persistence/sqlite-provider.d.ts +5 -1
  65. package/dist/persistence/sqlite-provider.d.ts.map +1 -1
  66. package/dist/persistence/sqlite-provider.js +22 -2
  67. package/dist/persistence/sqlite-provider.js.map +1 -1
  68. package/dist/planning/github-projection.d.ts +8 -8
  69. package/dist/planning/github-projection.d.ts.map +1 -1
  70. package/dist/planning/github-projection.js +42 -42
  71. package/dist/planning/github-projection.js.map +1 -1
  72. package/dist/plugins/types.d.ts +21 -21
  73. package/dist/queue/pipeline-runner.d.ts.map +1 -1
  74. package/dist/queue/pipeline-runner.js +4 -0
  75. package/dist/queue/pipeline-runner.js.map +1 -1
  76. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  77. package/dist/runtime/curator-extra-ops.js +9 -1
  78. package/dist/runtime/curator-extra-ops.js.map +1 -1
  79. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  80. package/dist/runtime/facades/memory-facade.js +169 -0
  81. package/dist/runtime/facades/memory-facade.js.map +1 -1
  82. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  83. package/dist/runtime/orchestrate-ops.js +133 -4
  84. package/dist/runtime/orchestrate-ops.js.map +1 -1
  85. package/dist/runtime/runtime.d.ts.map +1 -1
  86. package/dist/runtime/runtime.js +128 -90
  87. package/dist/runtime/runtime.js.map +1 -1
  88. package/dist/runtime/session-briefing.d.ts.map +1 -1
  89. package/dist/runtime/session-briefing.js +44 -11
  90. package/dist/runtime/session-briefing.js.map +1 -1
  91. package/dist/runtime/shutdown-registry.d.ts +36 -0
  92. package/dist/runtime/shutdown-registry.d.ts.map +1 -0
  93. package/dist/runtime/shutdown-registry.js +74 -0
  94. package/dist/runtime/shutdown-registry.js.map +1 -0
  95. package/dist/runtime/types.d.ts +10 -1
  96. package/dist/runtime/types.d.ts.map +1 -1
  97. package/dist/subagent/concurrency-manager.d.ts +29 -0
  98. package/dist/subagent/concurrency-manager.d.ts.map +1 -0
  99. package/dist/subagent/concurrency-manager.js +73 -0
  100. package/dist/subagent/concurrency-manager.js.map +1 -0
  101. package/dist/subagent/dispatcher.d.ts +41 -0
  102. package/dist/subagent/dispatcher.d.ts.map +1 -0
  103. package/dist/subagent/dispatcher.js +259 -0
  104. package/dist/subagent/dispatcher.js.map +1 -0
  105. package/dist/subagent/index.d.ts +14 -0
  106. package/dist/subagent/index.d.ts.map +1 -0
  107. package/dist/subagent/index.js +15 -0
  108. package/dist/subagent/index.js.map +1 -0
  109. package/dist/subagent/orphan-reaper.d.ts +37 -0
  110. package/dist/subagent/orphan-reaper.d.ts.map +1 -0
  111. package/dist/subagent/orphan-reaper.js +71 -0
  112. package/dist/subagent/orphan-reaper.js.map +1 -0
  113. package/dist/subagent/result-aggregator.d.ts +7 -0
  114. package/dist/subagent/result-aggregator.d.ts.map +1 -0
  115. package/dist/subagent/result-aggregator.js +57 -0
  116. package/dist/subagent/result-aggregator.js.map +1 -0
  117. package/dist/subagent/task-checkout.d.ts +36 -0
  118. package/dist/subagent/task-checkout.d.ts.map +1 -0
  119. package/dist/subagent/task-checkout.js +52 -0
  120. package/dist/subagent/task-checkout.js.map +1 -0
  121. package/dist/subagent/types.d.ts +114 -0
  122. package/dist/subagent/types.d.ts.map +1 -0
  123. package/dist/subagent/types.js +9 -0
  124. package/dist/subagent/types.js.map +1 -0
  125. package/dist/subagent/workspace-resolver.d.ts +35 -0
  126. package/dist/subagent/workspace-resolver.d.ts.map +1 -0
  127. package/dist/subagent/workspace-resolver.js +99 -0
  128. package/dist/subagent/workspace-resolver.js.map +1 -0
  129. package/dist/transport/http-server.d.ts.map +1 -1
  130. package/dist/transport/http-server.js +49 -3
  131. package/dist/transport/http-server.js.map +1 -1
  132. package/dist/transport/ws-server.d.ts.map +1 -1
  133. package/dist/transport/ws-server.js +7 -0
  134. package/dist/transport/ws-server.js.map +1 -1
  135. package/dist/vault/linking.d.ts +3 -4
  136. package/dist/vault/linking.d.ts.map +1 -1
  137. package/dist/vault/linking.js +79 -32
  138. package/dist/vault/linking.js.map +1 -1
  139. package/dist/vault/vault-maintenance.d.ts.map +1 -1
  140. package/dist/vault/vault-maintenance.js +7 -14
  141. package/dist/vault/vault-maintenance.js.map +1 -1
  142. package/dist/vault/vault-memories.d.ts.map +1 -1
  143. package/dist/vault/vault-memories.js +19 -9
  144. package/dist/vault/vault-memories.js.map +1 -1
  145. package/dist/vault/vault-schema.d.ts +1 -0
  146. package/dist/vault/vault-schema.d.ts.map +1 -1
  147. package/dist/vault/vault-schema.js +20 -0
  148. package/dist/vault/vault-schema.js.map +1 -1
  149. package/dist/vault/vault.d.ts.map +1 -1
  150. package/dist/vault/vault.js +7 -3
  151. package/dist/vault/vault.js.map +1 -1
  152. package/package.json +8 -2
  153. package/src/__tests__/adapters/claude-code-adapter.test.ts +167 -0
  154. package/src/__tests__/adapters/registry.test.ts +100 -0
  155. package/src/__tests__/packs/pack-lifecycle.test.ts +379 -0
  156. package/src/__tests__/subagent/concurrency-manager.test.ts +132 -0
  157. package/src/__tests__/subagent/dispatcher.test.ts +195 -0
  158. package/src/__tests__/subagent/orphan-reaper.test.ts +141 -0
  159. package/src/__tests__/subagent/result-aggregator.test.ts +141 -0
  160. package/src/__tests__/subagent/task-checkout.test.ts +86 -0
  161. package/src/__tests__/subagent/workspace-resolver.test.ts +138 -0
  162. package/src/adapters/claude-code-adapter.ts +163 -0
  163. package/src/adapters/index.ts +22 -0
  164. package/src/adapters/registry.ts +53 -0
  165. package/src/adapters/types.ts +114 -0
  166. package/src/curator/curator.ts +1 -0
  167. package/src/index.ts +38 -1
  168. package/src/packs/index.ts +5 -1
  169. package/src/packs/lockfile.ts +70 -5
  170. package/src/packs/pack-installer.ts +78 -2
  171. package/src/packs/pack-lifecycle.ts +115 -0
  172. package/src/packs/pack-lockfile.test.ts +1 -1
  173. package/src/packs/pack-system.test.ts +1 -1
  174. package/src/packs/types.ts +40 -2
  175. package/src/persistence/sqlite-provider.ts +26 -2
  176. package/src/runtime/admin-setup-ops.test.ts +9 -4
  177. package/src/runtime/orchestrate-ops.ts +153 -1
  178. package/src/runtime/runtime.ts +15 -0
  179. package/src/runtime/session-briefing.test.ts +94 -2
  180. package/src/runtime/session-briefing.ts +48 -12
  181. package/src/runtime/types.ts +6 -0
  182. package/src/subagent/concurrency-manager.ts +89 -0
  183. package/src/subagent/dispatcher.ts +326 -0
  184. package/src/subagent/index.ts +28 -0
  185. package/src/subagent/orphan-reaper.ts +82 -0
  186. package/src/subagent/result-aggregator.ts +66 -0
  187. package/src/subagent/task-checkout.ts +60 -0
  188. package/src/subagent/types.ts +138 -0
  189. package/src/subagent/workspace-resolver.ts +117 -0
  190. package/src/vault/vault-scaling.test.ts +3 -2
  191. package/vitest.config.ts +2 -0
  192. package/src/hooks/index.ts +0 -6
@@ -22,6 +22,7 @@ import type { Vault } from '../vault/vault.js';
22
22
  import type { PluginRegistry } from '../plugins/plugin-registry.js';
23
23
  import type { PluginContext } from '../plugins/types.js';
24
24
  import type { PackRuntime } from '../domain-packs/pack-runtime.js';
25
+ import { PackLifecycleManager } from './pack-lifecycle.js';
25
26
 
26
27
  const MANIFEST_FILENAME = 'soleri-pack.json';
27
28
 
@@ -31,6 +32,7 @@ const MANIFEST_FILENAME = 'soleri-pack.json';
31
32
 
32
33
  export class PackInstaller {
33
34
  private packs = new Map<string, InstalledPack>();
35
+ readonly lifecycle = new PackLifecycleManager();
34
36
 
35
37
  constructor(
36
38
  private vault: Vault,
@@ -206,17 +208,21 @@ export class PackInstaller {
206
208
  const hooksDir = join(packDir, manifest.hooks?.dir ?? 'hooks');
207
209
  const hooks = existsSync(hooksDir) ? listMarkdownFiles(hooksDir) : [];
208
210
 
209
- // Track installed pack
211
+ // Track installed pack with lifecycle
212
+ this.lifecycle.initState(manifest.id, 'installed');
213
+ this.lifecycle.transition(manifest.id, 'ready', 'Initial install');
214
+
210
215
  const installed: InstalledPack = {
211
216
  id: manifest.id,
212
217
  manifest,
213
218
  directory: packDir,
214
- status: 'installed',
219
+ status: 'ready',
215
220
  vaultEntries,
216
221
  skills,
217
222
  hooks,
218
223
  facadesRegistered,
219
224
  installedAt: Date.now(),
225
+ transitions: this.lifecycle.getTransitions(manifest.id),
220
226
  };
221
227
  this.packs.set(manifest.id, installed);
222
228
 
@@ -231,17 +237,23 @@ export class PackInstaller {
231
237
  } catch (e) {
232
238
  const error = e instanceof Error ? e.message : String(e);
233
239
 
240
+ // Transition to error state
241
+ this.lifecycle.initState(manifest.id, 'installed');
242
+ this.lifecycle.transition(manifest.id, 'error', error);
243
+
234
244
  this.packs.set(manifest.id, {
235
245
  id: manifest.id,
236
246
  manifest,
237
247
  directory: packDir,
238
248
  status: 'error',
239
249
  error,
250
+ errorMessage: error,
240
251
  vaultEntries: 0,
241
252
  skills: [],
242
253
  hooks: [],
243
254
  facadesRegistered: false,
244
255
  installedAt: Date.now(),
256
+ transitions: this.lifecycle.getTransitions(manifest.id),
245
257
  });
246
258
 
247
259
  return {
@@ -269,11 +281,75 @@ export class PackInstaller {
269
281
  this.pluginRegistry.deactivate(packId);
270
282
  }
271
283
 
284
+ // Transition to uninstalled
285
+ try {
286
+ this.lifecycle.transition(packId, 'uninstalled', 'User uninstall');
287
+ } catch {
288
+ // May not be tracked in lifecycle — continue anyway
289
+ }
290
+ this.lifecycle.remove(packId);
291
+
272
292
  pack.status = 'uninstalled';
273
293
  this.packs.delete(packId);
274
294
  return true;
275
295
  }
276
296
 
297
+ /**
298
+ * Disable a pack — deactivates capabilities but preserves vault entries.
299
+ */
300
+ disable(packId: string): boolean {
301
+ const pack = this.packs.get(packId);
302
+ if (!pack) return false;
303
+
304
+ this.lifecycle.transition(packId, 'disabled', 'User disabled');
305
+
306
+ // Deactivate facades
307
+ if (pack.facadesRegistered) {
308
+ this.pluginRegistry.deactivate(packId);
309
+ }
310
+
311
+ pack.status = 'disabled';
312
+ pack.disabledAt = Date.now();
313
+ pack.transitions = this.lifecycle.getTransitions(packId);
314
+ return true;
315
+ }
316
+
317
+ /**
318
+ * Enable a previously disabled pack — reactivates capabilities.
319
+ */
320
+ async enable(packId: string, runtimeCtx?: unknown, packRuntime?: PackRuntime): Promise<boolean> {
321
+ const pack = this.packs.get(packId);
322
+ if (!pack) return false;
323
+
324
+ this.lifecycle.transition(packId, 'ready', 'User enabled');
325
+
326
+ // Reactivate facades
327
+ if (pack.manifest.facades.length > 0 && this.pluginRegistry.get(packId)) {
328
+ const ctx: PluginContext = {
329
+ packRuntime:
330
+ packRuntime ??
331
+ ({
332
+ vault: {},
333
+ getProject: () => undefined,
334
+ listProjects: () => [],
335
+ createCheck: () => '',
336
+ validateCheck: () => null,
337
+ validateAndConsume: () => null,
338
+ } as unknown as PackRuntime),
339
+ runtime: runtimeCtx ?? {},
340
+ manifest: this.pluginRegistry.get(packId)!.manifest,
341
+ directory: pack.directory,
342
+ };
343
+ await this.pluginRegistry.activate(packId, ctx);
344
+ pack.facadesRegistered = true;
345
+ }
346
+
347
+ pack.status = 'ready';
348
+ pack.disabledAt = undefined;
349
+ pack.transitions = this.lifecycle.getTransitions(packId);
350
+ return true;
351
+ }
352
+
277
353
  /**
278
354
  * Get an installed pack by ID.
279
355
  */
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Pack Lifecycle Manager — Central state machine for pack lifecycle management.
3
+ *
4
+ * Manages in-memory pack states, validates transitions against VALID_TRANSITIONS,
5
+ * records transition history, and notifies listeners on state changes.
6
+ */
7
+
8
+ import type { PackState, PackTransition } from './types.js';
9
+ import { VALID_TRANSITIONS } from './types.js';
10
+
11
+ type TransitionListener = (packId: string, from: PackState, to: PackState, reason?: string) => void;
12
+
13
+ interface PackEntry {
14
+ state: PackState;
15
+ transitions: PackTransition[];
16
+ }
17
+
18
+ export class PackLifecycleManager {
19
+ private packs: Map<string, PackEntry> = new Map();
20
+ private listeners: Array<TransitionListener> = [];
21
+
22
+ /**
23
+ * Transition a pack to a new state. Validates against VALID_TRANSITIONS.
24
+ * Throws a descriptive error if the transition is not allowed.
25
+ */
26
+ transition(packId: string, to: PackState, reason?: string): void {
27
+ const entry = this.packs.get(packId);
28
+ if (!entry) {
29
+ throw new Error(`Pack '${packId}' is not being tracked`);
30
+ }
31
+
32
+ const currentState = entry.state;
33
+ const validTargets = VALID_TRANSITIONS[currentState];
34
+
35
+ if (!validTargets.includes(to)) {
36
+ throw new Error(
37
+ `Invalid pack lifecycle transition for '${packId}': '${currentState}' \u2192 '${to}'. Valid targets from '${currentState}': ${validTargets.join(', ')}`,
38
+ );
39
+ }
40
+
41
+ const transition: PackTransition = {
42
+ from: currentState,
43
+ to,
44
+ timestamp: Date.now(),
45
+ reason,
46
+ };
47
+
48
+ entry.state = to;
49
+ entry.transitions.push(transition);
50
+
51
+ for (const listener of this.listeners) {
52
+ listener(packId, currentState, to, reason);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Set initial state without transition validation (for loading from lockfile).
58
+ */
59
+ initState(packId: string, state: PackState): void {
60
+ this.packs.set(packId, { state, transitions: [] });
61
+ }
62
+
63
+ /**
64
+ * Returns current state of a pack, or undefined if not tracked.
65
+ */
66
+ getState(packId: string): PackState | undefined {
67
+ return this.packs.get(packId)?.state;
68
+ }
69
+
70
+ /**
71
+ * Returns the full transition history for a pack.
72
+ */
73
+ getTransitions(packId: string): PackTransition[] {
74
+ return this.packs.get(packId)?.transitions ?? [];
75
+ }
76
+
77
+ /**
78
+ * Register a listener for state transitions. Returns an unsubscribe function.
79
+ */
80
+ onTransition(callback: TransitionListener): () => void {
81
+ this.listeners.push(callback);
82
+ return () => {
83
+ const idx = this.listeners.indexOf(callback);
84
+ if (idx !== -1) {
85
+ this.listeners.splice(idx, 1);
86
+ }
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Remove a pack from tracking entirely.
92
+ */
93
+ remove(packId: string): void {
94
+ this.packs.delete(packId);
95
+ }
96
+
97
+ /**
98
+ * List all tracked packs with their current state.
99
+ */
100
+ listAll(): Array<{ packId: string; state: PackState }> {
101
+ const result: Array<{ packId: string; state: PackState }> = [];
102
+ for (const [packId, entry] of this.packs) {
103
+ result.push({ packId, state: entry.state });
104
+ }
105
+ return result;
106
+ }
107
+
108
+ /**
109
+ * Clear all tracked state and listeners.
110
+ */
111
+ reset(): void {
112
+ this.packs.clear();
113
+ this.listeners.length = 0;
114
+ }
115
+ }
@@ -81,7 +81,7 @@ describe('PackLockfile', () => {
81
81
  expect(existsSync(lockPath)).toBe(true);
82
82
 
83
83
  const data = JSON.parse(readFileSync(lockPath, 'utf-8'));
84
- expect(data.version).toBe(1);
84
+ expect(data.version).toBe(2);
85
85
  expect(data.packs['test-pack'].version).toBe('1.0.0');
86
86
  });
87
87
 
@@ -417,7 +417,7 @@ describe('PackInstaller', () => {
417
417
  const pack = installer.get('specific');
418
418
  expect(pack).toBeDefined();
419
419
  expect(pack?.manifest.version).toBe('3.0.0');
420
- expect(pack?.status).toBe('installed');
420
+ expect(pack?.status).toBe('ready');
421
421
  });
422
422
  });
423
423
  });
@@ -108,13 +108,43 @@ export type PackManifest = z.infer<typeof packManifestSchema>;
108
108
  // INSTALL TYPES
109
109
  // =============================================================================
110
110
 
111
- export type PackStatus = 'installed' | 'error' | 'uninstalled';
111
+ // ─── Lifecycle States ─────────────────────────────────────────────────
112
+
113
+ /** Full lifecycle state for a pack */
114
+ export type PackState =
115
+ | 'installed' // Just installed, not yet activated
116
+ | 'ready' // Active — capabilities, skills, hooks all live
117
+ | 'disabled' // Temporarily deactivated — vault entries kept, capabilities off
118
+ | 'error' // Failed to activate — error message in errorMessage field
119
+ | 'upgrade_pending' // New version available, old version still active
120
+ | 'uninstalled'; // Removed — vault entries remain (permanent knowledge)
121
+
122
+ /** @deprecated Use PackState instead */
123
+ export type PackStatus = PackState;
124
+
125
+ /** Valid state transitions — key is "from" state, values are allowed "to" states */
126
+ export const VALID_TRANSITIONS: Record<PackState, PackState[]> = {
127
+ installed: ['ready', 'error', 'uninstalled'],
128
+ ready: ['ready', 'disabled', 'error', 'upgrade_pending', 'uninstalled'],
129
+ disabled: ['ready', 'uninstalled'],
130
+ error: ['ready', 'uninstalled'],
131
+ upgrade_pending: ['ready', 'error', 'uninstalled'],
132
+ uninstalled: ['installed'],
133
+ };
134
+
135
+ /** A recorded state transition */
136
+ export interface PackTransition {
137
+ from: PackState;
138
+ to: PackState;
139
+ timestamp: number;
140
+ reason?: string;
141
+ }
112
142
 
113
143
  export interface InstalledPack {
114
144
  id: string;
115
145
  manifest: PackManifest;
116
146
  directory: string;
117
- status: PackStatus;
147
+ status: PackState;
118
148
  error?: string;
119
149
  /** Number of vault entries seeded */
120
150
  vaultEntries: number;
@@ -125,6 +155,14 @@ export interface InstalledPack {
125
155
  /** Whether facades were registered via plugin system */
126
156
  facadesRegistered: boolean;
127
157
  installedAt: number;
158
+ /** Lifecycle transition history (most recent last) */
159
+ transitions?: PackTransition[];
160
+ /** When the pack was disabled (if in disabled state) */
161
+ disabledAt?: number;
162
+ /** Error message (if in error state) */
163
+ errorMessage?: string;
164
+ /** Version available for upgrade (if in upgrade_pending state) */
165
+ upgradeVersion?: string;
128
166
  }
129
167
 
130
168
  export interface InstallResult {
@@ -3,9 +3,14 @@
3
3
  *
4
4
  * Supports both positional (array) and named (object) parameters.
5
5
  * Exposes getDatabase() for backward-compat consumers that need the raw db.
6
+ *
7
+ * better-sqlite3 is loaded lazily at construction time (not at module import)
8
+ * so that code paths that never instantiate a provider don't require the
9
+ * native module to be installed.
6
10
  */
7
11
 
8
- import Database from 'better-sqlite3';
12
+ import type Database from 'better-sqlite3';
13
+ import { createRequire } from 'node:module';
9
14
  import { mkdirSync } from 'node:fs';
10
15
  import { dirname } from 'node:path';
11
16
  import type {
@@ -15,6 +20,24 @@ import type {
15
20
  FtsSearchOptions,
16
21
  } from './types.js';
17
22
 
23
+ type DatabaseConstructor = typeof Database;
24
+ let _DatabaseCtor: DatabaseConstructor | undefined;
25
+
26
+ function loadDriver(): DatabaseConstructor {
27
+ if (!_DatabaseCtor) {
28
+ const req = createRequire(import.meta.url);
29
+ try {
30
+ _DatabaseCtor = req('better-sqlite3') as DatabaseConstructor;
31
+ } catch {
32
+ throw new Error(
33
+ 'better-sqlite3 is required for persistence but is not installed.\n' +
34
+ 'Run: npm install better-sqlite3',
35
+ );
36
+ }
37
+ }
38
+ return _DatabaseCtor;
39
+ }
40
+
18
41
  /** Apply performance-tuning PRAGMAs for file-backed SQLite databases. */
19
42
  export function applyPerformancePragmas(db: Database.Database): void {
20
43
  db.pragma('cache_size = -64000'); // 64MB
@@ -28,8 +51,9 @@ export class SQLitePersistenceProvider implements PersistenceProvider {
28
51
  private db: Database.Database;
29
52
 
30
53
  constructor(path: string = ':memory:') {
54
+ const Driver = loadDriver();
31
55
  if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
32
- this.db = new Database(path);
56
+ this.db = new Driver(path);
33
57
  if (path !== ':memory:') applyPerformancePragmas(this.db);
34
58
  }
35
59
 
@@ -5,17 +5,21 @@ import type { OpDefinition } from '../facades/types.js';
5
5
 
6
6
  // ─── Mock Node.js fs/os modules ────────────────────────────────────────
7
7
 
8
+ /** Normalize path separators so Windows backslash paths match forward-slash keys */
9
+ const norm = (p: string): string => p.replace(/\\/g, '/');
10
+
8
11
  const mockFs: Record<string, string> = {};
9
12
  const mockDirs = new Set<string>();
10
13
 
11
14
  vi.mock('node:fs', () => ({
12
- existsSync: vi.fn((p: string) => p in mockFs || mockDirs.has(p)),
15
+ existsSync: vi.fn((p: string) => norm(p) in mockFs || mockDirs.has(norm(p))),
13
16
  readFileSync: vi.fn((p: string) => {
14
- if (p in mockFs) return mockFs[p];
17
+ const key = norm(p);
18
+ if (key in mockFs) return mockFs[key];
15
19
  throw new Error(`ENOENT: ${p}`);
16
20
  }),
17
21
  writeFileSync: vi.fn((p: string, content: string) => {
18
- mockFs[p] = content;
22
+ mockFs[norm(p)] = content;
19
23
  }),
20
24
  mkdirSync: vi.fn((_p: string) => undefined),
21
25
  copyFileSync: vi.fn(),
@@ -30,7 +34,8 @@ vi.mock('node:os', () => ({
30
34
 
31
35
  vi.mock('node:path', async () => {
32
36
  const actual = await vi.importActual<typeof import('node:path')>('node:path');
33
- return actual;
37
+ // Always use posix path semantics so mock filesystem keys (forward slashes) work on all platforms
38
+ return { ...actual.posix, default: actual.posix };
34
39
  });
35
40
 
36
41
  vi.mock('./claude-md-helpers.js', () => ({
@@ -38,6 +38,7 @@ import {
38
38
  import { detectRationalizations } from '../planning/rationalization-detector.js';
39
39
  import { ImpactAnalyzer } from '../planning/impact-analyzer.js';
40
40
  import type { ImpactReport } from '../planning/impact-analyzer.js';
41
+ import { recordPlanFeedback } from './plan-feedback-helper.js';
41
42
 
42
43
  // ---------------------------------------------------------------------------
43
44
  // Intent detection — keyword-based mapping from prompt to intent
@@ -235,7 +236,7 @@ export function createOrchestrateOps(
235
236
  runtime: AgentRuntime,
236
237
  facades?: FacadeConfig[],
237
238
  ): OpDefinition[] {
238
- const { planner, brainIntelligence, vault, contextHealth } = runtime;
239
+ const { planner, brain, brainIntelligence, vault, contextHealth } = runtime;
239
240
  const agentId = runtime.config.agentId;
240
241
 
241
242
  return [
@@ -385,11 +386,149 @@ export function createOrchestrateOps(
385
386
  planId: z.string().describe('ID of the plan to execute (flow planId or legacy planId)'),
386
387
  domain: z.string().optional().describe('Domain for brain session tracking'),
387
388
  context: z.string().optional().describe('Additional context for the brain session'),
389
+ runtime: z
390
+ .string()
391
+ .optional()
392
+ .describe(
393
+ 'Runtime adapter type (e.g. "claude-code", "codex"). ' +
394
+ 'When provided, dispatches via the adapter instead of the flow engine.',
395
+ ),
396
+ subagent: z
397
+ .boolean()
398
+ .optional()
399
+ .describe(
400
+ 'When true, dispatches plan tasks via SubagentDispatcher instead of FlowExecutor. ' +
401
+ 'Each task runs as a separate subagent process.',
402
+ ),
403
+ parallel: z
404
+ .boolean()
405
+ .optional()
406
+ .describe(
407
+ 'Run subagent tasks in parallel (default: true). Only applies when subagent=true.',
408
+ ),
409
+ maxConcurrent: z
410
+ .number()
411
+ .optional()
412
+ .describe('Max concurrent subagents (default: 3). Only applies when subagent=true.'),
388
413
  }),
389
414
  handler: async (params) => {
390
415
  const planId = params.planId as string;
391
416
  const domain = params.domain as string | undefined;
392
417
  const context = params.context as string | undefined;
418
+ const runtimeType = params.runtime as string | undefined;
419
+ const useSubagent = params.subagent as boolean | undefined;
420
+ const parallelMode = params.parallel as boolean | undefined;
421
+ const maxConcurrentParam = params.maxConcurrent as number | undefined;
422
+
423
+ // ── Subagent dispatch path ───────────────────────────────────
424
+ // When subagent=true, dispatch plan tasks via SubagentDispatcher.
425
+ // Each task runs as a separate child process via the adapter layer.
426
+ if (useSubagent && runtime.subagentDispatcher) {
427
+ const entry = planStore.get(planId);
428
+ const legacyPlan = !entry ? planner.get(planId) : undefined;
429
+ const tasks =
430
+ entry?.plan.steps.map((s) => ({
431
+ taskId: s.id,
432
+ prompt: s.name,
433
+ workspace: process.cwd(),
434
+ runtime: runtimeType,
435
+ timeout: 300_000,
436
+ })) ??
437
+ legacyPlan?.tasks?.map((t) => ({
438
+ taskId: t.id,
439
+ prompt: t.title ?? t.description ?? '',
440
+ workspace: process.cwd(),
441
+ runtime: runtimeType,
442
+ timeout: 300_000,
443
+ })) ??
444
+ [];
445
+
446
+ const aggregated = await runtime.subagentDispatcher.dispatch(tasks, {
447
+ parallel: parallelMode ?? true,
448
+ maxConcurrent: maxConcurrentParam ?? 3,
449
+ });
450
+
451
+ // Track in brain session
452
+ const existingSession = brainIntelligence.getSessionByPlanId(planId);
453
+ const session =
454
+ existingSession && !existingSession.endedAt
455
+ ? existingSession
456
+ : brainIntelligence.lifecycle({
457
+ action: 'start',
458
+ domain,
459
+ context,
460
+ planId,
461
+ });
462
+
463
+ contextHealth.track({
464
+ type: 'orchestrate_execute',
465
+ payloadSize: JSON.stringify(aggregated).length,
466
+ });
467
+ const healthStatus = contextHealth.check();
468
+ const healthWarning = buildHealthWarning(healthStatus, vault);
469
+
470
+ return {
471
+ plan: { id: planId, status: 'executing' },
472
+ session,
473
+ subagent: {
474
+ status: aggregated.status,
475
+ totalTasks: aggregated.totalTasks,
476
+ completed: aggregated.completed,
477
+ failed: aggregated.failed,
478
+ durationMs: aggregated.durationMs,
479
+ totalUsage: aggregated.totalUsage,
480
+ },
481
+ ...(healthWarning ? { contextHealth: healthWarning } : {}),
482
+ };
483
+ }
484
+
485
+ // ── Adapter dispatch path ────────────────────────────────────
486
+ // When a runtime is specified, dispatch the plan's prompt via the
487
+ // adapter instead of the flow engine. This is the integration point
488
+ // for multi-runtime support (GH #410).
489
+ if (runtimeType && runtime.adapterRegistry) {
490
+ const adapter = runtime.adapterRegistry.get(runtimeType);
491
+ const entry = planStore.get(planId);
492
+ const prompt = entry?.plan.summary ?? `Execute plan ${planId}`;
493
+
494
+ const adapterResult = await adapter.execute({
495
+ runId: `${planId}-${Date.now()}`,
496
+ prompt,
497
+ workspace: process.cwd(),
498
+ config: { planId, domain },
499
+ });
500
+
501
+ // Track in brain session
502
+ const existingSession = brainIntelligence.getSessionByPlanId(planId);
503
+ const session =
504
+ existingSession && !existingSession.endedAt
505
+ ? existingSession
506
+ : brainIntelligence.lifecycle({
507
+ action: 'start',
508
+ domain,
509
+ context,
510
+ planId,
511
+ });
512
+
513
+ contextHealth.track({
514
+ type: 'orchestrate_execute',
515
+ payloadSize: JSON.stringify(adapterResult).length,
516
+ });
517
+ const healthStatus = contextHealth.check();
518
+ const healthWarning = buildHealthWarning(healthStatus, vault);
519
+
520
+ return {
521
+ plan: { id: planId, status: 'executing' },
522
+ session,
523
+ adapter: {
524
+ type: runtimeType,
525
+ exitCode: adapterResult.exitCode,
526
+ summary: adapterResult.summary,
527
+ usage: adapterResult.usage,
528
+ },
529
+ ...(healthWarning ? { contextHealth: healthWarning } : {}),
530
+ };
531
+ }
393
532
 
394
533
  // Look up flow plan
395
534
  const entry = planStore.get(planId);
@@ -636,6 +775,19 @@ export function createOrchestrateOps(
636
775
  filesModified,
637
776
  });
638
777
 
778
+ // Record brain feedback for vault entries referenced in plan decisions
779
+ if (planObj && planObj.decisions) {
780
+ try {
781
+ recordPlanFeedback(
782
+ { objective: planObj.objective, decisions: planObj.decisions },
783
+ brain,
784
+ brainIntelligence,
785
+ );
786
+ } catch {
787
+ // Brain feedback is best-effort
788
+ }
789
+ }
790
+
639
791
  // Extract knowledge — runs regardless of plan existence
640
792
  let extraction = null;
641
793
  try {
@@ -61,6 +61,9 @@ import { generatePersonaInstructions } from '../persona/prompt-generator.js';
61
61
  import { OperatorProfileStore } from '../operator/operator-profile.js';
62
62
  import { ContextHealthMonitor } from './context-health.js';
63
63
  import { ShutdownRegistry } from './shutdown-registry.js';
64
+ import { RuntimeAdapterRegistry } from '../adapters/registry.js';
65
+ import { ClaudeCodeRuntimeAdapter } from '../adapters/claude-code-adapter.js';
66
+ import { SubagentDispatcher } from '../subagent/dispatcher.js';
64
67
 
65
68
  /**
66
69
  * Create a fully initialized agent runtime.
@@ -322,6 +325,8 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
322
325
  shutdownRegistry.register('pipelineRunner', () => pipelineRunner.stop());
323
326
  // Agency manager — close FSWatchers and debounce timers
324
327
  shutdownRegistry.register('agencyManager', () => agencyManager.disable());
328
+
329
+ shutdownRegistry.register('subagentDispatcher', () => subagentDispatcher.cleanup());
325
330
  // Loop manager — clear accumulated state
326
331
  shutdownRegistry.register('loopManager', () => {
327
332
  if (loop.isActive()) {
@@ -333,6 +338,14 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
333
338
  }
334
339
  });
335
340
 
341
+ // Runtime Adapter Registry — dispatch work to different AI CLIs
342
+ const adapterRegistry = new RuntimeAdapterRegistry();
343
+ adapterRegistry.register('claude-code', new ClaudeCodeRuntimeAdapter());
344
+ adapterRegistry.setDefault('claude-code');
345
+
346
+ // Subagent Dispatcher — spawn and manage child agent processes
347
+ const subagentDispatcher = new SubagentDispatcher({ adapterRegistry });
348
+
336
349
  return {
337
350
  config,
338
351
  logger,
@@ -379,6 +392,8 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
379
392
  const p = loadPersona(agentId, config.persona ?? undefined);
380
393
  return generatePersonaInstructions(p);
381
394
  })(),
395
+ adapterRegistry,
396
+ subagentDispatcher,
382
397
  contextHealth: new ContextHealthMonitor(),
383
398
  shutdownRegistry,
384
399
  createdAt: Date.now(),