@runcore-sh/runcore 0.4.0 → 0.5.1

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 (351) hide show
  1. package/dictionary.json +2 -2
  2. package/dist/activity/log.js +2 -2
  3. package/dist/activity/log.js.map +1 -1
  4. package/dist/agents/governed-spawn.d.ts.map +1 -1
  5. package/dist/cli.js +101 -11
  6. package/dist/cli.js.map +1 -1
  7. package/dist/extensions/cache.d.ts +57 -0
  8. package/dist/extensions/cache.d.ts.map +1 -0
  9. package/dist/extensions/cache.js +173 -0
  10. package/dist/extensions/cache.js.map +1 -0
  11. package/dist/extensions/client.d.ts +55 -0
  12. package/dist/extensions/client.d.ts.map +1 -0
  13. package/dist/extensions/client.js +120 -0
  14. package/dist/extensions/client.js.map +1 -0
  15. package/dist/extensions/index.d.ts +13 -0
  16. package/dist/extensions/index.d.ts.map +1 -0
  17. package/dist/extensions/index.js +12 -0
  18. package/dist/extensions/index.js.map +1 -0
  19. package/dist/extensions/loader.d.ts +50 -0
  20. package/dist/extensions/loader.d.ts.map +1 -0
  21. package/dist/extensions/loader.js +166 -0
  22. package/dist/extensions/loader.js.map +1 -0
  23. package/dist/extensions/manifest.d.ts +38 -0
  24. package/dist/extensions/manifest.d.ts.map +1 -0
  25. package/dist/extensions/manifest.js +17 -0
  26. package/dist/extensions/manifest.js.map +1 -0
  27. package/dist/extensions/stubs.d.ts +27 -0
  28. package/dist/extensions/stubs.d.ts.map +1 -0
  29. package/dist/extensions/stubs.js +45 -0
  30. package/dist/extensions/stubs.js.map +1 -0
  31. package/dist/lib/audit.js +2 -2
  32. package/dist/lib/audit.js.map +1 -1
  33. package/dist/lib/brain-migrate.d.ts +21 -0
  34. package/dist/lib/brain-migrate.d.ts.map +1 -0
  35. package/dist/lib/brain-migrate.js +137 -0
  36. package/dist/lib/brain-migrate.js.map +1 -0
  37. package/dist/lib/paths.d.ts +27 -0
  38. package/dist/lib/paths.d.ts.map +1 -1
  39. package/dist/lib/paths.js +65 -0
  40. package/dist/lib/paths.js.map +1 -1
  41. package/dist/llm/call-log.d.ts +40 -0
  42. package/dist/llm/call-log.d.ts.map +1 -0
  43. package/dist/llm/call-log.js +35 -0
  44. package/dist/llm/call-log.js.map +1 -0
  45. package/dist/llm/complete.d.ts +6 -0
  46. package/dist/llm/complete.d.ts.map +1 -1
  47. package/dist/llm/complete.js +27 -0
  48. package/dist/llm/complete.js.map +1 -1
  49. package/dist/mcp-server.js +118 -2
  50. package/dist/mcp-server.js.map +1 -1
  51. package/dist/memory/file-backed.d.ts +4 -0
  52. package/dist/memory/file-backed.d.ts.map +1 -1
  53. package/dist/memory/file-backed.js +4 -0
  54. package/dist/memory/file-backed.js.map +1 -1
  55. package/dist/memory/vector-index.d.ts +4 -12
  56. package/dist/memory/vector-index.d.ts.map +1 -1
  57. package/dist/memory/vector-index.js +11 -93
  58. package/dist/memory/vector-index.js.map +1 -1
  59. package/dist/search/brain-docs.d.ts +17 -7
  60. package/dist/search/brain-docs.d.ts.map +1 -1
  61. package/dist/search/brain-docs.js +170 -52
  62. package/dist/search/brain-docs.js.map +1 -1
  63. package/dist/search/brain-rag.d.ts +45 -0
  64. package/dist/search/brain-rag.d.ts.map +1 -0
  65. package/dist/search/brain-rag.js +275 -0
  66. package/dist/search/brain-rag.js.map +1 -0
  67. package/dist/search/chunker.d.ts +24 -0
  68. package/dist/search/chunker.d.ts.map +1 -0
  69. package/dist/search/chunker.js +95 -0
  70. package/dist/search/chunker.js.map +1 -0
  71. package/dist/search/embedder.d.ts +16 -0
  72. package/dist/search/embedder.d.ts.map +1 -0
  73. package/dist/search/embedder.js +108 -0
  74. package/dist/search/embedder.js.map +1 -0
  75. package/dist/search/file-watcher.d.ts +11 -0
  76. package/dist/search/file-watcher.d.ts.map +1 -0
  77. package/dist/search/file-watcher.js +86 -0
  78. package/dist/search/file-watcher.js.map +1 -0
  79. package/dist/server.d.ts.map +1 -1
  80. package/dist/server.js +814 -472
  81. package/dist/server.js.map +1 -1
  82. package/dist/sessions/store.d.ts +9 -0
  83. package/dist/sessions/store.d.ts.map +1 -1
  84. package/dist/sessions/store.js.map +1 -1
  85. package/dist/settings.d.ts +26 -0
  86. package/dist/settings.d.ts.map +1 -1
  87. package/dist/settings.js +78 -2
  88. package/dist/settings.js.map +1 -1
  89. package/dist/tracing/init.d.ts +1 -1
  90. package/dist/tracing/init.d.ts.map +1 -1
  91. package/dist/utils/logger.js +2 -2
  92. package/dist/utils/logger.js.map +1 -1
  93. package/module-tiers.json +164 -0
  94. package/package.json +9 -13
  95. package/public/avatar/cache/1184385ec5522b57.mp4 +0 -0
  96. package/public/avatar/cache/1f15f6a1ebd7e439.mp4 +0 -0
  97. package/public/avatar/cache/2c7e47ff0bdeb8d1.mp4 +0 -0
  98. package/public/avatar/cache/5f308566f7abb8f2.mp4 +0 -0
  99. package/public/avatar/cache/62f9cfba848d724e.mp4 +0 -0
  100. package/public/avatar/cache/6d64e657e6bf2aab.mp4 +0 -0
  101. package/public/avatar/cache/763ad0349e0b6f26.mp4 +0 -0
  102. package/public/avatar/cache/81a516cfd461b2b9.mp4 +0 -0
  103. package/public/avatar/cache/9366de15fd6910ca.mp4 +0 -0
  104. package/public/avatar/cache/ade41a846b283895.mp4 +0 -0
  105. package/public/avatar/cache/b6066e5c65383eec.mp4 +0 -0
  106. package/public/avatar/cache/edadb75d37891fc7.mp4 +0 -0
  107. package/public/avatar/cache/f0ae159640621dd9.mp4 +0 -0
  108. package/public/avatar/cache/fc2e5419adf29d96.mp4 +0 -0
  109. package/public/index.html +379 -59
  110. package/dist/agents/autonomous.js +0 -749
  111. package/dist/agents/autonomous.js.map +0 -1
  112. package/dist/agents/commit.js +0 -113
  113. package/dist/agents/commit.js.map +0 -1
  114. package/dist/agents/continue.js +0 -158
  115. package/dist/agents/continue.js.map +0 -1
  116. package/dist/agents/cooldown.js +0 -397
  117. package/dist/agents/cooldown.js.map +0 -1
  118. package/dist/agents/dedup-guard.js +0 -131
  119. package/dist/agents/dedup-guard.js.map +0 -1
  120. package/dist/agents/feed.js +0 -176
  121. package/dist/agents/feed.js.map +0 -1
  122. package/dist/agents/governance.js +0 -292
  123. package/dist/agents/governance.js.map +0 -1
  124. package/dist/agents/governed-spawn.js +0 -192
  125. package/dist/agents/governed-spawn.js.map +0 -1
  126. package/dist/agents/heartbeat.js +0 -324
  127. package/dist/agents/heartbeat.js.map +0 -1
  128. package/dist/agents/instance-manager.js +0 -850
  129. package/dist/agents/instance-manager.js.map +0 -1
  130. package/dist/agents/issue-reporter.js +0 -123
  131. package/dist/agents/issue-reporter.js.map +0 -1
  132. package/dist/agents/issues.js +0 -141
  133. package/dist/agents/issues.js.map +0 -1
  134. package/dist/agents/locks.js +0 -234
  135. package/dist/agents/locks.js.map +0 -1
  136. package/dist/agents/memory.js +0 -93
  137. package/dist/agents/memory.js.map +0 -1
  138. package/dist/agents/monitor.js +0 -235
  139. package/dist/agents/monitor.js.map +0 -1
  140. package/dist/agents/orchestration.js +0 -715
  141. package/dist/agents/orchestration.js.map +0 -1
  142. package/dist/agents/recover.js +0 -166
  143. package/dist/agents/recover.js.map +0 -1
  144. package/dist/agents/reflection.js +0 -199
  145. package/dist/agents/reflection.js.map +0 -1
  146. package/dist/agents/runtime/bus.js +0 -174
  147. package/dist/agents/runtime/bus.js.map +0 -1
  148. package/dist/agents/runtime/config.js +0 -101
  149. package/dist/agents/runtime/config.js.map +0 -1
  150. package/dist/agents/runtime/driver.js +0 -214
  151. package/dist/agents/runtime/driver.js.map +0 -1
  152. package/dist/agents/runtime/errors.js +0 -40
  153. package/dist/agents/runtime/errors.js.map +0 -1
  154. package/dist/agents/runtime/index.js +0 -54
  155. package/dist/agents/runtime/index.js.map +0 -1
  156. package/dist/agents/runtime/lifecycle.js +0 -116
  157. package/dist/agents/runtime/lifecycle.js.map +0 -1
  158. package/dist/agents/runtime/manager.js +0 -948
  159. package/dist/agents/runtime/manager.js.map +0 -1
  160. package/dist/agents/runtime/registry.js +0 -195
  161. package/dist/agents/runtime/registry.js.map +0 -1
  162. package/dist/agents/runtime/resources.js +0 -146
  163. package/dist/agents/runtime/resources.js.map +0 -1
  164. package/dist/agents/runtime/types.js +0 -24
  165. package/dist/agents/runtime/types.js.map +0 -1
  166. package/dist/agents/spawn-policy.js +0 -202
  167. package/dist/agents/spawn-policy.js.map +0 -1
  168. package/dist/agents/spawn.js +0 -970
  169. package/dist/agents/spawn.js.map +0 -1
  170. package/dist/agents/triage.js +0 -81
  171. package/dist/agents/triage.js.map +0 -1
  172. package/dist/agents/workflow.js +0 -543
  173. package/dist/agents/workflow.js.map +0 -1
  174. package/dist/avatar/client.js +0 -172
  175. package/dist/avatar/client.js.map +0 -1
  176. package/dist/avatar/sidecar.js +0 -125
  177. package/dist/avatar/sidecar.js.map +0 -1
  178. package/dist/browser/sessions.js +0 -122
  179. package/dist/browser/sessions.js.map +0 -1
  180. package/dist/capabilities/definitions/browser.js +0 -242
  181. package/dist/capabilities/definitions/browser.js.map +0 -1
  182. package/dist/channels/whatsapp.js +0 -200
  183. package/dist/channels/whatsapp.js.map +0 -1
  184. package/dist/credentials/store.js +0 -189
  185. package/dist/credentials/store.js.map +0 -1
  186. package/dist/files/deep-index.js +0 -337
  187. package/dist/files/deep-index.js.map +0 -1
  188. package/dist/files/extract.js +0 -33
  189. package/dist/files/extract.js.map +0 -1
  190. package/dist/files/gdrive.js +0 -246
  191. package/dist/files/gdrive.js.map +0 -1
  192. package/dist/github/client.js +0 -408
  193. package/dist/github/client.js.map +0 -1
  194. package/dist/github/commit-analysis.js +0 -276
  195. package/dist/github/commit-analysis.js.map +0 -1
  196. package/dist/github/contributor-stats.js +0 -119
  197. package/dist/github/contributor-stats.js.map +0 -1
  198. package/dist/github/issue-sla.js +0 -220
  199. package/dist/github/issue-sla.js.map +0 -1
  200. package/dist/github/issue-triage.js +0 -286
  201. package/dist/github/issue-triage.js.map +0 -1
  202. package/dist/github/pr-readiness.js +0 -197
  203. package/dist/github/pr-readiness.js.map +0 -1
  204. package/dist/github/pr-review.js +0 -410
  205. package/dist/github/pr-review.js.map +0 -1
  206. package/dist/github/release-notes.js +0 -227
  207. package/dist/github/release-notes.js.map +0 -1
  208. package/dist/github/repo-health.js +0 -303
  209. package/dist/github/repo-health.js.map +0 -1
  210. package/dist/github/retry.js +0 -117
  211. package/dist/github/retry.js.map +0 -1
  212. package/dist/github/types.js +0 -8
  213. package/dist/github/types.js.map +0 -1
  214. package/dist/github/webhooks.js +0 -153
  215. package/dist/github/webhooks.js.map +0 -1
  216. package/dist/google/auth.js +0 -325
  217. package/dist/google/auth.js.map +0 -1
  218. package/dist/google/calendar-timer.js +0 -91
  219. package/dist/google/calendar-timer.js.map +0 -1
  220. package/dist/google/calendar.js +0 -270
  221. package/dist/google/calendar.js.map +0 -1
  222. package/dist/google/docs.js +0 -309
  223. package/dist/google/docs.js.map +0 -1
  224. package/dist/google/gmail-send.js +0 -219
  225. package/dist/google/gmail-send.js.map +0 -1
  226. package/dist/google/gmail-timer.js +0 -223
  227. package/dist/google/gmail-timer.js.map +0 -1
  228. package/dist/google/gmail.js +0 -470
  229. package/dist/google/gmail.js.map +0 -1
  230. package/dist/google/plugin.js +0 -169
  231. package/dist/google/plugin.js.map +0 -1
  232. package/dist/google/tasks-timer.js +0 -107
  233. package/dist/google/tasks-timer.js.map +0 -1
  234. package/dist/google/tasks.js +0 -331
  235. package/dist/google/tasks.js.map +0 -1
  236. package/dist/google/temporal.js +0 -176
  237. package/dist/google/temporal.js.map +0 -1
  238. package/dist/integrations/gate.js +0 -100
  239. package/dist/integrations/gate.js.map +0 -1
  240. package/dist/integrations/github.js +0 -331
  241. package/dist/integrations/github.js.map +0 -1
  242. package/dist/integrations/google-tasks.js +0 -432
  243. package/dist/integrations/google-tasks.js.map +0 -1
  244. package/dist/mdns.js +0 -110
  245. package/dist/mdns.js.map +0 -1
  246. package/dist/notifications/channel.js +0 -83
  247. package/dist/notifications/channel.js.map +0 -1
  248. package/dist/notifications/channels/adapter.js +0 -55
  249. package/dist/notifications/channels/adapter.js.map +0 -1
  250. package/dist/notifications/channels/index.js +0 -6
  251. package/dist/notifications/channels/index.js.map +0 -1
  252. package/dist/notifications/channels/log.js +0 -29
  253. package/dist/notifications/channels/log.js.map +0 -1
  254. package/dist/notifications/email.js +0 -72
  255. package/dist/notifications/email.js.map +0 -1
  256. package/dist/notifications/engine.js +0 -198
  257. package/dist/notifications/engine.js.map +0 -1
  258. package/dist/notifications/index.js +0 -24
  259. package/dist/notifications/index.js.map +0 -1
  260. package/dist/notifications/phone.js +0 -48
  261. package/dist/notifications/phone.js.map +0 -1
  262. package/dist/notifications/sms.js +0 -65
  263. package/dist/notifications/sms.js.map +0 -1
  264. package/dist/notifications/types.js +0 -14
  265. package/dist/notifications/types.js.map +0 -1
  266. package/dist/notifications/webhook.js +0 -65
  267. package/dist/notifications/webhook.js.map +0 -1
  268. package/dist/resend/inbox.js +0 -199
  269. package/dist/resend/inbox.js.map +0 -1
  270. package/dist/resend/webhooks.js +0 -244
  271. package/dist/resend/webhooks.js.map +0 -1
  272. package/dist/search/browse.js +0 -225
  273. package/dist/search/browse.js.map +0 -1
  274. package/dist/search/perplexity.js +0 -41
  275. package/dist/search/perplexity.js.map +0 -1
  276. package/dist/slack/channels.js +0 -277
  277. package/dist/slack/channels.js.map +0 -1
  278. package/dist/slack/client.js +0 -468
  279. package/dist/slack/client.js.map +0 -1
  280. package/dist/slack/retry.js +0 -100
  281. package/dist/slack/retry.js.map +0 -1
  282. package/dist/slack/types.js +0 -52
  283. package/dist/slack/types.js.map +0 -1
  284. package/dist/slack/webhooks.js +0 -285
  285. package/dist/slack/webhooks.js.map +0 -1
  286. package/dist/stt/client.js +0 -66
  287. package/dist/stt/client.js.map +0 -1
  288. package/dist/stt/sidecar.js +0 -115
  289. package/dist/stt/sidecar.js.map +0 -1
  290. package/dist/tracing/bridge.js +0 -70
  291. package/dist/tracing/bridge.js.map +0 -1
  292. package/dist/tracing/correlation.js +0 -49
  293. package/dist/tracing/correlation.js.map +0 -1
  294. package/dist/tracing/index.js +0 -18
  295. package/dist/tracing/index.js.map +0 -1
  296. package/dist/tracing/init.js +0 -81
  297. package/dist/tracing/init.js.map +0 -1
  298. package/dist/tracing/instrument.js +0 -145
  299. package/dist/tracing/instrument.js.map +0 -1
  300. package/dist/tracing/middleware.js +0 -69
  301. package/dist/tracing/middleware.js.map +0 -1
  302. package/dist/tracing/tracer.js +0 -327
  303. package/dist/tracing/tracer.js.map +0 -1
  304. package/dist/tts/client.js +0 -48
  305. package/dist/tts/client.js.map +0 -1
  306. package/dist/tts/sidecar.js +0 -148
  307. package/dist/tts/sidecar.js.map +0 -1
  308. package/dist/twilio/call.js +0 -79
  309. package/dist/twilio/call.js.map +0 -1
  310. package/dist/vault/matcher.js +0 -197
  311. package/dist/vault/matcher.js.map +0 -1
  312. package/dist/vault/personal.js +0 -163
  313. package/dist/vault/personal.js.map +0 -1
  314. package/dist/vault/policy.js +0 -159
  315. package/dist/vault/policy.js.map +0 -1
  316. package/dist/vault/store.js +0 -122
  317. package/dist/vault/store.js.map +0 -1
  318. package/dist/vault/transfer.js +0 -188
  319. package/dist/vault/transfer.js.map +0 -1
  320. package/dist/volumes/index.js +0 -2
  321. package/dist/volumes/index.js.map +0 -1
  322. package/dist/volumes/manager.js +0 -462
  323. package/dist/volumes/manager.js.map +0 -1
  324. package/dist/volumes/types.js +0 -8
  325. package/dist/volumes/types.js.map +0 -1
  326. package/dist/webhooks/config.js +0 -214
  327. package/dist/webhooks/config.js.map +0 -1
  328. package/dist/webhooks/event-log.js +0 -132
  329. package/dist/webhooks/event-log.js.map +0 -1
  330. package/dist/webhooks/handler.js +0 -103
  331. package/dist/webhooks/handler.js.map +0 -1
  332. package/dist/webhooks/handlers.js +0 -231
  333. package/dist/webhooks/handlers.js.map +0 -1
  334. package/dist/webhooks/index.js +0 -33
  335. package/dist/webhooks/index.js.map +0 -1
  336. package/dist/webhooks/mount.js +0 -400
  337. package/dist/webhooks/mount.js.map +0 -1
  338. package/dist/webhooks/registry.js +0 -143
  339. package/dist/webhooks/registry.js.map +0 -1
  340. package/dist/webhooks/relay.js +0 -53
  341. package/dist/webhooks/relay.js.map +0 -1
  342. package/dist/webhooks/retry.js +0 -270
  343. package/dist/webhooks/retry.js.map +0 -1
  344. package/dist/webhooks/router.js +0 -290
  345. package/dist/webhooks/router.js.map +0 -1
  346. package/dist/webhooks/twilio.js +0 -129
  347. package/dist/webhooks/twilio.js.map +0 -1
  348. package/dist/webhooks/types.js +0 -8
  349. package/dist/webhooks/types.js.map +0 -1
  350. package/dist/webhooks/verify.js +0 -154
  351. package/dist/webhooks/verify.js.map +0 -1
@@ -1,970 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { writeFileSync, mkdirSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { writeTask, updateTask, readTask, readTaskOutput, listTasks, LOGS_DIR } from "./store.js";
5
- import { pushNotification } from "../goals/notifications.js";
6
- import { logActivity } from "../activity/log.js";
7
- import { rememberTaskOutcome, recordScar } from "./memory.js";
8
- import { makeCall } from "../twilio/call.js";
9
- import { attemptRecovery, recordRecoveryFailure } from "./recover.js";
10
- import { TaskCooldownManager } from "./cooldown.js";
11
- import { triageAgentOutput } from "./triage.js";
12
- import { releaseLocks } from "./locks.js";
13
- import { recordAgentSpawn, recordAgentCompletion } from "../metrics/collector.js";
14
- import { recordSpawnRateBlock, recordBridgeReport as recordBridgeReportMetric } from "../metrics/firewall-metrics.js";
15
- import { traceAgentSpawn } from "../tracing/instrument.js";
16
- import { getCorrelationId } from "../tracing/correlation.js";
17
- import { completeChat } from "../llm/complete.js";
18
- import { resolveAgentModelAsync, resolveAgentProvider } from "../settings.js";
19
- import { createLogger } from "../utils/logger.js";
20
- import { resolveEnv, getInstanceName, getInstanceNameLower, getAlertEmailFrom } from "../instance.js";
21
- import { skillRegistry as _skillRegistry } from "../skills/registry.js";
22
- import { getBoardProvider } from "../board/provider.js";
23
- import { BRAIN_DIR } from "../lib/paths.js";
24
- import { processAgentIssues } from "./issues.js";
25
- const log = createLogger("agent-spawn");
26
- /**
27
- * Update a board task's state/assignee. Best-effort — failures are logged, not thrown.
28
- * Used to move board items through in_progress → done/todo as agents work.
29
- * Exported so the monitor can also update board state on recovered completions.
30
- */
31
- export async function updateBoardTaskState(boardTaskId, changes) {
32
- try {
33
- const bp = getBoardProvider();
34
- const store = bp?.getStore?.();
35
- if (store) {
36
- await store.update(boardTaskId, changes);
37
- log.debug(`Board task ${boardTaskId} updated: ${JSON.stringify(changes)}`);
38
- }
39
- }
40
- catch (err) {
41
- log.warn(`Failed to update board task ${boardTaskId}: ${err instanceof Error ? err.message : String(err)}`);
42
- }
43
- }
44
- /** Track recent failures — email first, phone only as last resort. */
45
- const recentFailures = [];
46
- const FAILURE_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
47
- const EMAIL_THRESHOLD = 2; // 2+ failures in window → email
48
- const CALL_THRESHOLD = 5; // 5+ failures in window → phone (last resort)
49
- let lastEmailTime = 0;
50
- let lastCallTime = 0;
51
- const EMAIL_COOLDOWN_MS = 10 * 60 * 1000; // Don't email more than once per 10 min
52
- const CALL_COOLDOWN_MS = 30 * 60 * 1000; // Don't call more than once per 30 min
53
- /**
54
- * Global spawn rate limiter — prevents burst-spawning agents.
55
- * Tracks timestamps of recent spawns and enforces minimum spacing.
56
- */
57
- const recentSpawnTimestamps = [];
58
- const SPAWN_RATE_WINDOW_MS = 60_000; // 1 minute window
59
- const MAX_SPAWNS_PER_WINDOW = 10; // Max 10 spawns per minute
60
- function isSpawnRateLimited() {
61
- const now = Date.now();
62
- // Prune old entries
63
- while (recentSpawnTimestamps.length > 0 && recentSpawnTimestamps[0] < now - SPAWN_RATE_WINDOW_MS) {
64
- recentSpawnTimestamps.shift();
65
- }
66
- return recentSpawnTimestamps.length >= MAX_SPAWNS_PER_WINDOW;
67
- }
68
- function recordSpawn() {
69
- recentSpawnTimestamps.push(Date.now());
70
- }
71
- // ─── Skill Enrichment ─────────────────────────────────────────────────────────
72
- /**
73
- * Enrich a task's prompt with matched skill instructions from the SkillRegistry.
74
- * Transparent: if the registry isn't initialized or no skills match, returns the
75
- * original prompt unchanged.
76
- */
77
- async function enrichPromptWithSkills(task) {
78
- try {
79
- const matched = await _skillRegistry.findByTrigger(task.label);
80
- if (!matched)
81
- return task.prompt;
82
- const content = await _skillRegistry.getContent(matched.id);
83
- if (!content)
84
- return task.prompt;
85
- const skillPrompts = [`<skill name="${matched.name}" type="${matched.type}">\n${content}\n</skill>`];
86
- const skillNames = [matched.name];
87
- log.info(`Enriching agent "${task.label}" with skill: ${skillNames.join(", ")}`, { taskId: task.id });
88
- return [
89
- task.prompt,
90
- "",
91
- "---",
92
- "## Relevant Skills",
93
- "",
94
- ...skillPrompts,
95
- ].join("\n");
96
- }
97
- catch (err) {
98
- log.warn(`Skill enrichment failed for "${task.label}", using original prompt: ${err instanceof Error ? err.message : String(err)}`, { taskId: task.id });
99
- return task.prompt;
100
- }
101
- }
102
- /** Map of task ID → spawned ChildProcess (only for tasks we spawned this session). */
103
- const activeProcesses = new Map();
104
- /** Tasks that fell back from pool to direct — skip recovery to prevent double-retry. */
105
- const poolFallbackTasks = new Set();
106
- /** Track batch membership: sessionId → set of task IDs spawned together. */
107
- const sessionBatches = new Map();
108
- /** Collect results from each agent as they finish (sessionId → results[]) */
109
- const batchResults = new Map();
110
- /** Callback invoked when all agents from a session batch have completed. */
111
- let onBatchComplete = null;
112
- /** Register a callback for when an agent batch finishes. */
113
- export function setOnBatchComplete(cb) {
114
- onBatchComplete = cb;
115
- }
116
- /** Map of task ID → timeout timer. */
117
- const activeTimers = new Map();
118
- export { activeProcesses };
119
- /** Check if any agents are currently running (active processes or pending batches). */
120
- export function isAgentsBusy() {
121
- return activeProcesses.size > 0 || sessionBatches.size > 0;
122
- }
123
- /** Number of currently active agent processes. */
124
- export function activeAgentCount() {
125
- return activeProcesses.size;
126
- }
127
- /** Reference to the runtime pool. Set via setAgentPool() when the pool is initialized. */
128
- let agentPool = null;
129
- /** Wire the runtime pool into spawn.ts so spawnAgent delegates to it. */
130
- export function setAgentPool(pool) {
131
- agentPool = pool;
132
- }
133
- /** Check if the runtime pool is active. */
134
- export function hasAgentPool() {
135
- return agentPool !== null && !agentPool.isShuttingDown;
136
- }
137
- /**
138
- * Spawn the claude CLI for a task.
139
- *
140
- * When the runtime AgentPool is available, delegates to it for lifecycle
141
- * management, resource tracking, circuit breakers, and isolation.
142
- * Falls back to direct process spawning when the pool isn't initialized.
143
- *
144
- * Mutates task in-place (status, pid, startedAt) and writes to disk.
145
- */
146
- export async function spawnAgent(task) {
147
- return traceAgentSpawn(task.id, task.label, task.origin, async (span) => {
148
- const correlationId = getCorrelationId();
149
- if (correlationId) {
150
- span.setAttribute("dash.correlation_id", correlationId);
151
- }
152
- if (task.sessionId) {
153
- span.setAttribute("agent.session_id", task.sessionId);
154
- }
155
- log.info(`Spawning agent: ${task.label}`, { taskId: task.id, origin: task.origin, sessionId: task.sessionId });
156
- // Enrich prompt with matched skill instructions (transparent — no-op if registry unavailable)
157
- task.prompt = await enrichPromptWithSkills(task);
158
- // Global spawn rate limiter — prevent burst-spawning
159
- if (isSpawnRateLimited()) {
160
- log.warn(`Spawn rate-limited, skipping: ${task.label}`, { taskId: task.id });
161
- span.setAttribute("agent.rate_limited", true);
162
- recordSpawnRateBlock();
163
- logActivity({
164
- source: "agent",
165
- summary: `Spawn rate-limited, skipping: ${task.label}`,
166
- detail: `Max ${MAX_SPAWNS_PER_WINDOW} spawns per ${SPAWN_RATE_WINDOW_MS / 1000}s`,
167
- actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED",
168
- reason: "spawn rate limit exceeded",
169
- });
170
- return;
171
- }
172
- recordSpawn();
173
- // Delegate to runtime pool when available
174
- if (agentPool && !agentPool.isShuttingDown) {
175
- log.debug(`Using pool spawn for: ${task.label}`, { taskId: task.id });
176
- span.setAttribute("agent.spawn_mode", "pool");
177
- return spawnViaPool(task);
178
- }
179
- // Fallback: direct process spawning (original behavior)
180
- log.debug(`Using direct spawn for: ${task.label}`, { taskId: task.id });
181
- span.setAttribute("agent.spawn_mode", "direct");
182
- return spawnDirect(task);
183
- });
184
- }
185
- /**
186
- * Spawn through the AgentPool runtime.
187
- * The pool handles resource allocation, circuit breakers, isolation, and monitoring.
188
- */
189
- async function spawnViaPool(task) {
190
- try {
191
- const instance = await agentPool.spawn({
192
- taskId: task.id,
193
- label: task.label,
194
- prompt: task.prompt,
195
- cwd: task.cwd,
196
- origin: task.origin,
197
- tags: task.sessionId ? [`session:${task.sessionId}`] : [],
198
- config: {
199
- timeoutMs: task.timeoutMs ?? 600000,
200
- maxRetries: 2,
201
- backoffMs: 2000,
202
- backoffMultiplier: 2,
203
- maxBackoffMs: 30000,
204
- env: {},
205
- isolation: "shared",
206
- priority: 50,
207
- },
208
- });
209
- // Sync task state from runtime instance
210
- task.status = "running";
211
- task.pid = instance.pid;
212
- task.startedAt = new Date().toISOString();
213
- await writeTask(task);
214
- // Move board task to in_progress and write back the agent task ID for causal backrefs
215
- if (task.boardTaskId) {
216
- updateBoardTaskState(task.boardTaskId, { state: "in_progress", assignee: `${getInstanceNameLower()}-agent`, agentTaskId: task.id });
217
- }
218
- // Track batch membership (still needed for continuation)
219
- if (task.sessionId) {
220
- if (!sessionBatches.has(task.sessionId)) {
221
- sessionBatches.set(task.sessionId, new Set());
222
- }
223
- sessionBatches.get(task.sessionId).add(task.id);
224
- }
225
- // Listen for completion via the runtime bus
226
- // Use .on() with self-removal instead of .once() to avoid race conditions
227
- // where another agent's event consumes this listener
228
- const onCompleted = (data) => {
229
- if (data.agentId === instance.id) {
230
- agentPool?.runtimeManager.bus.off("agent:completed", onCompleted);
231
- agentPool?.runtimeManager.bus.off("agent:failed", onFailed);
232
- handlePoolCompletion(task, "completed", data.exitCode ?? 0);
233
- }
234
- };
235
- const onFailed = (data) => {
236
- if (data.agentId === instance.id) {
237
- agentPool?.runtimeManager.bus.off("agent:completed", onCompleted);
238
- agentPool?.runtimeManager.bus.off("agent:failed", onFailed);
239
- handlePoolCompletion(task, "failed", null);
240
- }
241
- };
242
- agentPool.runtimeManager.bus.on("agent:completed", onCompleted);
243
- agentPool.runtimeManager.bus.on("agent:failed", onFailed);
244
- recordAgentSpawn();
245
- logActivity({
246
- source: "agent",
247
- summary: `Spawned agent via pool: ${task.label}`,
248
- detail: `Instance ${instance.id}, PID ${instance.pid}, task ${task.id}`,
249
- actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED",
250
- reason: task.origin === "ai" ? "planner selected from backlog" : "user chat triggered agent",
251
- });
252
- }
253
- catch (err) {
254
- // Pool rejected the spawn (circuit breaker, resources, etc.)
255
- // Fall back to direct spawning, but mark task to skip recovery
256
- // (pool already handles retries — don't double up with recovery agents)
257
- poolFallbackTasks.add(task.id);
258
- logActivity({
259
- source: "agent",
260
- summary: `Pool spawn failed, falling back to direct (no recovery): ${task.label}`,
261
- detail: err instanceof Error ? err.message : String(err),
262
- actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED",
263
- reason: "pool spawn failed, falling back",
264
- });
265
- return spawnDirect(task);
266
- }
267
- }
268
- /** Handle completion callbacks when spawned via pool. */
269
- async function handlePoolCompletion(task, status, exitCode) {
270
- log.info(`Agent ${status}: ${task.label}`, { taskId: task.id, exitCode, sessionId: task.sessionId });
271
- // Release file locks held by this agent
272
- releaseLocks(task.id).catch((err) => {
273
- log.warn(`Failed to release locks for agent ${task.id}: ${err instanceof Error ? err.message : String(err)}`);
274
- });
275
- // Record agent completion metric
276
- if (task.startedAt) {
277
- const durationMs = Date.now() - new Date(task.startedAt).getTime();
278
- recordAgentCompletion(durationMs, status === "completed");
279
- }
280
- // DASH-143: Record cooldown IMMEDIATELY on failure — before setting
281
- // board task back to "todo". This closes the race window where the
282
- // autonomous loop re-plans the same task before batch-level cooldown fires.
283
- if (status === "failed" && task.boardTaskId) {
284
- TaskCooldownManager.getInstance().recordFailure(task.boardTaskId, task.label, `Pool terminal failure (exit ${exitCode})`);
285
- }
286
- // Update board task state: done on success, back to todo on failure
287
- if (task.boardTaskId) {
288
- if (status === "completed") {
289
- await updateBoardTaskState(task.boardTaskId, { state: "done" });
290
- }
291
- else {
292
- // Failed: move back to todo and clear assignee so it can be retried
293
- await updateBoardTaskState(task.boardTaskId, { state: "todo", assignee: null });
294
- }
295
- }
296
- const output = await readTaskOutput(task.id).catch(() => "");
297
- const resultSummary = output.trim().slice(0, 1000) || undefined;
298
- // Process issue reports from read-only autonomous agents
299
- if (task.readOnly && status === "completed" && output) {
300
- processAgentIssues(output, task.id).catch((err) => {
301
- log.warn(`Failed to process issues from ${task.id}: ${err instanceof Error ? err.message : String(err)}`);
302
- });
303
- }
304
- // Notifications
305
- const outputSnippet = resultSummary ? `\nOutput:\n${resultSummary}` : "";
306
- pushNotification({
307
- timestamp: new Date().toISOString(),
308
- source: "agent",
309
- message: `Agent task "${task.label}" ${status} (exit ${exitCode}).${outputSnippet}`,
310
- });
311
- // Triage: check if agent surfaced questions for the human
312
- const needsHuman = await triageAgentOutput({ ...task, status, exitCode: exitCode ?? undefined }, output).catch(() => false);
313
- // On failure via pool: the RuntimeManager already handles retries at the
314
- // instance level (maybeRetry with exponential backoff). Don't also spawn
315
- // a recovery agent — that creates double-retry fan-out. Only track for
316
- // phone alerts and record if this was a recovery task that failed.
317
- if (status === "failed") {
318
- if (task.label.startsWith("Fix: ")) {
319
- recordRecoveryFailure();
320
- }
321
- if (!needsHuman) {
322
- trackFailureForAlert(task.label);
323
- }
324
- }
325
- // Post-completion reflection for autonomous tasks (skip for recovery agents —
326
- // session-level reflection already covers them, and per-agent LLM calls add latency)
327
- if (!task.label.startsWith("Fix: ")) {
328
- reflectOnCompletion({ ...task, status, exitCode: exitCode ?? undefined }, output).catch(() => { });
329
- }
330
- // Scar evaluation for successful Fix: agents — check if the repair qualifies as a scar
331
- if (task.label.startsWith("Fix: ") && status === "completed") {
332
- evaluateScar({ ...task, status, exitCode: exitCode ?? undefined }, output).catch(() => { });
333
- }
334
- // Batch continuation
335
- if (task.sessionId && sessionBatches.has(task.sessionId)) {
336
- const batch = sessionBatches.get(task.sessionId);
337
- batch.delete(task.id);
338
- if (!batchResults.has(task.sessionId)) {
339
- batchResults.set(task.sessionId, []);
340
- }
341
- batchResults.get(task.sessionId).push({ label: task.label, status });
342
- if (batch.size === 0) {
343
- sessionBatches.delete(task.sessionId);
344
- const allResults = batchResults.get(task.sessionId) ?? [{ label: task.label, status }];
345
- batchResults.delete(task.sessionId);
346
- if (onBatchComplete) {
347
- const sid = task.sessionId;
348
- // 500ms settlement: task store writes are sync, just need metadata flush
349
- setTimeout(() => {
350
- logActivity({ source: "agent", summary: `Agent batch complete for session ${sid} (${allResults.length} agents)`, actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED", reason: "agent batch completed" });
351
- Promise.resolve(onBatchComplete(sid, allResults)).catch((err) => {
352
- logActivity({ source: "agent", summary: `Auto-continue callback error: ${err instanceof Error ? err.message : String(err)}`, actionLabel: "AUTONOMOUS", reason: "batch continuation error" });
353
- });
354
- }, 500);
355
- }
356
- }
357
- }
358
- }
359
- /** Send agent failure alert email via Resend (fire-and-forget). */
360
- async function sendFailureEmail(count, names) {
361
- const apiKey = process.env.RESEND_API_KEY;
362
- if (!apiKey) {
363
- log.warn("No RESEND_API_KEY — skipping agent failure email");
364
- return;
365
- }
366
- try {
367
- const res = await fetch("https://api.resend.com/emails", {
368
- method: "POST",
369
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
370
- body: JSON.stringify({
371
- from: `${getInstanceName()} <${getAlertEmailFrom()}>`,
372
- to: [resolveEnv("ALERT_EMAIL_TO") ?? ""].filter(Boolean),
373
- subject: `[AGENT ALERT] ${count} agent failures in the last few minutes`,
374
- html: `<div style="font-family:sans-serif;max-width:600px;">
375
- <div style="background:#f59e0b;color:white;padding:16px;border-radius:8px 8px 0 0;">
376
- <h2 style="margin:0;">Agent Failures</h2>
377
- </div>
378
- <div style="border:1px solid #e5e7eb;border-top:none;padding:16px;border-radius:0 0 8px 8px;">
379
- <p><strong>${count} agents</strong> failed within a 5-minute window.</p>
380
- <p><strong>Agents:</strong> ${names}</p>
381
- <p style="color:#6b7280;font-size:12px;">Check the agent logs at /ops for details. A phone call will follow only if failures continue to escalate.</p>
382
- </div>
383
- </div>`,
384
- }),
385
- });
386
- if (res.ok) {
387
- logActivity({ source: "agent", summary: `Emailed you about ${count} agent failures` });
388
- }
389
- else {
390
- log.error("Agent failure email failed", { status: res.status });
391
- }
392
- }
393
- catch (err) {
394
- log.error("Agent failure email exception", { error: String(err) });
395
- }
396
- }
397
- /** Track failure — email first, phone only as last resort. */
398
- function trackFailureForAlert(label) {
399
- const now = Date.now();
400
- recentFailures.push({ label, time: now });
401
- while (recentFailures.length > 0 && recentFailures[0].time < now - FAILURE_WINDOW_MS) {
402
- recentFailures.shift();
403
- }
404
- const count = recentFailures.length;
405
- const names = recentFailures.map((f) => f.label).join(", ");
406
- // Tier 1: Email at 2+ failures
407
- if (count >= EMAIL_THRESHOLD && now - lastEmailTime > EMAIL_COOLDOWN_MS) {
408
- lastEmailTime = now;
409
- sendFailureEmail(count, names).catch(() => { });
410
- }
411
- // Tier 2: Phone call at 5+ failures (last resort, after email)
412
- if (count >= CALL_THRESHOLD && now - lastCallTime > CALL_COOLDOWN_MS) {
413
- lastCallTime = now;
414
- makeCall({
415
- message: `Hey, it's ${getInstanceName()}. ${count} agents have failed in the last few minutes. I already sent you an email with details. This is getting serious — please check the logs.`,
416
- }).then((r) => {
417
- logActivity({ source: "agent", summary: r.ok ? `Called you about ${count} agent failures (escalated from email)` : `Failed to call: ${r.message}` });
418
- }).catch(() => { });
419
- }
420
- }
421
- /** Direct process spawning — original behavior without runtime pool. */
422
- async function spawnDirect(task) {
423
- const stdoutPath = join(LOGS_DIR, `${task.id}.stdout.log`);
424
- const stderrPath = join(LOGS_DIR, `${task.id}.stderr.log`);
425
- const promptPath = join(LOGS_DIR, `${task.id}.prompt.txt`);
426
- // Write prompt to file to avoid shell escaping issues
427
- writeFileSync(promptPath, task.prompt, "utf-8");
428
- // Clean environment: remove CLAUDECODE to allow nested Claude Code sessions.
429
- // Claude CLI refuses to run inside another session if this env var is set.
430
- const cleanEnv = { ...process.env };
431
- delete cleanEnv.CLAUDECODE;
432
- // Use node as the wrapper — avoids all shell encoding/escaping issues.
433
- // spawnSync with windowsHide prevents claude from opening a console window.
434
- const wrapperScript = `
435
- const fs = require("fs");
436
- const { spawnSync } = require("child_process");
437
- const prompt = fs.readFileSync(${JSON.stringify(promptPath)}, "utf-8");
438
- const r = spawnSync("claude", [
439
- "--print", "--output-format", "text", "--dangerously-skip-permissions", prompt
440
- ], {
441
- cwd: ${JSON.stringify(task.cwd)},
442
- encoding: "utf-8",
443
- maxBuffer: 50 * 1024 * 1024,
444
- timeout: ${task.timeoutMs ?? 600000},
445
- windowsHide: true
446
- });
447
- fs.writeFileSync(${JSON.stringify(stdoutPath)}, r.stdout || "", "utf-8");
448
- fs.writeFileSync(${JSON.stringify(stderrPath)}, r.stderr || "", "utf-8");
449
- process.exit(r.status || 0);
450
- `;
451
- const child = spawn(process.execPath, ["--eval", wrapperScript], {
452
- cwd: task.cwd,
453
- detached: true,
454
- stdio: "ignore",
455
- env: cleanEnv,
456
- windowsHide: true,
457
- });
458
- // Let the instance exit without killing the child
459
- child.unref();
460
- task.status = "running";
461
- task.pid = child.pid;
462
- task.startedAt = new Date().toISOString();
463
- await writeTask(task);
464
- activeProcesses.set(task.id, child);
465
- // Move board task to in_progress and write back the agent task ID for causal backrefs
466
- if (task.boardTaskId) {
467
- updateBoardTaskState(task.boardTaskId, { state: "in_progress", assignee: `${getInstanceNameLower()}-agent`, agentTaskId: task.id });
468
- }
469
- // Track batch: group tasks by sessionId so we know when a batch finishes
470
- if (task.sessionId) {
471
- if (!sessionBatches.has(task.sessionId)) {
472
- sessionBatches.set(task.sessionId, new Set());
473
- }
474
- sessionBatches.get(task.sessionId).add(task.id);
475
- }
476
- recordAgentSpawn();
477
- logActivity({
478
- source: "agent",
479
- summary: `Spawned agent: ${task.label}`,
480
- detail: `PID ${child.pid}, task ${task.id}`,
481
- actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED",
482
- reason: task.origin === "ai" ? "planner selected from backlog" : "user chat triggered agent",
483
- });
484
- // Exit handler
485
- child.on("exit", async (code) => {
486
- log.info(`Agent process exited: ${task.label}`, { taskId: task.id, exitCode: code, pid: child.pid });
487
- activeProcesses.delete(task.id);
488
- clearTaskTimer(task.id);
489
- // Release file locks held by this agent
490
- releaseLocks(task.id).catch((err) => {
491
- log.warn(`Failed to release locks for agent ${task.id}: ${err instanceof Error ? err.message : String(err)}`);
492
- });
493
- // Brief delay to ensure file buffers are flushed.
494
- // writeFileSync in the wrapper script guarantees data is on disk,
495
- // but the OS may still be updating file metadata. 100ms is sufficient.
496
- await new Promise((r) => setTimeout(r, 100));
497
- const output = await readTaskOutput(task.id);
498
- const resultSummary = output.trim().slice(0, 1000) || undefined;
499
- // Determine success: exit 0 is clean success, but a null/non-zero exit code
500
- // with substantial output likely means the agent did its work but the process
501
- // exited uncleanly (signal, timeout, claude CLI quirk). Trust the output.
502
- const hasSubstantialOutput = output.trim().length > 100;
503
- const finalStatus = code === 0 || (code == null && hasSubstantialOutput)
504
- ? "completed"
505
- : "failed";
506
- // Record agent completion metric
507
- if (task.startedAt) {
508
- const durationMs = Date.now() - new Date(task.startedAt).getTime();
509
- recordAgentCompletion(durationMs, finalStatus === "completed");
510
- }
511
- await updateTask(task.id, {
512
- status: finalStatus,
513
- exitCode: code ?? undefined,
514
- finishedAt: new Date().toISOString(),
515
- resultSummary,
516
- });
517
- // DASH-143: Record cooldown IMMEDIATELY on failure — before setting
518
- // board task back to "todo". Prevents autonomous re-planning race.
519
- if (finalStatus === "failed" && task.boardTaskId) {
520
- TaskCooldownManager.getInstance().recordFailure(task.boardTaskId, task.label, `Direct spawn failure (exit ${code})`);
521
- }
522
- // Update board task state: done on success, back to todo on failure
523
- if (task.boardTaskId) {
524
- if (finalStatus === "completed") {
525
- await updateBoardTaskState(task.boardTaskId, { state: "done" });
526
- }
527
- else {
528
- await updateBoardTaskState(task.boardTaskId, { state: "todo", assignee: null });
529
- }
530
- }
531
- // Durable memory — survives restarts, retrievable by future turns
532
- rememberTaskOutcome({ ...task, status: finalStatus, exitCode: code ?? undefined }, output).catch(() => { });
533
- // Post-completion reflection for autonomous tasks (skip for recovery agents —
534
- // session-level reflection already covers them, and per-agent LLM calls add latency)
535
- if (!task.label.startsWith("Fix: ")) {
536
- reflectOnCompletion({ ...task, status: finalStatus, exitCode: code ?? undefined }, output).catch(() => { });
537
- }
538
- // Scar evaluation for successful Fix: agents — check if the repair qualifies as a scar
539
- if (task.label.startsWith("Fix: ") && finalStatus === "completed") {
540
- evaluateScar({ ...task, status: finalStatus, exitCode: code ?? undefined }, output).catch(() => { });
541
- }
542
- logActivity({
543
- source: "agent",
544
- summary: `Agent ${finalStatus}: ${task.label}`,
545
- detail: `Exit code ${code}, task ${task.id}`,
546
- actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED",
547
- reason: `agent ${finalStatus}`,
548
- });
549
- const outputSnippet = resultSummary
550
- ? `\nOutput:\n${resultSummary}`
551
- : "";
552
- pushNotification({
553
- timestamp: new Date().toISOString(),
554
- source: "agent",
555
- message: `Agent task "${task.label}" ${finalStatus} (exit ${code}).${outputSnippet}`,
556
- });
557
- // Triage: check if agent surfaced questions for the human
558
- const needsHuman = await triageAgentOutput({ ...task, status: finalStatus, exitCode: code ?? undefined }, output).catch(() => false);
559
- // On failure: attempt recovery ONLY if not blocked on human questions
560
- // and not a pool-fallback task (pool already handles retries)
561
- if (finalStatus === "failed" && !needsHuman) {
562
- // Record if this was itself a recovery agent that failed
563
- if (task.label.startsWith("Fix: ")) {
564
- recordRecoveryFailure();
565
- }
566
- const isPoolFallback = poolFallbackTasks.has(task.id);
567
- if (isPoolFallback) {
568
- poolFallbackTasks.delete(task.id);
569
- logActivity({
570
- source: "agent",
571
- summary: `Skipping recovery for pool-fallback task: ${task.label}`,
572
- actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED",
573
- reason: "pool-fallback task — no double retry",
574
- });
575
- }
576
- else {
577
- attemptRecovery({ ...task, status: "failed", exitCode: code ?? undefined }, output).catch(() => { });
578
- }
579
- trackFailureForAlert(task.label);
580
- }
581
- // Check if this was the last agent in a batch → trigger continuation
582
- if (task.sessionId && sessionBatches.has(task.sessionId)) {
583
- const batch = sessionBatches.get(task.sessionId);
584
- batch.delete(task.id);
585
- // Collect this agent's result
586
- if (!batchResults.has(task.sessionId)) {
587
- batchResults.set(task.sessionId, []);
588
- }
589
- batchResults.get(task.sessionId).push({ label: task.label, status: finalStatus });
590
- if (batch.size === 0) {
591
- sessionBatches.delete(task.sessionId);
592
- const allResults = batchResults.get(task.sessionId) ?? [{ label: task.label, status: finalStatus }];
593
- batchResults.delete(task.sessionId);
594
- // All agents done — pass all results to continuation
595
- if (onBatchComplete) {
596
- const sid = task.sessionId;
597
- // 500ms settlement: task store writes are sync, just need metadata flush
598
- setTimeout(() => {
599
- logActivity({ source: "agent", summary: `Agent batch complete for session ${sid} (${allResults.length} agents)`, actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED", reason: "agent batch completed" });
600
- Promise.resolve(onBatchComplete(sid, allResults)).catch((err) => {
601
- logActivity({ source: "agent", summary: `Auto-continue callback error: ${err instanceof Error ? err.message : String(err)}`, actionLabel: "AUTONOMOUS", reason: "batch continuation error" });
602
- });
603
- }, 500);
604
- }
605
- }
606
- }
607
- });
608
- // Error handler (spawn failure)
609
- child.on("error", async (err) => {
610
- activeProcesses.delete(task.id);
611
- clearTaskTimer(task.id);
612
- poolFallbackTasks.delete(task.id);
613
- // Record agent failure metric — balances the in-flight gauge from recordAgentSpawn()
614
- if (task.startedAt) {
615
- const durationMs = Date.now() - new Date(task.startedAt).getTime();
616
- recordAgentCompletion(durationMs, false);
617
- }
618
- else {
619
- // Spawned but never got startedAt — still need to decrement in-flight
620
- recordAgentCompletion(0, false);
621
- }
622
- // Release file locks held by this agent
623
- releaseLocks(task.id).catch(() => { });
624
- await updateTask(task.id, {
625
- status: "failed",
626
- error: err.message,
627
- finishedAt: new Date().toISOString(),
628
- });
629
- rememberTaskOutcome({ ...task, status: "failed", error: err.message }).catch(() => { });
630
- logActivity({
631
- source: "agent",
632
- summary: `Agent spawn error: ${task.label}`,
633
- detail: err.message,
634
- actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED",
635
- reason: "agent spawn error",
636
- });
637
- pushNotification({
638
- timestamp: new Date().toISOString(),
639
- source: "agent",
640
- message: `Agent task "${task.label}" failed to spawn: ${err.message}`,
641
- });
642
- // Clean up batch membership so batch completion isn't stuck
643
- if (task.sessionId && sessionBatches.has(task.sessionId)) {
644
- const batch = sessionBatches.get(task.sessionId);
645
- batch.delete(task.id);
646
- if (!batchResults.has(task.sessionId)) {
647
- batchResults.set(task.sessionId, []);
648
- }
649
- batchResults.get(task.sessionId).push({ label: task.label, status: "failed" });
650
- if (batch.size === 0) {
651
- sessionBatches.delete(task.sessionId);
652
- const allResults = batchResults.get(task.sessionId) ?? [{ label: task.label, status: "failed" }];
653
- batchResults.delete(task.sessionId);
654
- if (onBatchComplete) {
655
- const sid = task.sessionId;
656
- setTimeout(() => {
657
- Promise.resolve(onBatchComplete(sid, allResults)).catch(() => { });
658
- }, 2000);
659
- }
660
- }
661
- }
662
- });
663
- // Timeout
664
- if (task.timeoutMs && task.timeoutMs > 0) {
665
- const timer = setTimeout(() => {
666
- if (activeProcesses.has(task.id)) {
667
- logActivity({
668
- source: "agent",
669
- summary: `Agent timed out: ${task.label}`,
670
- detail: `Killing PID ${child.pid} after ${task.timeoutMs}ms`,
671
- actionLabel: task.origin === "ai" ? "AUTONOMOUS" : "PROMPTED",
672
- reason: "agent timeout exceeded",
673
- });
674
- try {
675
- if (process.platform === "win32" && child.pid) {
676
- spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { shell: true });
677
- }
678
- else {
679
- child.kill("SIGTERM");
680
- }
681
- }
682
- catch { }
683
- }
684
- }, task.timeoutMs);
685
- activeTimers.set(task.id, timer);
686
- }
687
- }
688
- /** Cancel a running agent task. Tries pool first, then direct process kill. */
689
- export async function cancelAgent(taskId) {
690
- // Try cancelling via pool (it tracks runtime instances by taskId)
691
- if (agentPool) {
692
- const instance = agentPool.runtimeManager.getByTaskId(taskId);
693
- if (instance) {
694
- try {
695
- await agentPool.terminate(instance.id, "Cancelled by user");
696
- }
697
- catch {
698
- // Fall through to direct cancellation
699
- }
700
- }
701
- }
702
- // Direct process kill (for legacy/direct spawns)
703
- const child = activeProcesses.get(taskId);
704
- if (child) {
705
- activeProcesses.delete(taskId);
706
- clearTaskTimer(taskId);
707
- try {
708
- if (process.platform === "win32" && child.pid) {
709
- spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { shell: true });
710
- }
711
- else {
712
- child.kill("SIGTERM");
713
- }
714
- }
715
- catch { }
716
- }
717
- // Release file locks held by this agent
718
- releaseLocks(taskId).catch((err) => {
719
- log.warn(`Failed to release locks on cancel for ${taskId}: ${err instanceof Error ? err.message : String(err)}`);
720
- });
721
- // Record cancellation as a failed completion to keep in-flight gauge accurate
722
- const taskRecord = await readTask(taskId);
723
- if (taskRecord?.startedAt) {
724
- const durationMs = Date.now() - new Date(taskRecord.startedAt).getTime();
725
- recordAgentCompletion(durationMs, false);
726
- }
727
- const updated = await updateTask(taskId, {
728
- status: "cancelled",
729
- finishedAt: new Date().toISOString(),
730
- });
731
- if (updated) {
732
- logActivity({
733
- source: "agent",
734
- summary: `Agent cancelled: ${updated.label}`,
735
- detail: `Task ${taskId}`,
736
- actionLabel: updated.origin === "ai" ? "AUTONOMOUS" : "PROMPTED",
737
- reason: "agent cancelled by user",
738
- });
739
- pushNotification({
740
- timestamp: new Date().toISOString(),
741
- source: "agent",
742
- message: `Agent task "${updated.label}" was cancelled.`,
743
- });
744
- }
745
- return updated !== null;
746
- }
747
- // ─── Reflection ───────────────────────────────────────────────────────────────
748
- const reflectionLog = createLogger("reflection");
749
- const REFLECTION_PROMPT = `You are reflecting on a completed autonomous agent task. Answer concisely in JSON:
750
- {
751
- "movedGoalForward": true/false,
752
- "hitGuardrail": true/false,
753
- "adjustment": "what to do differently next time, or null if nothing",
754
- "summary": "1 sentence: what happened and was it useful?"
755
- }
756
- Be honest. If the task failed or produced nothing useful, say so.`;
757
- async function reflectOnCompletion(task, output) {
758
- // Only reflect on autonomous actions
759
- if (task.origin !== "ai")
760
- return;
761
- try {
762
- // Tier 1: Micro-reflection for routine successes — skip LLM call
763
- const isRoutineSuccess = task.exitCode === 0 && output.trim().length > 100;
764
- if (isRoutineSuccess) {
765
- const microReflection = {
766
- movedGoalForward: true,
767
- hitGuardrail: false,
768
- adjustment: undefined,
769
- summary: `Completed "${task.label}" successfully.`,
770
- };
771
- await updateTask(task.id, { reflection: microReflection });
772
- reflectionLog.info(`Micro-reflection (routine success): "${task.label}"`);
773
- return;
774
- }
775
- // Tier 2: Full LLM reflection for non-routine completions
776
- const provider = resolveAgentProvider();
777
- const model = await resolveAgentModelAsync();
778
- const response = await completeChat({
779
- messages: [
780
- { role: "system", content: REFLECTION_PROMPT },
781
- { role: "user", content: JSON.stringify({
782
- label: task.label,
783
- status: task.status,
784
- output: output.slice(0, 1000),
785
- exitCode: task.exitCode,
786
- }) },
787
- ],
788
- model,
789
- provider,
790
- });
791
- // Parse JSON response — strip markdown fences if present
792
- const jsonStr = response.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
793
- const reflection = JSON.parse(jsonStr);
794
- // Write reflection to task record
795
- await updateTask(task.id, { reflection });
796
- // Log reflective activity entry (only for non-routine cases that warranted LLM analysis)
797
- logActivity({
798
- source: "agent",
799
- summary: `Reflection on "${task.label}": ${reflection.summary}`,
800
- actionLabel: "REFLECTIVE",
801
- reason: "post-completion autonomous reflection",
802
- backref: task.id,
803
- });
804
- reflectionLog.info(`Reflected on "${task.label}": ${reflection.summary}`);
805
- }
806
- catch (err) {
807
- reflectionLog.warn(`Reflection failed for "${task.label}": ${err instanceof Error ? err.message : String(err)}`);
808
- }
809
- }
810
- // ─── Scar Evaluation ──────────────────────────────────────────────────────────
811
- const SCAR_EVAL_PROMPT = `You are evaluating whether a completed "Fix:" agent's work qualifies as a scar — a self-repair proven by code, tests, and validation.
812
-
813
- A fix qualifies as a scar ONLY if the output shows ALL of these:
814
- 1. A clear root cause was identified (the "anchor")
815
- 2. Code was changed to correct the defect
816
- 3. Tests or validation were added/run to prove the fix works
817
-
818
- Respond in JSON:
819
- {
820
- "isScar": true/false,
821
- "anchor": "root cause description (or null if not a scar)",
822
- "woundSummary": "one-line description of the original failure (or null)",
823
- "artifacts": {
824
- "prevention": ["measures added to prevent recurrence"],
825
- "detection": ["signals added to detect the issue earlier"],
826
- "correction": ["code changes that corrected the defect"],
827
- "regressionTests": ["test files/cases that guard against regression"]
828
- }
829
- }
830
-
831
- If the fix was partial, untested, or just a workaround, set isScar to false.`;
832
- /**
833
- * Evaluate whether a completed Fix: agent produced a scar-worthy repair.
834
- * Called for successful Fix: agents after completion.
835
- */
836
- async function evaluateScar(task, output) {
837
- // Only evaluate successful Fix: agents
838
- if (!task.label.startsWith("Fix: "))
839
- return;
840
- if (task.status !== "completed")
841
- return;
842
- try {
843
- const provider = resolveAgentProvider();
844
- const model = await resolveAgentModelAsync();
845
- const response = await completeChat({
846
- messages: [
847
- { role: "system", content: SCAR_EVAL_PROMPT },
848
- { role: "user", content: JSON.stringify({
849
- label: task.label,
850
- output: output.slice(0, 2000),
851
- exitCode: task.exitCode,
852
- }) },
853
- ],
854
- model,
855
- provider,
856
- });
857
- const jsonStr = response.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
858
- const evaluation = JSON.parse(jsonStr);
859
- if (!evaluation.isScar) {
860
- reflectionLog.info(`Fix "${task.label}" did not qualify as scar`);
861
- return;
862
- }
863
- await recordScar({
864
- anchor: evaluation.anchor,
865
- woundSummary: evaluation.woundSummary,
866
- healedAt: new Date().toISOString(),
867
- agentId: task.id,
868
- artifacts: {
869
- prevention: evaluation.artifacts?.prevention ?? [],
870
- detection: evaluation.artifacts?.detection ?? [],
871
- correction: evaluation.artifacts?.correction ?? [],
872
- regressionTests: evaluation.artifacts?.regressionTests ?? [],
873
- },
874
- });
875
- logActivity({
876
- source: "agent",
877
- summary: `Scar recorded for "${task.label}": ${evaluation.woundSummary}`,
878
- actionLabel: "REFLECTIVE",
879
- reason: "fix agent produced validated self-repair",
880
- backref: task.id,
881
- });
882
- }
883
- catch (err) {
884
- reflectionLog.warn(`Scar evaluation failed for "${task.label}": ${err instanceof Error ? err.message : String(err)}`);
885
- }
886
- }
887
- /**
888
- * Generate a consolidated handoff report when a board task hits 3+ failures.
889
- * Persists to brain/agents/bridge-reports/ and pushes a notification.
890
- */
891
- export async function generateBridgeReport(boardTaskId, taskLabel, failureCount) {
892
- try {
893
- const allTasks = await listTasks();
894
- const related = allTasks
895
- .filter((t) => t.label === taskLabel || t.label === `Fix: ${taskLabel}`)
896
- .sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
897
- const attempts = related
898
- .filter((t) => t.label === taskLabel)
899
- .map((t, i) => ({
900
- round: i + 1,
901
- exitCode: t.exitCode ?? null,
902
- outputSnippet: (t.resultSummary ?? "").slice(0, 300),
903
- timestamp: t.finishedAt ?? t.createdAt ?? "",
904
- }));
905
- const recoveryAttempts = related
906
- .filter((t) => t.label.startsWith("Fix: "))
907
- .map((t) => `Recovery ${t.status}: ${(t.resultSummary ?? "").slice(0, 200)}`);
908
- const rootCauses = [];
909
- for (const t of related) {
910
- if (t.reflection?.adjustment)
911
- rootCauses.push(t.reflection.adjustment);
912
- }
913
- const report = {
914
- taskId: boardTaskId,
915
- taskLabel,
916
- failureCount,
917
- attempts,
918
- rootCauses: [...new Set(rootCauses)],
919
- recoveryAttempts,
920
- recommendation: rootCauses.length > 0
921
- ? `Repeated root cause: ${rootCauses[0]}. Manual investigation needed.`
922
- : `${failureCount} failures with no clear root cause. Check agent logs for ${taskLabel}.`,
923
- };
924
- const reportText = [
925
- `## Bridge Report: "${taskLabel}"`,
926
- `**${failureCount} consecutive failures** — handing off to human.`,
927
- ``,
928
- `### Attempts`,
929
- ...attempts.map((a) => `- Round ${a.round} (exit ${a.exitCode}): ${a.outputSnippet.slice(0, 100)}...`),
930
- ``,
931
- `### Root Causes Identified`,
932
- ...(rootCauses.length > 0 ? rootCauses.map((r) => `- ${r}`) : ["- No root causes identified by reflection"]),
933
- ``,
934
- `### Recovery Attempts`,
935
- ...(recoveryAttempts.length > 0 ? recoveryAttempts.map((r) => `- ${r}`) : ["- None"]),
936
- ``,
937
- `### Recommendation`,
938
- report.recommendation,
939
- ].join("\n");
940
- pushNotification({
941
- timestamp: new Date().toISOString(),
942
- source: "agent",
943
- message: reportText,
944
- });
945
- recordBridgeReportMetric();
946
- logActivity({
947
- source: "agent",
948
- summary: `Bridge report generated for "${taskLabel}" (${failureCount} failures)`,
949
- detail: JSON.stringify(report),
950
- actionLabel: "AUTONOMOUS",
951
- reason: "structural impasse — repeated same-task failures",
952
- });
953
- // Persist as brain document for future reference
954
- const reportsDir = join(BRAIN_DIR, "agents", "bridge-reports");
955
- mkdirSync(reportsDir, { recursive: true });
956
- const reportPath = join(reportsDir, `${boardTaskId}-${Date.now()}.md`);
957
- writeFileSync(reportPath, reportText, "utf-8");
958
- }
959
- catch (err) {
960
- reflectionLog.warn(`Bridge report failed for "${taskLabel}": ${err instanceof Error ? err.message : String(err)}`);
961
- }
962
- }
963
- function clearTaskTimer(taskId) {
964
- const timer = activeTimers.get(taskId);
965
- if (timer) {
966
- clearTimeout(timer);
967
- activeTimers.delete(taskId);
968
- }
969
- }
970
- //# sourceMappingURL=spawn.js.map