@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
@@ -36,6 +36,9 @@ import type { PipelineRunner } from '../queue/pipeline-runner.js';
36
36
  import type { OperatorProfileStore } from '../operator/operator-profile.js';
37
37
  import type { OperatorContextStore } from '../operator/operator-context-store.js';
38
38
  import type { ContextHealthMonitor } from './context-health.js';
39
+ import type { ShutdownRegistry } from './shutdown-registry.js';
40
+ import type { RuntimeAdapterRegistry } from '../adapters/registry.js';
41
+ import type { SubagentDispatcher } from '../subagent/dispatcher.js';
39
42
 
40
43
  /**
41
44
  * Configuration for creating an agent runtime.
@@ -128,10 +131,16 @@ export interface AgentRuntime {
128
131
  persona: import('../persona/types.js').PersonaConfig;
129
132
  /** Generated persona system instructions for LLM context. */
130
133
  personaInstructions: import('../persona/types.js').PersonaSystemInstructions;
134
+ /** Runtime adapter registry — dispatch work to different AI CLIs. */
135
+ adapterRegistry: RuntimeAdapterRegistry;
136
+ /** Subagent dispatcher — spawn and manage child agent processes. */
137
+ subagentDispatcher: SubagentDispatcher;
131
138
  /** Context health monitor — tracks tool call volume and context window fill. */
132
139
  contextHealth: ContextHealthMonitor;
140
+ /** Shutdown registry — centralized cleanup for timers, watchers, child processes. */
141
+ shutdownRegistry: ShutdownRegistry;
133
142
  /** Timestamp (ms since epoch) when this runtime was created. */
134
143
  createdAt: number;
135
- /** Close the vault database connection. Call on shutdown. */
144
+ /** Close all runtime resources (vault, timers, watchers). Call on shutdown. */
136
145
  close(): void;
137
146
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Per-agent-type semaphore for controlling concurrent subagent runs.
3
+ *
4
+ * Pure promise-based — zero external deps. FIFO ordering guarantees
5
+ * the first waiter is the first to acquire a slot when one frees up.
6
+ */
7
+
8
+ interface TypeState {
9
+ active: number;
10
+ waiters: Array<() => void>;
11
+ }
12
+
13
+ const DEFAULT_MAX_CONCURRENT = 3;
14
+
15
+ export class ConcurrencyManager {
16
+ private state: Map<string, TypeState> = new Map();
17
+
18
+ /**
19
+ * Acquire a concurrency slot for the given adapter type.
20
+ * Resolves immediately if a slot is available, otherwise queues
21
+ * and resolves when a slot frees up (FIFO).
22
+ */
23
+ async acquire(type: string, maxConcurrent: number = DEFAULT_MAX_CONCURRENT): Promise<void> {
24
+ const entry = this.getOrCreate(type);
25
+
26
+ if (entry.active < maxConcurrent) {
27
+ entry.active++;
28
+ return;
29
+ }
30
+
31
+ // At capacity — park until a slot opens.
32
+ return new Promise<void>((resolve) => {
33
+ entry.waiters.push(() => {
34
+ entry.active++;
35
+ resolve();
36
+ });
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Release a concurrency slot for the given adapter type.
42
+ * If waiters exist, the first one (FIFO) is unblocked.
43
+ * No-op if the type is not tracked.
44
+ */
45
+ release(type: string): void {
46
+ const entry = this.state.get(type);
47
+ if (!entry) return;
48
+
49
+ entry.active = Math.max(0, entry.active - 1);
50
+
51
+ if (entry.waiters.length > 0) {
52
+ const next = entry.waiters.shift()!;
53
+ next();
54
+ }
55
+ }
56
+
57
+ /** Return the number of active slots for a type (0 if untracked). */
58
+ getActive(type: string): number {
59
+ return this.state.get(type)?.active ?? 0;
60
+ }
61
+
62
+ /** Return the number of waiters queued for a type (0 if untracked). */
63
+ getWaiting(type: string): number {
64
+ return this.state.get(type)?.waiters.length ?? 0;
65
+ }
66
+
67
+ /** Clear all state, resolving any pending waiters immediately. */
68
+ reset(): void {
69
+ for (const entry of this.state.values()) {
70
+ for (const waiter of entry.waiters) {
71
+ waiter();
72
+ }
73
+ entry.waiters.length = 0;
74
+ entry.active = 0;
75
+ }
76
+ this.state.clear();
77
+ }
78
+
79
+ // ── internal ──────────────────────────────────────────────────────
80
+
81
+ private getOrCreate(type: string): TypeState {
82
+ let entry = this.state.get(type);
83
+ if (!entry) {
84
+ entry = { active: 0, waiters: [] };
85
+ this.state.set(type, entry);
86
+ }
87
+ return entry;
88
+ }
89
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * SubagentDispatcher — the core orchestrator for subagent execution.
3
+ *
4
+ * Composes: TaskCheckout, WorkspaceResolver, ConcurrencyManager,
5
+ * OrphanReaper, and RuntimeAdapterRegistry to dispatch tasks to
6
+ * child agent processes.
7
+ */
8
+
9
+ import type { RuntimeAdapterRegistry } from '../adapters/registry.js';
10
+ import type {
11
+ SubagentTask,
12
+ SubagentResult,
13
+ SubagentStatus,
14
+ DispatchOptions,
15
+ AggregatedResult,
16
+ } from './types.js';
17
+ import { TaskCheckout } from './task-checkout.js';
18
+ import { WorkspaceResolver } from './workspace-resolver.js';
19
+ import { ConcurrencyManager } from './concurrency-manager.js';
20
+ import { OrphanReaper } from './orphan-reaper.js';
21
+ import { aggregate } from './result-aggregator.js';
22
+
23
+ const DEFAULT_TIMEOUT = 300_000; // 5 minutes
24
+ const DEFAULT_MAX_CONCURRENT = 3;
25
+
26
+ export interface SubagentDispatcherConfig {
27
+ /** RuntimeAdapterRegistry for looking up adapters by type */
28
+ adapterRegistry: RuntimeAdapterRegistry;
29
+ /** Base directory for git worktree isolation */
30
+ baseDir?: string;
31
+ }
32
+
33
+ export class SubagentDispatcher {
34
+ private readonly checkout = new TaskCheckout();
35
+ private readonly workspace: WorkspaceResolver;
36
+ private readonly concurrency = new ConcurrencyManager();
37
+ private readonly reaper: OrphanReaper;
38
+ private readonly adapterRegistry: RuntimeAdapterRegistry;
39
+
40
+ constructor(config: SubagentDispatcherConfig) {
41
+ this.adapterRegistry = config.adapterRegistry;
42
+ this.workspace = new WorkspaceResolver(config.baseDir ?? process.cwd());
43
+ this.reaper = new OrphanReaper((taskId) => {
44
+ // On orphan: release the task claim and clean up workspace
45
+ this.checkout.release(taskId);
46
+ this.workspace.cleanup(taskId);
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Dispatch one or more tasks to subagents.
52
+ *
53
+ * Tasks run in parallel by default (controlled by options.parallel).
54
+ * Each task goes through: claim → resolve workspace → acquire slot →
55
+ * execute via adapter → collect result.
56
+ */
57
+ async dispatch(tasks: SubagentTask[], options: DispatchOptions = {}): Promise<AggregatedResult> {
58
+ const {
59
+ parallel = true,
60
+ maxConcurrent = DEFAULT_MAX_CONCURRENT,
61
+ worktreeIsolation = false,
62
+ timeout = DEFAULT_TIMEOUT,
63
+ onTaskUpdate,
64
+ } = options;
65
+
66
+ if (tasks.length === 0) {
67
+ return aggregate([]);
68
+ }
69
+
70
+ // Resolve dependency order
71
+ const ordered = this.resolveDependencies(tasks);
72
+
73
+ if (parallel) {
74
+ // Run independent tasks in parallel, respecting dependencies
75
+ const results = await this.dispatchParallel(ordered, {
76
+ maxConcurrent,
77
+ worktreeIsolation,
78
+ timeout,
79
+ onTaskUpdate,
80
+ });
81
+ return aggregate(results);
82
+ }
83
+
84
+ // Sequential dispatch — await in loop is intentional (tasks must run one at a time)
85
+ const results: SubagentResult[] = [];
86
+ for (const task of ordered) {
87
+ // eslint-disable-line no-await-in-loop
88
+ onTaskUpdate?.(task.taskId, 'running');
89
+ const result = await this.executeTask(task, worktreeIsolation, timeout);
90
+ results.push(result);
91
+ onTaskUpdate?.(task.taskId, result.status);
92
+
93
+ // Stop on failure in sequential mode
94
+ if (result.exitCode !== 0) break;
95
+ }
96
+
97
+ return aggregate(results);
98
+ }
99
+
100
+ /** Clean up all resources (worktrees, claims, concurrency) */
101
+ cleanup(): void {
102
+ this.workspace.cleanupAll();
103
+ this.checkout.releaseAll();
104
+ this.concurrency.reset();
105
+ this.reaper.clear();
106
+ }
107
+
108
+ /** Run orphan detection and cleanup */
109
+ reapOrphans(): SubagentResult[] {
110
+ const orphaned = this.reaper.reap();
111
+ return orphaned.map((p) => ({
112
+ taskId: p.taskId,
113
+ status: 'orphaned' as const,
114
+ exitCode: 1,
115
+ error: `Process ${p.pid} died unexpectedly`,
116
+ durationMs: Date.now() - p.registeredAt,
117
+ pid: p.pid,
118
+ }));
119
+ }
120
+
121
+ // ── Internal ──────────────────────────────────────────────────────
122
+
123
+ private async dispatchParallel(
124
+ tasks: SubagentTask[],
125
+ opts: {
126
+ maxConcurrent: number;
127
+ worktreeIsolation: boolean;
128
+ timeout: number;
129
+ onTaskUpdate?: (taskId: string, status: SubagentStatus) => void;
130
+ },
131
+ ): Promise<SubagentResult[]> {
132
+ const results = new Map<string, SubagentResult>();
133
+ const pending = new Map<string, SubagentTask>();
134
+ const completed = new Set<string>();
135
+
136
+ // Initialize all tasks as pending
137
+ for (const task of tasks) {
138
+ pending.set(task.taskId, task);
139
+ }
140
+
141
+ // Process in waves until all done
142
+ while (pending.size > 0) {
143
+ // Find tasks whose dependencies are all completed
144
+ const ready: SubagentTask[] = [];
145
+ for (const [_id, task] of pending) {
146
+ const deps = task.dependencies ?? [];
147
+ if (deps.every((d) => completed.has(d))) {
148
+ ready.push(task);
149
+ }
150
+ }
151
+
152
+ if (ready.length === 0 && pending.size > 0) {
153
+ // Deadlock — remaining tasks have unmet dependencies
154
+ for (const [deadId, task] of pending) {
155
+ results.set(deadId, {
156
+ taskId: deadId,
157
+ status: 'failed',
158
+ exitCode: 1,
159
+ error: `Unresolvable dependencies: ${(task.dependencies ?? []).filter((d) => !completed.has(d)).join(', ')}`,
160
+ durationMs: 0,
161
+ });
162
+ }
163
+ break;
164
+ }
165
+
166
+ // Dispatch ready tasks in parallel with concurrency control
167
+ // eslint-disable-next-line no-await-in-loop -- waves must complete before next wave
168
+ const waveResults = await Promise.allSettled(
169
+ ready.map(async (task) => {
170
+ opts.onTaskUpdate?.(task.taskId, 'running');
171
+ await this.concurrency.acquire(task.runtime ?? 'default', opts.maxConcurrent);
172
+ try {
173
+ return await this.executeTask(
174
+ task,
175
+ opts.worktreeIsolation,
176
+ task.timeout ?? opts.timeout,
177
+ );
178
+ } finally {
179
+ this.concurrency.release(task.runtime ?? 'default');
180
+ }
181
+ }),
182
+ );
183
+
184
+ // Collect results
185
+ for (let i = 0; i < ready.length; i++) {
186
+ const task = ready[i];
187
+ const settled = waveResults[i];
188
+ const result: SubagentResult =
189
+ settled.status === 'fulfilled'
190
+ ? settled.value
191
+ : {
192
+ taskId: task.taskId,
193
+ status: 'failed',
194
+ exitCode: 1,
195
+ error: settled.reason?.message ?? 'Unknown error',
196
+ durationMs: 0,
197
+ };
198
+
199
+ results.set(task.taskId, result);
200
+ completed.add(task.taskId);
201
+ pending.delete(task.taskId);
202
+ opts.onTaskUpdate?.(task.taskId, result.status);
203
+ }
204
+ }
205
+
206
+ // Return in original task order
207
+ return tasks.map((t) => results.get(t.taskId)!);
208
+ }
209
+
210
+ private async executeTask(
211
+ task: SubagentTask,
212
+ worktreeIsolation: boolean,
213
+ timeout: number,
214
+ ): Promise<SubagentResult> {
215
+ const startTime = Date.now();
216
+
217
+ // 1. Claim the task
218
+ const claimed = this.checkout.claim(task.taskId, 'dispatcher');
219
+ if (!claimed) {
220
+ return {
221
+ taskId: task.taskId,
222
+ status: 'failed',
223
+ exitCode: 1,
224
+ error: 'Task already claimed by another process',
225
+ durationMs: Date.now() - startTime,
226
+ };
227
+ }
228
+
229
+ // 2. Resolve workspace
230
+ const workspace = this.workspace.resolve(task.taskId, task.workspace, worktreeIsolation);
231
+
232
+ // 3. Get adapter
233
+ const adapterType = task.runtime ?? this.getDefaultAdapterType();
234
+ let adapter;
235
+ try {
236
+ adapter = this.adapterRegistry.get(adapterType);
237
+ } catch {
238
+ this.checkout.release(task.taskId);
239
+ return {
240
+ taskId: task.taskId,
241
+ status: 'failed',
242
+ exitCode: 1,
243
+ error: `Adapter '${adapterType}' not found in registry`,
244
+ durationMs: Date.now() - startTime,
245
+ };
246
+ }
247
+
248
+ // 4. Execute with timeout
249
+ try {
250
+ const resultPromise = adapter.execute({
251
+ runId: `subagent-${task.taskId}-${Date.now()}`,
252
+ prompt: task.prompt,
253
+ workspace,
254
+ config: { ...task.config, timeout },
255
+ });
256
+
257
+ const timeoutPromise = new Promise<never>((_, reject) => {
258
+ setTimeout(() => reject(new Error('Task timed out')), timeout);
259
+ });
260
+
261
+ const adapterResult = await Promise.race([resultPromise, timeoutPromise]);
262
+
263
+ return {
264
+ taskId: task.taskId,
265
+ status: adapterResult.exitCode === 0 ? 'completed' : 'failed',
266
+ exitCode: adapterResult.exitCode,
267
+ summary: adapterResult.summary,
268
+ usage: adapterResult.usage,
269
+ sessionState: adapterResult.sessionState,
270
+ durationMs: Date.now() - startTime,
271
+ };
272
+ } catch (err) {
273
+ return {
274
+ taskId: task.taskId,
275
+ status: 'failed',
276
+ exitCode: 1,
277
+ error: err instanceof Error ? err.message : String(err),
278
+ durationMs: Date.now() - startTime,
279
+ };
280
+ } finally {
281
+ // Cleanup
282
+ this.checkout.release(task.taskId);
283
+ if (worktreeIsolation) {
284
+ this.workspace.cleanup(task.taskId);
285
+ }
286
+ }
287
+ }
288
+
289
+ /** Topological sort by dependencies (stable — preserves input order for equal deps) */
290
+ private resolveDependencies(tasks: SubagentTask[]): SubagentTask[] {
291
+ const taskMap = new Map(tasks.map((t) => [t.taskId, t]));
292
+ const sorted: SubagentTask[] = [];
293
+ const visited = new Set<string>();
294
+ const visiting = new Set<string>();
295
+
296
+ const visit = (id: string) => {
297
+ if (visited.has(id)) return;
298
+ if (visiting.has(id)) return; // cycle — skip
299
+ visiting.add(id);
300
+
301
+ const task = taskMap.get(id);
302
+ if (task) {
303
+ for (const dep of task.dependencies ?? []) {
304
+ visit(dep);
305
+ }
306
+ visited.add(id);
307
+ visiting.delete(id);
308
+ sorted.push(task);
309
+ }
310
+ };
311
+
312
+ for (const task of tasks) {
313
+ visit(task.taskId);
314
+ }
315
+
316
+ return sorted;
317
+ }
318
+
319
+ private getDefaultAdapterType(): string {
320
+ try {
321
+ return this.adapterRegistry.getDefault().type;
322
+ } catch {
323
+ return 'claude-code';
324
+ }
325
+ }
326
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Subagent runtime engine — spawn, manage, and aggregate results
3
+ * from child agent processes.
4
+ *
5
+ * @module subagent
6
+ */
7
+
8
+ // Types
9
+ export type {
10
+ SubagentTask,
11
+ SubagentStatus,
12
+ SubagentResult,
13
+ DispatchOptions,
14
+ AggregatedResult,
15
+ ClaimInfo,
16
+ WorktreeInfo,
17
+ TrackedProcess,
18
+ } from './types.js';
19
+
20
+ // Components
21
+ export { TaskCheckout } from './task-checkout.js';
22
+ export { WorkspaceResolver } from './workspace-resolver.js';
23
+ export { ConcurrencyManager } from './concurrency-manager.js';
24
+ export { OrphanReaper } from './orphan-reaper.js';
25
+ export { aggregate as aggregateResults } from './result-aggregator.js';
26
+
27
+ // Dispatcher
28
+ export { SubagentDispatcher } from './dispatcher.js';
@@ -0,0 +1,82 @@
1
+ /**
2
+ * OrphanReaper — tracks spawned child processes and detects dead/orphaned ones.
3
+ *
4
+ * Uses `process.kill(pid, 0)` (signal 0) as an existence check:
5
+ * - No error → process is alive
6
+ * - ESRCH → process is dead (reap it)
7
+ * - EPERM → process is alive but we lack permission to signal it
8
+ */
9
+
10
+ import type { TrackedProcess } from './types.js';
11
+
12
+ export class OrphanReaper {
13
+ private readonly tracked = new Map<number, TrackedProcess>();
14
+ private readonly onOrphan?: (taskId: string, pid: number) => void;
15
+
16
+ constructor(onOrphan?: (taskId: string, pid: number) => void) {
17
+ this.onOrphan = onOrphan;
18
+ }
19
+
20
+ /** Start tracking a process. */
21
+ register(pid: number, taskId: string): void {
22
+ this.tracked.set(pid, { pid, taskId, registeredAt: Date.now() });
23
+ }
24
+
25
+ /** Stop tracking a process (called on normal completion). */
26
+ unregister(pid: number): void {
27
+ this.tracked.delete(pid);
28
+ }
29
+
30
+ /**
31
+ * Check each tracked PID for liveness. Dead processes are removed from
32
+ * tracking, the onOrphan callback is invoked, and they are returned.
33
+ */
34
+ reap(): TrackedProcess[] {
35
+ const reaped: TrackedProcess[] = [];
36
+
37
+ for (const [pid, entry] of this.tracked) {
38
+ if (!this.isAlive(pid)) {
39
+ this.tracked.delete(pid);
40
+ this.onOrphan?.(entry.taskId, pid);
41
+ reaped.push(entry);
42
+ }
43
+ }
44
+
45
+ return reaped;
46
+ }
47
+
48
+ /** Return all currently tracked processes. */
49
+ listTracked(): TrackedProcess[] {
50
+ return [...this.tracked.values()];
51
+ }
52
+
53
+ /** Check if a PID is currently tracked. */
54
+ isTracked(pid: number): boolean {
55
+ return this.tracked.has(pid);
56
+ }
57
+
58
+ /** Clear all tracked processes without killing them. */
59
+ clear(): void {
60
+ this.tracked.clear();
61
+ }
62
+
63
+ // ── internals ──────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * Signal-0 existence check.
67
+ * - No error → alive
68
+ * - EPERM → alive (exists but we can't signal it)
69
+ * - ESRCH → dead
70
+ */
71
+ private isAlive(pid: number): boolean {
72
+ try {
73
+ process.kill(pid, 0);
74
+ return true;
75
+ } catch (err: unknown) {
76
+ const code = (err as NodeJS.ErrnoException).code;
77
+ if (code === 'EPERM') return true;
78
+ // ESRCH or any other error → treat as dead
79
+ return false;
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Result aggregator — merges results from multiple parallel subagent
3
+ * executions into a single summary.
4
+ */
5
+
6
+ import type { AggregatedResult, SubagentResult } from './types.js';
7
+
8
+ export function aggregate(results: SubagentResult[]): AggregatedResult {
9
+ if (results.length === 0) {
10
+ return {
11
+ status: 'all-passed',
12
+ totalTasks: 0,
13
+ completed: 0,
14
+ failed: 0,
15
+ totalUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
16
+ filesChanged: [],
17
+ combinedSummary: '',
18
+ durationMs: 0,
19
+ results: [],
20
+ };
21
+ }
22
+
23
+ const completed = results.filter((r) => r.exitCode === 0).length;
24
+ const failed = results.length - completed;
25
+
26
+ const status: AggregatedResult['status'] =
27
+ failed === 0 ? 'all-passed' : completed === 0 ? 'all-failed' : 'partial';
28
+
29
+ const totalUsage = {
30
+ inputTokens: 0,
31
+ outputTokens: 0,
32
+ totalTokens: 0,
33
+ };
34
+ for (const r of results) {
35
+ totalUsage.inputTokens += r.usage?.inputTokens ?? 0;
36
+ totalUsage.outputTokens += r.usage?.outputTokens ?? 0;
37
+ totalUsage.totalTokens += r.usage?.totalTokens ?? 0;
38
+ }
39
+
40
+ const fileSet = new Set<string>();
41
+ for (const r of results) {
42
+ if (r.filesChanged) {
43
+ for (const f of r.filesChanged) fileSet.add(f);
44
+ }
45
+ }
46
+
47
+ const combinedSummary = results
48
+ .filter((r) => r.summary)
49
+ .map((r) => `[${r.taskId}] ${r.summary}`)
50
+ .join('\n');
51
+
52
+ // Parallel wall-clock: max of all durations
53
+ const durationMs = Math.max(...results.map((r) => r.durationMs));
54
+
55
+ return {
56
+ status,
57
+ totalTasks: results.length,
58
+ completed,
59
+ failed,
60
+ totalUsage,
61
+ filesChanged: [...fileSet],
62
+ combinedSummary,
63
+ durationMs,
64
+ results,
65
+ };
66
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Atomic task claim system — prevents two subagents from working the same task.
3
+ *
4
+ * Pure in-memory Map backing store. No async, no external deps.
5
+ */
6
+
7
+ import type { ClaimInfo } from './types.js';
8
+
9
+ export class TaskCheckout {
10
+ private readonly claims = new Map<string, ClaimInfo>();
11
+
12
+ /**
13
+ * Attempt to claim a task for a claimer.
14
+ * Returns true if the claim succeeds (or the same claimer already holds it).
15
+ * Returns false if the task is already claimed by a different claimer.
16
+ */
17
+ claim(taskId: string, claimerId: string): boolean {
18
+ const existing = this.claims.get(taskId);
19
+ if (existing) {
20
+ return existing.claimerId === claimerId;
21
+ }
22
+ this.claims.set(taskId, { taskId, claimerId, claimedAt: Date.now() });
23
+ return true;
24
+ }
25
+
26
+ /**
27
+ * Release a claimed task. Returns true if released, false if not claimed.
28
+ */
29
+ release(taskId: string): boolean {
30
+ return this.claims.delete(taskId);
31
+ }
32
+
33
+ /**
34
+ * Get claim info for a task, or undefined if unclaimed.
35
+ */
36
+ getClaimer(taskId: string): ClaimInfo | undefined {
37
+ return this.claims.get(taskId);
38
+ }
39
+
40
+ /**
41
+ * List all active claims.
42
+ */
43
+ listClaimed(): ClaimInfo[] {
44
+ return [...this.claims.values()];
45
+ }
46
+
47
+ /**
48
+ * Check whether a task is available (unclaimed).
49
+ */
50
+ isAvailable(taskId: string): boolean {
51
+ return !this.claims.has(taskId);
52
+ }
53
+
54
+ /**
55
+ * Clear all claims. Useful for cleanup between dispatch rounds.
56
+ */
57
+ releaseAll(): void {
58
+ this.claims.clear();
59
+ }
60
+ }