@soleri/core 9.4.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 (228) 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/hooks/candidate-scorer.d.ts +28 -0
  38. package/dist/hooks/candidate-scorer.d.ts.map +1 -0
  39. package/dist/hooks/candidate-scorer.js +20 -0
  40. package/dist/hooks/candidate-scorer.js.map +1 -0
  41. package/dist/hooks/index.d.ts +2 -0
  42. package/dist/hooks/index.d.ts.map +1 -0
  43. package/dist/hooks/index.js +2 -0
  44. package/dist/hooks/index.js.map +1 -0
  45. package/dist/index.d.ts +14 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +12 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/llm/llm-client.d.ts.map +1 -1
  50. package/dist/llm/llm-client.js +1 -0
  51. package/dist/llm/llm-client.js.map +1 -1
  52. package/dist/packs/index.d.ts +3 -2
  53. package/dist/packs/index.d.ts.map +1 -1
  54. package/dist/packs/index.js +3 -2
  55. package/dist/packs/index.js.map +1 -1
  56. package/dist/packs/lockfile.d.ts +23 -1
  57. package/dist/packs/lockfile.d.ts.map +1 -1
  58. package/dist/packs/lockfile.js +50 -4
  59. package/dist/packs/lockfile.js.map +1 -1
  60. package/dist/packs/pack-installer.d.ts +10 -0
  61. package/dist/packs/pack-installer.d.ts.map +1 -1
  62. package/dist/packs/pack-installer.js +69 -2
  63. package/dist/packs/pack-installer.js.map +1 -1
  64. package/dist/packs/pack-lifecycle.d.ts +50 -0
  65. package/dist/packs/pack-lifecycle.d.ts.map +1 -0
  66. package/dist/packs/pack-lifecycle.js +91 -0
  67. package/dist/packs/pack-lifecycle.js.map +1 -0
  68. package/dist/packs/types.d.ts +64 -44
  69. package/dist/packs/types.d.ts.map +1 -1
  70. package/dist/packs/types.js +9 -0
  71. package/dist/packs/types.js.map +1 -1
  72. package/dist/persistence/sqlite-provider.d.ts +5 -1
  73. package/dist/persistence/sqlite-provider.d.ts.map +1 -1
  74. package/dist/persistence/sqlite-provider.js +22 -2
  75. package/dist/persistence/sqlite-provider.js.map +1 -1
  76. package/dist/planning/github-projection.d.ts +8 -8
  77. package/dist/planning/github-projection.d.ts.map +1 -1
  78. package/dist/planning/github-projection.js +42 -42
  79. package/dist/planning/github-projection.js.map +1 -1
  80. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  81. package/dist/planning/plan-lifecycle.js +6 -1
  82. package/dist/planning/plan-lifecycle.js.map +1 -1
  83. package/dist/plugins/types.d.ts +21 -21
  84. package/dist/queue/pipeline-runner.d.ts.map +1 -1
  85. package/dist/queue/pipeline-runner.js +4 -0
  86. package/dist/queue/pipeline-runner.js.map +1 -1
  87. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  88. package/dist/runtime/curator-extra-ops.js +9 -1
  89. package/dist/runtime/curator-extra-ops.js.map +1 -1
  90. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  91. package/dist/runtime/facades/memory-facade.js +169 -0
  92. package/dist/runtime/facades/memory-facade.js.map +1 -1
  93. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  94. package/dist/runtime/orchestrate-ops.js +133 -4
  95. package/dist/runtime/orchestrate-ops.js.map +1 -1
  96. package/dist/runtime/runtime.d.ts.map +1 -1
  97. package/dist/runtime/runtime.js +128 -90
  98. package/dist/runtime/runtime.js.map +1 -1
  99. package/dist/runtime/session-briefing.d.ts.map +1 -1
  100. package/dist/runtime/session-briefing.js +44 -11
  101. package/dist/runtime/session-briefing.js.map +1 -1
  102. package/dist/runtime/shutdown-registry.d.ts +36 -0
  103. package/dist/runtime/shutdown-registry.d.ts.map +1 -0
  104. package/dist/runtime/shutdown-registry.js +74 -0
  105. package/dist/runtime/shutdown-registry.js.map +1 -0
  106. package/dist/runtime/types.d.ts +10 -1
  107. package/dist/runtime/types.d.ts.map +1 -1
  108. package/dist/subagent/concurrency-manager.d.ts +29 -0
  109. package/dist/subagent/concurrency-manager.d.ts.map +1 -0
  110. package/dist/subagent/concurrency-manager.js +73 -0
  111. package/dist/subagent/concurrency-manager.js.map +1 -0
  112. package/dist/subagent/dispatcher.d.ts +41 -0
  113. package/dist/subagent/dispatcher.d.ts.map +1 -0
  114. package/dist/subagent/dispatcher.js +259 -0
  115. package/dist/subagent/dispatcher.js.map +1 -0
  116. package/dist/subagent/index.d.ts +14 -0
  117. package/dist/subagent/index.d.ts.map +1 -0
  118. package/dist/subagent/index.js +15 -0
  119. package/dist/subagent/index.js.map +1 -0
  120. package/dist/subagent/orphan-reaper.d.ts +37 -0
  121. package/dist/subagent/orphan-reaper.d.ts.map +1 -0
  122. package/dist/subagent/orphan-reaper.js +71 -0
  123. package/dist/subagent/orphan-reaper.js.map +1 -0
  124. package/dist/subagent/result-aggregator.d.ts +7 -0
  125. package/dist/subagent/result-aggregator.d.ts.map +1 -0
  126. package/dist/subagent/result-aggregator.js +57 -0
  127. package/dist/subagent/result-aggregator.js.map +1 -0
  128. package/dist/subagent/task-checkout.d.ts +36 -0
  129. package/dist/subagent/task-checkout.d.ts.map +1 -0
  130. package/dist/subagent/task-checkout.js +52 -0
  131. package/dist/subagent/task-checkout.js.map +1 -0
  132. package/dist/subagent/types.d.ts +114 -0
  133. package/dist/subagent/types.d.ts.map +1 -0
  134. package/dist/subagent/types.js +9 -0
  135. package/dist/subagent/types.js.map +1 -0
  136. package/dist/subagent/workspace-resolver.d.ts +35 -0
  137. package/dist/subagent/workspace-resolver.d.ts.map +1 -0
  138. package/dist/subagent/workspace-resolver.js +99 -0
  139. package/dist/subagent/workspace-resolver.js.map +1 -0
  140. package/dist/transport/http-server.d.ts.map +1 -1
  141. package/dist/transport/http-server.js +49 -3
  142. package/dist/transport/http-server.js.map +1 -1
  143. package/dist/transport/ws-server.d.ts.map +1 -1
  144. package/dist/transport/ws-server.js +7 -0
  145. package/dist/transport/ws-server.js.map +1 -1
  146. package/dist/vault/linking.d.ts +3 -4
  147. package/dist/vault/linking.d.ts.map +1 -1
  148. package/dist/vault/linking.js +79 -32
  149. package/dist/vault/linking.js.map +1 -1
  150. package/dist/vault/vault-maintenance.d.ts.map +1 -1
  151. package/dist/vault/vault-maintenance.js +7 -14
  152. package/dist/vault/vault-maintenance.js.map +1 -1
  153. package/dist/vault/vault-memories.d.ts.map +1 -1
  154. package/dist/vault/vault-memories.js +19 -9
  155. package/dist/vault/vault-memories.js.map +1 -1
  156. package/dist/vault/vault-schema.d.ts +1 -0
  157. package/dist/vault/vault-schema.d.ts.map +1 -1
  158. package/dist/vault/vault-schema.js +20 -0
  159. package/dist/vault/vault-schema.js.map +1 -1
  160. package/dist/vault/vault.d.ts.map +1 -1
  161. package/dist/vault/vault.js +7 -3
  162. package/dist/vault/vault.js.map +1 -1
  163. package/package.json +8 -2
  164. package/src/__tests__/adapters/claude-code-adapter.test.ts +167 -0
  165. package/src/__tests__/adapters/registry.test.ts +100 -0
  166. package/src/__tests__/packs/pack-lifecycle.test.ts +379 -0
  167. package/src/__tests__/subagent/concurrency-manager.test.ts +132 -0
  168. package/src/__tests__/subagent/dispatcher.test.ts +195 -0
  169. package/src/__tests__/subagent/orphan-reaper.test.ts +141 -0
  170. package/src/__tests__/subagent/result-aggregator.test.ts +141 -0
  171. package/src/__tests__/subagent/task-checkout.test.ts +86 -0
  172. package/src/__tests__/subagent/workspace-resolver.test.ts +138 -0
  173. package/src/adapters/claude-code-adapter.ts +163 -0
  174. package/src/adapters/index.ts +22 -0
  175. package/src/adapters/registry.ts +53 -0
  176. package/src/adapters/types.ts +114 -0
  177. package/src/brain/brain.ts +120 -46
  178. package/src/brain/intelligence.ts +42 -34
  179. package/src/chat/agent-loop.ts +1 -1
  180. package/src/chat/notifications.ts +4 -0
  181. package/src/control/intent-router.ts +10 -8
  182. package/src/curator/curator.ts +146 -29
  183. package/src/hooks/candidate-scorer.test.ts +76 -0
  184. package/src/hooks/candidate-scorer.ts +39 -0
  185. package/src/index.ts +40 -1
  186. package/src/llm/llm-client.ts +1 -0
  187. package/src/packs/index.ts +5 -1
  188. package/src/packs/lockfile.ts +70 -5
  189. package/src/packs/pack-installer.ts +78 -2
  190. package/src/packs/pack-lifecycle.ts +115 -0
  191. package/src/packs/pack-lockfile.test.ts +1 -1
  192. package/src/packs/pack-system.test.ts +1 -1
  193. package/src/packs/types.ts +40 -2
  194. package/src/persistence/sqlite-provider.ts +27 -2
  195. package/src/planning/github-projection.ts +48 -44
  196. package/src/planning/plan-lifecycle.ts +14 -1
  197. package/src/queue/pipeline-runner.ts +4 -0
  198. package/src/runtime/admin-setup-ops.test.ts +9 -4
  199. package/src/runtime/curator-extra-ops.test.ts +7 -0
  200. package/src/runtime/curator-extra-ops.ts +10 -1
  201. package/src/runtime/facades/curator-facade.test.ts +7 -0
  202. package/src/runtime/facades/memory-facade.ts +187 -0
  203. package/src/runtime/orchestrate-ops.ts +156 -4
  204. package/src/runtime/runtime.test.ts +50 -2
  205. package/src/runtime/runtime.ts +132 -89
  206. package/src/runtime/session-briefing.test.ts +94 -2
  207. package/src/runtime/session-briefing.ts +48 -12
  208. package/src/runtime/shutdown-registry.test.ts +151 -0
  209. package/src/runtime/shutdown-registry.ts +85 -0
  210. package/src/runtime/types.ts +10 -1
  211. package/src/subagent/concurrency-manager.ts +89 -0
  212. package/src/subagent/dispatcher.ts +326 -0
  213. package/src/subagent/index.ts +28 -0
  214. package/src/subagent/orphan-reaper.ts +82 -0
  215. package/src/subagent/result-aggregator.ts +66 -0
  216. package/src/subagent/task-checkout.ts +60 -0
  217. package/src/subagent/types.ts +138 -0
  218. package/src/subagent/workspace-resolver.ts +117 -0
  219. package/src/transport/http-server.ts +50 -3
  220. package/src/transport/ws-server.ts +8 -0
  221. package/src/vault/linking.test.ts +12 -0
  222. package/src/vault/linking.ts +90 -44
  223. package/src/vault/vault-maintenance.ts +11 -18
  224. package/src/vault/vault-memories.ts +21 -13
  225. package/src/vault/vault-scaling.test.ts +3 -2
  226. package/src/vault/vault-schema.ts +21 -0
  227. package/src/vault/vault.ts +8 -3
  228. package/vitest.config.ts +2 -0
@@ -28,6 +28,13 @@ function mockRuntime() {
28
28
  start: vi.fn(),
29
29
  stop: vi.fn(),
30
30
  },
31
+ shutdownRegistry: {
32
+ register: vi.fn(),
33
+ closeAll: vi.fn(),
34
+ closeAllSync: vi.fn(),
35
+ size: 0,
36
+ isClosed: false,
37
+ },
31
38
  } as unknown as AgentRuntime;
32
39
  }
33
40
 
@@ -10,9 +10,18 @@ import type { OpDefinition } from '../facades/types.js';
10
10
  import type { AgentRuntime } from './types.js';
11
11
 
12
12
  export function createCuratorExtraOps(runtime: AgentRuntime): OpDefinition[] {
13
- const { curator, jobQueue, pipelineRunner } = runtime;
13
+ const { curator, jobQueue, pipelineRunner, shutdownRegistry } = runtime;
14
14
  let consolidationInterval: ReturnType<typeof setInterval> | null = null;
15
15
 
16
+ // Register cleanup for any consolidation interval started during this session
17
+ shutdownRegistry.register('curatorConsolidation', () => {
18
+ if (consolidationInterval) {
19
+ clearInterval(consolidationInterval);
20
+ consolidationInterval = null;
21
+ }
22
+ pipelineRunner.stop();
23
+ });
24
+
16
25
  return [
17
26
  // ─── Entry History ──────────────────────────────────────────────
18
27
  {
@@ -30,6 +30,13 @@ function mockRuntime(): AgentRuntime {
30
30
  start: vi.fn(),
31
31
  stop: vi.fn(),
32
32
  },
33
+ shutdownRegistry: {
34
+ register: vi.fn(),
35
+ closeAll: vi.fn(),
36
+ closeAllSync: vi.fn(),
37
+ size: 0,
38
+ isClosed: false,
39
+ },
33
40
  } as unknown as AgentRuntime;
34
41
  }
35
42
 
@@ -195,6 +195,193 @@ export function createMemoryFacadeOps(runtime: AgentRuntime): OpDefinition[] {
195
195
  },
196
196
  },
197
197
 
198
+ // ─── Handoff ────────────────────────────────────────────────
199
+ {
200
+ name: 'handoff_generate',
201
+ description:
202
+ 'Generate a structured handoff document for context transitions. ' +
203
+ 'Pulls from active plan (if any) and recent session memories to produce ' +
204
+ 'a markdown document that can bootstrap a new context window. ' +
205
+ 'Ephemeral — NOT persisted to vault.',
206
+ auth: 'read',
207
+ schema: z.object({
208
+ projectPath: z
209
+ .string()
210
+ .optional()
211
+ .default('.')
212
+ .describe('Project path for filtering memories'),
213
+ sessionLimit: z
214
+ .number()
215
+ .optional()
216
+ .default(3)
217
+ .describe('Number of recent session memories to include'),
218
+ }),
219
+ handler: async (params) => {
220
+ const { planner } = runtime;
221
+ const projectPath = params.projectPath as string;
222
+ const sessionLimit = (params.sessionLimit as number) ?? 3;
223
+
224
+ const sections: string[] = [];
225
+ const now = new Date().toISOString();
226
+
227
+ sections.push('# Handoff Document');
228
+ sections.push('');
229
+ sections.push(`Generated: ${now}`);
230
+ sections.push('');
231
+
232
+ // ─── Active Plan Context ───────────────────────────
233
+ const activePlans = planner.getActive();
234
+ if (activePlans.length > 0) {
235
+ const plan = activePlans[0]; // Most relevant active plan
236
+ sections.push('## Active Plan');
237
+ sections.push('');
238
+ sections.push(`| Field | Value |`);
239
+ sections.push(`|-------|-------|`);
240
+ sections.push(`| **Plan ID** | ${plan.id} |`);
241
+ sections.push(`| **Objective** | ${plan.objective} |`);
242
+ sections.push(`| **Status** | ${plan.status} |`);
243
+ sections.push(`| **Scope** | ${plan.scope} |`);
244
+ sections.push('');
245
+
246
+ // Decisions
247
+ if (plan.decisions.length > 0) {
248
+ sections.push('### Decisions');
249
+ sections.push('');
250
+ for (const d of plan.decisions) {
251
+ if (typeof d === 'string') {
252
+ sections.push(`- ${d}`);
253
+ } else {
254
+ sections.push(`- **${d.decision}** — ${d.rationale}`);
255
+ }
256
+ }
257
+ sections.push('');
258
+ }
259
+
260
+ // Task status summary
261
+ if (plan.tasks.length > 0) {
262
+ sections.push('### Tasks');
263
+ sections.push('');
264
+ sections.push('| # | Task | Status |');
265
+ sections.push('|---|------|--------|');
266
+ for (let i = 0; i < plan.tasks.length; i++) {
267
+ const t = plan.tasks[i];
268
+ sections.push(`| ${i + 1} | ${t.title} | ${t.status} |`);
269
+ }
270
+ sections.push('');
271
+ }
272
+
273
+ // Approach
274
+ if (plan.approach) {
275
+ sections.push('### Approach');
276
+ sections.push('');
277
+ sections.push(plan.approach);
278
+ sections.push('');
279
+ }
280
+
281
+ // Additional active plans (just IDs)
282
+ if (activePlans.length > 1) {
283
+ sections.push('### Other Active Plans');
284
+ sections.push('');
285
+ for (let i = 1; i < activePlans.length; i++) {
286
+ const p = activePlans[i];
287
+ sections.push(`- **${p.id}**: ${p.objective} (${p.status})`);
288
+ }
289
+ sections.push('');
290
+ }
291
+ } else {
292
+ sections.push('## Active Plan');
293
+ sections.push('');
294
+ sections.push('No active plans.');
295
+ sections.push('');
296
+ }
297
+
298
+ // ─── Recent Session Context ────────────────────────
299
+ const recentSessions = vault.listMemories({
300
+ type: 'session',
301
+ projectPath,
302
+ limit: sessionLimit,
303
+ });
304
+
305
+ if (recentSessions.length > 0) {
306
+ sections.push('## Recent Sessions');
307
+ sections.push('');
308
+ for (const session of recentSessions) {
309
+ sections.push(`### ${session.createdAt}`);
310
+ sections.push('');
311
+ if (session.summary) {
312
+ sections.push(session.summary);
313
+ sections.push('');
314
+ }
315
+ if (session.nextSteps && session.nextSteps.length > 0) {
316
+ sections.push('**Next steps:**');
317
+ for (const step of session.nextSteps) {
318
+ sections.push(`- ${step}`);
319
+ }
320
+ sections.push('');
321
+ }
322
+ if (session.decisions && session.decisions.length > 0) {
323
+ sections.push('**Decisions:**');
324
+ for (const d of session.decisions) {
325
+ sections.push(`- ${d}`);
326
+ }
327
+ sections.push('');
328
+ }
329
+ if (session.filesModified && session.filesModified.length > 0) {
330
+ sections.push(`**Files modified:** ${session.filesModified.join(', ')}`);
331
+ sections.push('');
332
+ }
333
+ }
334
+ } else {
335
+ sections.push('## Recent Sessions');
336
+ sections.push('');
337
+ sections.push('No recent session memories found.');
338
+ sections.push('');
339
+ }
340
+
341
+ // ─── Resumption Hints ──────────────────────────────
342
+ sections.push('## Resumption');
343
+ sections.push('');
344
+ sections.push('Use this document to restore context after a context window transition.');
345
+ sections.push('');
346
+ if (activePlans.length > 0) {
347
+ const plan = activePlans[0];
348
+ const pendingTasks = plan.tasks.filter(
349
+ (t) => t.status === 'pending' || t.status === 'in_progress',
350
+ );
351
+ if (pendingTasks.length > 0) {
352
+ sections.push('**Immediate next actions:**');
353
+ for (const t of pendingTasks.slice(0, 5)) {
354
+ sections.push(
355
+ `- ${t.status === 'in_progress' ? '[IN PROGRESS]' : '[PENDING]'} ${t.title}`,
356
+ );
357
+ }
358
+ sections.push('');
359
+ }
360
+ if (plan.status === 'executing') {
361
+ sections.push(
362
+ '> Plan is in `executing` state. Continue with pending tasks or call `op:plan_reconcile` if complete.',
363
+ );
364
+ } else if (plan.status === 'reconciling') {
365
+ sections.push(
366
+ '> Plan is in `reconciling` state. Call `op:plan_complete_lifecycle` to finalize.',
367
+ );
368
+ }
369
+ }
370
+
371
+ const markdown = sections.join('\n');
372
+
373
+ return {
374
+ handoff: markdown,
375
+ meta: {
376
+ activePlanCount: activePlans.length,
377
+ activePlanId: activePlans.length > 0 ? activePlans[0].id : null,
378
+ recentSessionCount: recentSessions.length,
379
+ generatedAt: now,
380
+ },
381
+ };
382
+ },
383
+ },
384
+
198
385
  // ─── Satellite ops ───────────────────────────────────────────
199
386
  ...createMemoryExtraOps(runtime),
200
387
  ...createMemoryCrossProjectOps(runtime),
@@ -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 {
@@ -847,7 +999,7 @@ export function createOrchestrateOps(
847
999
  }
848
1000
 
849
1001
  // 2. Detect GitHub context
850
- const ctx = detectGitHubContext(projectPath);
1002
+ const ctx = await detectGitHubContext(projectPath);
851
1003
  if (!ctx) {
852
1004
  return {
853
1005
  status: 'skipped',
@@ -885,7 +1037,7 @@ export function createOrchestrateOps(
885
1037
  };
886
1038
  }
887
1039
 
888
- const updated = updateGitHubIssueBody(ctx.repo, linkToIssue, body);
1040
+ const updated = await updateGitHubIssueBody(ctx.repo, linkToIssue, body);
889
1041
  if (!updated) {
890
1042
  return {
891
1043
  status: 'error',
@@ -949,7 +1101,7 @@ export function createOrchestrateOps(
949
1101
  continue;
950
1102
  }
951
1103
 
952
- const issueNumber = createGitHubIssue(ctx.repo, task.title, body, {
1104
+ const issueNumber = await createGitHubIssue(ctx.repo, task.title, body, {
953
1105
  milestone: milestoneNumber,
954
1106
  labels: labels.length > 0 ? labels : undefined,
955
1107
  });
@@ -81,7 +81,10 @@ vi.mock('../governance/governance.js', () => ({
81
81
  }));
82
82
 
83
83
  vi.mock('../loop/loop-manager.js', () => ({
84
- LoopManager: mockClass(),
84
+ LoopManager: vi.fn(function (this: Record<string, unknown>) {
85
+ this.isActive = vi.fn().mockReturnValue(false);
86
+ this.cancelLoop = vi.fn();
87
+ }),
85
88
  }));
86
89
 
87
90
  vi.mock('../control/identity-manager.js', () => ({
@@ -185,7 +188,9 @@ vi.mock('../context/context-engine.js', () => ({
185
188
  }));
186
189
 
187
190
  vi.mock('../agency/agency-manager.js', () => ({
188
- AgencyManager: mockClass(),
191
+ AgencyManager: vi.fn(function (this: Record<string, unknown>) {
192
+ this.disable = vi.fn();
193
+ }),
189
194
  }));
190
195
 
191
196
  vi.mock('../vault/knowledge-review.js', () => ({
@@ -220,6 +225,7 @@ vi.mock('../queue/job-queue.js', () => ({
220
225
  vi.mock('../queue/pipeline-runner.js', () => ({
221
226
  PipelineRunner: vi.fn(function (this: Record<string, unknown>) {
222
227
  this.registerHandler = vi.fn();
228
+ this.stop = vi.fn();
223
229
  }),
224
230
  }));
225
231
 
@@ -261,6 +267,18 @@ vi.mock('./context-health.js', () => ({
261
267
  }),
262
268
  }));
263
269
 
270
+ vi.mock('./shutdown-registry.js', () => ({
271
+ ShutdownRegistry: vi.fn(function (this: Record<string, unknown>) {
272
+ this.register = vi.fn();
273
+ this.closeAll = vi.fn().mockResolvedValue(undefined);
274
+ this.closeAllSync = vi.fn();
275
+ this.size = 0;
276
+ this.isClosed = false;
277
+ this.entries = [];
278
+ this.closed = false;
279
+ }),
280
+ }));
281
+
264
282
  vi.mock('node:fs', () => ({
265
283
  existsSync: vi.fn().mockReturnValue(false),
266
284
  mkdirSync: vi.fn(),
@@ -360,4 +378,34 @@ describe('createAgentRuntime', () => {
360
378
  it('initializes context health monitor', () => {
361
379
  expect(runtime.contextHealth).toBeDefined();
362
380
  });
381
+
382
+ it('initializes shutdown registry', () => {
383
+ expect(runtime.shutdownRegistry).toBeDefined();
384
+ expect(runtime.shutdownRegistry.register).toBeDefined();
385
+ });
386
+
387
+ it('registers cleanup callbacks with shutdown registry', () => {
388
+ // vaultManager, pipelineRunner, agencyManager, loopManager
389
+ expect(runtime.shutdownRegistry.register).toHaveBeenCalledWith(
390
+ 'vaultManager',
391
+ expect.any(Function),
392
+ );
393
+ expect(runtime.shutdownRegistry.register).toHaveBeenCalledWith(
394
+ 'pipelineRunner',
395
+ expect.any(Function),
396
+ );
397
+ expect(runtime.shutdownRegistry.register).toHaveBeenCalledWith(
398
+ 'agencyManager',
399
+ expect.any(Function),
400
+ );
401
+ expect(runtime.shutdownRegistry.register).toHaveBeenCalledWith(
402
+ 'loopManager',
403
+ expect.any(Function),
404
+ );
405
+ });
406
+
407
+ it('close() calls shutdownRegistry.closeAllSync()', () => {
408
+ runtime.close();
409
+ expect(runtime.shutdownRegistry.closeAllSync).toHaveBeenCalled();
410
+ });
363
411
  });