macro-agent 0.0.10 → 0.0.12

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 (518) hide show
  1. package/.macro-agent/teams/self-driving/prompts/grinder.md +27 -0
  2. package/.macro-agent/teams/self-driving/prompts/judge.md +27 -0
  3. package/.macro-agent/teams/self-driving/prompts/planner.md +33 -0
  4. package/.macro-agent/teams/self-driving/roles/grinder.yaml +17 -0
  5. package/.macro-agent/teams/self-driving/roles/judge.yaml +24 -0
  6. package/.macro-agent/teams/self-driving/roles/planner.yaml +18 -0
  7. package/.macro-agent/teams/self-driving/team.yaml +103 -0
  8. package/.macro-agent/teams/structured/prompts/developer.md +26 -0
  9. package/.macro-agent/teams/structured/prompts/lead.md +25 -0
  10. package/.macro-agent/teams/structured/prompts/reviewer.md +24 -0
  11. package/.macro-agent/teams/structured/roles/developer.yaml +12 -0
  12. package/.macro-agent/teams/structured/roles/lead.yaml +11 -0
  13. package/.macro-agent/teams/structured/roles/reviewer.yaml +19 -0
  14. package/.macro-agent/teams/structured/team.yaml +89 -0
  15. package/.sudocode/issues.jsonl +56 -51
  16. package/.sudocode/specs.jsonl +8 -1
  17. package/CLAUDE.md +121 -30
  18. package/README.md +60 -3
  19. package/dist/acp/macro-agent.d.ts +4 -0
  20. package/dist/acp/macro-agent.d.ts.map +1 -1
  21. package/dist/acp/macro-agent.js +50 -4
  22. package/dist/acp/macro-agent.js.map +1 -1
  23. package/dist/acp/session-mapper.d.ts +20 -1
  24. package/dist/acp/session-mapper.d.ts.map +1 -1
  25. package/dist/acp/session-mapper.js +90 -1
  26. package/dist/acp/session-mapper.js.map +1 -1
  27. package/dist/acp/types.d.ts +24 -1
  28. package/dist/acp/types.d.ts.map +1 -1
  29. package/dist/acp/types.js.map +1 -1
  30. package/dist/agent/agent-manager.d.ts +40 -1
  31. package/dist/agent/agent-manager.d.ts.map +1 -1
  32. package/dist/agent/agent-manager.js +172 -8
  33. package/dist/agent/agent-manager.js.map +1 -1
  34. package/dist/agent/types.d.ts +22 -0
  35. package/dist/agent/types.d.ts.map +1 -1
  36. package/dist/agent/types.js.map +1 -1
  37. package/dist/agent/wake.d.ts +15 -0
  38. package/dist/agent/wake.d.ts.map +1 -1
  39. package/dist/agent/wake.js +15 -0
  40. package/dist/agent/wake.js.map +1 -1
  41. package/dist/agent-detection/command-builder.d.ts +30 -0
  42. package/dist/agent-detection/command-builder.d.ts.map +1 -0
  43. package/dist/agent-detection/command-builder.js +71 -0
  44. package/dist/agent-detection/command-builder.js.map +1 -0
  45. package/dist/agent-detection/detector.d.ts +84 -0
  46. package/dist/agent-detection/detector.d.ts.map +1 -0
  47. package/dist/agent-detection/detector.js +240 -0
  48. package/dist/agent-detection/detector.js.map +1 -0
  49. package/dist/agent-detection/index.d.ts +12 -0
  50. package/dist/agent-detection/index.d.ts.map +1 -0
  51. package/dist/agent-detection/index.js +14 -0
  52. package/dist/agent-detection/index.js.map +1 -0
  53. package/dist/agent-detection/registry.d.ts +53 -0
  54. package/dist/agent-detection/registry.d.ts.map +1 -0
  55. package/dist/agent-detection/registry.js +177 -0
  56. package/dist/agent-detection/registry.js.map +1 -0
  57. package/dist/agent-detection/types.d.ts +121 -0
  58. package/dist/agent-detection/types.d.ts.map +1 -0
  59. package/dist/agent-detection/types.js +20 -0
  60. package/dist/agent-detection/types.js.map +1 -0
  61. package/dist/api/server.d.ts +5 -1
  62. package/dist/api/server.d.ts.map +1 -1
  63. package/dist/api/server.js +362 -0
  64. package/dist/api/server.js.map +1 -1
  65. package/dist/api/types.d.ts +50 -1
  66. package/dist/api/types.d.ts.map +1 -1
  67. package/dist/cli/acp.d.ts +2 -0
  68. package/dist/cli/acp.d.ts.map +1 -1
  69. package/dist/cli/acp.js +8 -1
  70. package/dist/cli/acp.js.map +1 -1
  71. package/dist/cli/index.js +29 -0
  72. package/dist/cli/index.js.map +1 -1
  73. package/dist/cli/mcp.js +38 -0
  74. package/dist/cli/mcp.js.map +1 -1
  75. package/dist/config/index.d.ts +2 -0
  76. package/dist/config/index.d.ts.map +1 -0
  77. package/dist/config/index.js +2 -0
  78. package/dist/config/index.js.map +1 -0
  79. package/dist/config/project-config.d.ts +46 -0
  80. package/dist/config/project-config.d.ts.map +1 -0
  81. package/dist/config/project-config.js +68 -0
  82. package/dist/config/project-config.js.map +1 -0
  83. package/dist/lifecycle/cascade.d.ts +1 -1
  84. package/dist/lifecycle/cascade.d.ts.map +1 -1
  85. package/dist/lifecycle/handlers/index.d.ts +4 -0
  86. package/dist/lifecycle/handlers/index.d.ts.map +1 -1
  87. package/dist/lifecycle/handlers/index.js +2 -0
  88. package/dist/lifecycle/handlers/index.js.map +1 -1
  89. package/dist/lifecycle/handlers/worker.d.ts +4 -0
  90. package/dist/lifecycle/handlers/worker.d.ts.map +1 -1
  91. package/dist/lifecycle/handlers/worker.js +35 -3
  92. package/dist/lifecycle/handlers/worker.js.map +1 -1
  93. package/dist/mail/conversation-map.d.ts +33 -0
  94. package/dist/mail/conversation-map.d.ts.map +1 -0
  95. package/dist/mail/conversation-map.js +61 -0
  96. package/dist/mail/conversation-map.js.map +1 -0
  97. package/dist/mail/index.d.ts +11 -0
  98. package/dist/mail/index.d.ts.map +1 -0
  99. package/dist/mail/index.js +11 -0
  100. package/dist/mail/index.js.map +1 -0
  101. package/dist/mail/mail-service.d.ts +85 -0
  102. package/dist/mail/mail-service.d.ts.map +1 -0
  103. package/dist/mail/mail-service.js +121 -0
  104. package/dist/mail/mail-service.js.map +1 -0
  105. package/dist/mail/stores/eventstore-conversation-store.d.ts +40 -0
  106. package/dist/mail/stores/eventstore-conversation-store.d.ts.map +1 -0
  107. package/dist/mail/stores/eventstore-conversation-store.js +131 -0
  108. package/dist/mail/stores/eventstore-conversation-store.js.map +1 -0
  109. package/dist/mail/stores/eventstore-participant-store.d.ts +43 -0
  110. package/dist/mail/stores/eventstore-participant-store.d.ts.map +1 -0
  111. package/dist/mail/stores/eventstore-participant-store.js +145 -0
  112. package/dist/mail/stores/eventstore-participant-store.js.map +1 -0
  113. package/dist/mail/stores/eventstore-thread-store.d.ts +46 -0
  114. package/dist/mail/stores/eventstore-thread-store.d.ts.map +1 -0
  115. package/dist/mail/stores/eventstore-thread-store.js +118 -0
  116. package/dist/mail/stores/eventstore-thread-store.js.map +1 -0
  117. package/dist/mail/stores/eventstore-turn-store.d.ts +47 -0
  118. package/dist/mail/stores/eventstore-turn-store.d.ts.map +1 -0
  119. package/dist/mail/stores/eventstore-turn-store.js +153 -0
  120. package/dist/mail/stores/eventstore-turn-store.js.map +1 -0
  121. package/dist/mail/stores/index.d.ts +12 -0
  122. package/dist/mail/stores/index.d.ts.map +1 -0
  123. package/dist/mail/stores/index.js +12 -0
  124. package/dist/mail/stores/index.js.map +1 -0
  125. package/dist/mail/stores/types.d.ts +146 -0
  126. package/dist/mail/stores/types.d.ts.map +1 -0
  127. package/dist/mail/stores/types.js +13 -0
  128. package/dist/mail/stores/types.js.map +1 -0
  129. package/dist/mail/turn-recorder.d.ts +30 -0
  130. package/dist/mail/turn-recorder.d.ts.map +1 -0
  131. package/dist/mail/turn-recorder.js +98 -0
  132. package/dist/mail/turn-recorder.js.map +1 -0
  133. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  134. package/dist/map/adapter/acp-over-map.js +32 -2
  135. package/dist/map/adapter/acp-over-map.js.map +1 -1
  136. package/dist/map/adapter/event-translator.d.ts.map +1 -1
  137. package/dist/map/adapter/event-translator.js +4 -0
  138. package/dist/map/adapter/event-translator.js.map +1 -1
  139. package/dist/map/adapter/extensions/agent-detection.d.ts +49 -0
  140. package/dist/map/adapter/extensions/agent-detection.d.ts.map +1 -0
  141. package/dist/map/adapter/extensions/agent-detection.js +91 -0
  142. package/dist/map/adapter/extensions/agent-detection.js.map +1 -0
  143. package/dist/map/adapter/extensions/index.d.ts +10 -1
  144. package/dist/map/adapter/extensions/index.d.ts.map +1 -1
  145. package/dist/map/adapter/extensions/index.js +39 -0
  146. package/dist/map/adapter/extensions/index.js.map +1 -1
  147. package/dist/map/adapter/extensions/resume.d.ts +47 -0
  148. package/dist/map/adapter/extensions/resume.d.ts.map +1 -0
  149. package/dist/map/adapter/extensions/resume.js +59 -0
  150. package/dist/map/adapter/extensions/resume.js.map +1 -0
  151. package/dist/map/adapter/extensions/workspace-files.d.ts +42 -0
  152. package/dist/map/adapter/extensions/workspace-files.d.ts.map +1 -0
  153. package/dist/map/adapter/extensions/workspace-files.js +338 -0
  154. package/dist/map/adapter/extensions/workspace-files.js.map +1 -0
  155. package/dist/map/adapter/mail-handler-adapter.d.ts +27 -0
  156. package/dist/map/adapter/mail-handler-adapter.d.ts.map +1 -0
  157. package/dist/map/adapter/mail-handler-adapter.js +292 -0
  158. package/dist/map/adapter/mail-handler-adapter.js.map +1 -0
  159. package/dist/map/adapter/map-adapter.d.ts +34 -10
  160. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  161. package/dist/map/adapter/map-adapter.js +110 -14
  162. package/dist/map/adapter/map-adapter.js.map +1 -1
  163. package/dist/map/adapter/rpc-handler.d.ts +4 -1
  164. package/dist/map/adapter/rpc-handler.d.ts.map +1 -1
  165. package/dist/map/adapter/rpc-handler.js +6 -0
  166. package/dist/map/adapter/rpc-handler.js.map +1 -1
  167. package/dist/map/index.d.ts +1 -0
  168. package/dist/map/index.d.ts.map +1 -1
  169. package/dist/map/index.js +2 -0
  170. package/dist/map/index.js.map +1 -1
  171. package/dist/map/types.d.ts +3 -1
  172. package/dist/map/types.d.ts.map +1 -1
  173. package/dist/map/types.js.map +1 -1
  174. package/dist/mcp/mcp-server.d.ts +6 -0
  175. package/dist/mcp/mcp-server.d.ts.map +1 -1
  176. package/dist/mcp/mcp-server.js +45 -0
  177. package/dist/mcp/mcp-server.js.map +1 -1
  178. package/dist/mcp/tools/claim_task.d.ts +35 -0
  179. package/dist/mcp/tools/claim_task.d.ts.map +1 -0
  180. package/dist/mcp/tools/claim_task.js +58 -0
  181. package/dist/mcp/tools/claim_task.js.map +1 -0
  182. package/dist/mcp/tools/done.d.ts +15 -2
  183. package/dist/mcp/tools/done.d.ts.map +1 -1
  184. package/dist/mcp/tools/done.js +45 -10
  185. package/dist/mcp/tools/done.js.map +1 -1
  186. package/dist/mcp/tools/list_claimable_tasks.d.ts +38 -0
  187. package/dist/mcp/tools/list_claimable_tasks.d.ts.map +1 -0
  188. package/dist/mcp/tools/list_claimable_tasks.js +63 -0
  189. package/dist/mcp/tools/list_claimable_tasks.js.map +1 -0
  190. package/dist/mcp/tools/unclaim_task.d.ts +31 -0
  191. package/dist/mcp/tools/unclaim_task.d.ts.map +1 -0
  192. package/dist/mcp/tools/unclaim_task.js +47 -0
  193. package/dist/mcp/tools/unclaim_task.js.map +1 -0
  194. package/dist/metrics/index.d.ts +2 -0
  195. package/dist/metrics/index.d.ts.map +1 -0
  196. package/dist/metrics/index.js +2 -0
  197. package/dist/metrics/index.js.map +1 -0
  198. package/dist/metrics/metrics.d.ts +79 -0
  199. package/dist/metrics/metrics.d.ts.map +1 -0
  200. package/dist/metrics/metrics.js +166 -0
  201. package/dist/metrics/metrics.js.map +1 -0
  202. package/dist/roles/capabilities.d.ts +1 -0
  203. package/dist/roles/capabilities.d.ts.map +1 -1
  204. package/dist/roles/capabilities.js +3 -0
  205. package/dist/roles/capabilities.js.map +1 -1
  206. package/dist/roles/types.d.ts +1 -1
  207. package/dist/roles/types.d.ts.map +1 -1
  208. package/dist/router/channels.d.ts +2 -4
  209. package/dist/router/channels.d.ts.map +1 -1
  210. package/dist/router/channels.js.map +1 -1
  211. package/dist/router/message-router.d.ts +85 -9
  212. package/dist/router/message-router.d.ts.map +1 -1
  213. package/dist/router/message-router.js +203 -14
  214. package/dist/router/message-router.js.map +1 -1
  215. package/dist/router/role-resolver.d.ts +10 -1
  216. package/dist/router/role-resolver.d.ts.map +1 -1
  217. package/dist/router/role-resolver.js +15 -1
  218. package/dist/router/role-resolver.js.map +1 -1
  219. package/dist/router/types.d.ts +30 -1
  220. package/dist/router/types.d.ts.map +1 -1
  221. package/dist/router/types.js.map +1 -1
  222. package/dist/server/combined-server.d.ts +6 -0
  223. package/dist/server/combined-server.d.ts.map +1 -1
  224. package/dist/server/combined-server.js +24 -2
  225. package/dist/server/combined-server.js.map +1 -1
  226. package/dist/store/event-store.d.ts +14 -1
  227. package/dist/store/event-store.d.ts.map +1 -1
  228. package/dist/store/event-store.js +456 -4
  229. package/dist/store/event-store.js.map +1 -1
  230. package/dist/store/types/agents.d.ts +1 -1
  231. package/dist/store/types/agents.d.ts.map +1 -1
  232. package/dist/store/types/conversations.d.ts +91 -0
  233. package/dist/store/types/conversations.d.ts.map +1 -0
  234. package/dist/store/types/conversations.js +8 -0
  235. package/dist/store/types/conversations.js.map +1 -0
  236. package/dist/store/types/events.d.ts +1 -1
  237. package/dist/store/types/events.d.ts.map +1 -1
  238. package/dist/store/types/events.js.map +1 -1
  239. package/dist/store/types/index.d.ts +2 -0
  240. package/dist/store/types/index.d.ts.map +1 -1
  241. package/dist/store/types/index.js +2 -0
  242. package/dist/store/types/index.js.map +1 -1
  243. package/dist/store/types/sessions.d.ts +44 -0
  244. package/dist/store/types/sessions.d.ts.map +1 -0
  245. package/dist/store/types/sessions.js +9 -0
  246. package/dist/store/types/sessions.js.map +1 -0
  247. package/dist/store/types/tasks.d.ts +2 -0
  248. package/dist/store/types/tasks.d.ts.map +1 -1
  249. package/dist/task/backend/memory.d.ts +4 -1
  250. package/dist/task/backend/memory.d.ts.map +1 -1
  251. package/dist/task/backend/memory.js +81 -0
  252. package/dist/task/backend/memory.js.map +1 -1
  253. package/dist/task/backend/types.d.ts +30 -0
  254. package/dist/task/backend/types.d.ts.map +1 -1
  255. package/dist/task/backend/types.js.map +1 -1
  256. package/dist/teams/index.d.ts +4 -0
  257. package/dist/teams/index.d.ts.map +1 -0
  258. package/dist/teams/index.js +4 -0
  259. package/dist/teams/index.js.map +1 -0
  260. package/dist/teams/team-loader.d.ts +20 -0
  261. package/dist/teams/team-loader.d.ts.map +1 -0
  262. package/dist/teams/team-loader.js +293 -0
  263. package/dist/teams/team-loader.js.map +1 -0
  264. package/dist/teams/team-runtime.d.ts +139 -0
  265. package/dist/teams/team-runtime.d.ts.map +1 -0
  266. package/dist/teams/team-runtime.js +613 -0
  267. package/dist/teams/team-runtime.js.map +1 -0
  268. package/dist/teams/types.d.ts +266 -0
  269. package/dist/teams/types.d.ts.map +1 -0
  270. package/dist/teams/types.js +20 -0
  271. package/dist/teams/types.js.map +1 -0
  272. package/dist/trigger/router/trigger-router.d.ts +30 -3
  273. package/dist/trigger/router/trigger-router.d.ts.map +1 -1
  274. package/dist/trigger/router/trigger-router.js +30 -3
  275. package/dist/trigger/router/trigger-router.js.map +1 -1
  276. package/dist/trigger/wake/types.d.ts +31 -5
  277. package/dist/trigger/wake/types.d.ts.map +1 -1
  278. package/dist/trigger/wake/types.js +19 -0
  279. package/dist/trigger/wake/types.js.map +1 -1
  280. package/dist/workspace/dataplane-adapter.d.ts +1 -1
  281. package/dist/workspace/dataplane-adapter.d.ts.map +1 -1
  282. package/dist/workspace/dataplane-adapter.js +1 -1
  283. package/dist/workspace/dataplane-adapter.js.map +1 -1
  284. package/dist/workspace/index.d.ts +1 -1
  285. package/dist/workspace/index.d.ts.map +1 -1
  286. package/dist/workspace/strategies/index.d.ts +6 -0
  287. package/dist/workspace/strategies/index.d.ts.map +1 -0
  288. package/dist/workspace/strategies/index.js +5 -0
  289. package/dist/workspace/strategies/index.js.map +1 -0
  290. package/dist/workspace/strategies/optimistic.d.ts +26 -0
  291. package/dist/workspace/strategies/optimistic.d.ts.map +1 -0
  292. package/dist/workspace/strategies/optimistic.js +121 -0
  293. package/dist/workspace/strategies/optimistic.js.map +1 -0
  294. package/dist/workspace/strategies/queue.d.ts +26 -0
  295. package/dist/workspace/strategies/queue.d.ts.map +1 -0
  296. package/dist/workspace/strategies/queue.js +67 -0
  297. package/dist/workspace/strategies/queue.js.map +1 -0
  298. package/dist/workspace/strategies/registry.d.ts +37 -0
  299. package/dist/workspace/strategies/registry.d.ts.map +1 -0
  300. package/dist/workspace/strategies/registry.js +63 -0
  301. package/dist/workspace/strategies/registry.js.map +1 -0
  302. package/dist/workspace/strategies/trunk.d.ts +20 -0
  303. package/dist/workspace/strategies/trunk.d.ts.map +1 -0
  304. package/dist/workspace/strategies/trunk.js +108 -0
  305. package/dist/workspace/strategies/trunk.js.map +1 -0
  306. package/dist/workspace/strategies/types.d.ts +104 -0
  307. package/dist/workspace/strategies/types.d.ts.map +1 -0
  308. package/dist/workspace/strategies/types.js +11 -0
  309. package/dist/workspace/strategies/types.js.map +1 -0
  310. package/dist/workspace/types.d.ts +1 -1
  311. package/dist/workspace/types.d.ts.map +1 -1
  312. package/dist/workspace/workspace-manager.d.ts +1 -1
  313. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  314. package/docs/implementation-details.md +1127 -0
  315. package/docs/implementation-summary.md +448 -0
  316. package/docs/mail-integration.md +608 -0
  317. package/docs/plan-self-driving-support.md +433 -0
  318. package/docs/spec-self-driving-support.md +462 -0
  319. package/docs/team-templates.md +860 -0
  320. package/docs/teams.md +233 -0
  321. package/package.json +5 -3
  322. package/src/acp/__tests__/integration.test.ts +161 -1
  323. package/src/acp/__tests__/macro-agent.test.ts +95 -0
  324. package/src/acp/__tests__/session-persistence.test.ts +276 -0
  325. package/src/acp/macro-agent.ts +79 -7
  326. package/src/acp/session-mapper.ts +108 -1
  327. package/src/acp/types.ts +33 -1
  328. package/src/agent/agent-manager.ts +278 -6
  329. package/src/agent/types.ts +27 -0
  330. package/src/agent/wake.ts +15 -0
  331. package/src/agent-detection/__tests__/command-builder.test.ts +336 -0
  332. package/src/agent-detection/__tests__/detector.test.ts +768 -0
  333. package/src/agent-detection/__tests__/registry.test.ts +254 -0
  334. package/src/agent-detection/command-builder.ts +90 -0
  335. package/src/agent-detection/detector.ts +307 -0
  336. package/src/agent-detection/index.ts +36 -0
  337. package/src/agent-detection/registry.ts +200 -0
  338. package/src/agent-detection/types.ts +184 -0
  339. package/src/api/__tests__/conversation-api.test.ts +468 -0
  340. package/src/api/server.ts +425 -1
  341. package/src/api/types.ts +64 -1
  342. package/src/cli/acp.ts +9 -1
  343. package/src/cli/index.ts +44 -0
  344. package/src/cli/mcp.ts +47 -0
  345. package/src/config/index.ts +9 -0
  346. package/src/config/project-config.ts +107 -0
  347. package/src/lifecycle/cascade.ts +1 -1
  348. package/src/lifecycle/handlers/index.ts +8 -0
  349. package/src/lifecycle/handlers/worker.ts +48 -3
  350. package/src/mail/__tests__/conversation-lifecycle.test.ts +409 -0
  351. package/src/mail/__tests__/eventstore-stores.test.ts +1073 -0
  352. package/src/mail/__tests__/mail-full-agent.e2e.test.ts +575 -0
  353. package/src/mail/__tests__/mail-integration.test.ts +759 -0
  354. package/src/mail/__tests__/mail-map-protocol.e2e.test.ts +1068 -0
  355. package/src/mail/__tests__/mail-service.test.ts +506 -0
  356. package/src/mail/__tests__/turn-recorder.test.ts +328 -0
  357. package/src/mail/conversation-map.ts +107 -0
  358. package/src/mail/index.ts +25 -0
  359. package/src/mail/mail-service.ts +257 -0
  360. package/src/mail/stores/eventstore-conversation-store.ts +146 -0
  361. package/src/mail/stores/eventstore-participant-store.ts +172 -0
  362. package/src/mail/stores/eventstore-thread-store.ts +129 -0
  363. package/src/mail/stores/eventstore-turn-store.ts +173 -0
  364. package/src/mail/stores/index.ts +12 -0
  365. package/src/mail/stores/types.ts +160 -0
  366. package/src/mail/turn-recorder.ts +124 -0
  367. package/src/map/README.md +79 -0
  368. package/src/map/adapter/__tests__/extensions.test.ts +359 -0
  369. package/src/map/adapter/__tests__/map-adapter.test.ts +90 -0
  370. package/src/map/adapter/__tests__/workspace-files.test.ts +673 -0
  371. package/src/map/adapter/acp-over-map.ts +45 -2
  372. package/src/map/adapter/event-translator.ts +4 -0
  373. package/src/map/adapter/extensions/agent-detection.ts +201 -0
  374. package/src/map/adapter/extensions/index.ts +63 -0
  375. package/src/map/adapter/extensions/resume.ts +114 -0
  376. package/src/map/adapter/extensions/workspace-files.ts +449 -0
  377. package/src/map/adapter/mail-handler-adapter.ts +429 -0
  378. package/src/map/adapter/map-adapter.ts +173 -27
  379. package/src/map/adapter/rpc-handler.ts +8 -1
  380. package/src/map/index.ts +3 -0
  381. package/src/map/types.ts +3 -1
  382. package/src/mcp/mcp-server.ts +67 -0
  383. package/src/mcp/tools/claim_task.ts +86 -0
  384. package/src/mcp/tools/done.ts +59 -10
  385. package/src/mcp/tools/list_claimable_tasks.ts +93 -0
  386. package/src/mcp/tools/unclaim_task.ts +71 -0
  387. package/src/metrics/index.ts +9 -0
  388. package/src/metrics/metrics.ts +280 -0
  389. package/src/roles/capabilities.ts +3 -0
  390. package/src/roles/types.ts +2 -1
  391. package/src/router/README.md +120 -0
  392. package/src/router/__tests__/message-router.test.ts +561 -0
  393. package/src/router/channels.ts +3 -4
  394. package/src/router/message-router.ts +308 -22
  395. package/src/router/role-resolver.ts +22 -1
  396. package/src/router/types.ts +36 -1
  397. package/src/server/combined-server.ts +36 -2
  398. package/src/store/README.md +134 -0
  399. package/src/store/event-store.ts +546 -3
  400. package/src/store/types/agents.ts +1 -1
  401. package/src/store/types/conversations.ts +129 -0
  402. package/src/store/types/events.ts +5 -1
  403. package/src/store/types/index.ts +2 -0
  404. package/src/store/types/sessions.ts +53 -0
  405. package/src/store/types/tasks.ts +3 -0
  406. package/src/task/backend/memory.ts +116 -0
  407. package/src/task/backend/types.ts +43 -0
  408. package/src/teams/__tests__/cross-subsystem.integration.test.ts +983 -0
  409. package/src/teams/__tests__/e2e/team-runtime.e2e.test.ts +553 -0
  410. package/src/teams/__tests__/team-system.test.ts +1280 -0
  411. package/src/teams/index.ts +13 -0
  412. package/src/teams/team-loader.ts +434 -0
  413. package/src/teams/team-runtime.ts +727 -0
  414. package/src/teams/types.ts +377 -0
  415. package/src/trigger/router/trigger-router.ts +30 -3
  416. package/src/trigger/wake/types.ts +32 -5
  417. package/src/trigger/wake/wake-manager.ts +2 -2
  418. package/src/workspace/dataplane-adapter.ts +1 -1
  419. package/src/workspace/index.ts +1 -1
  420. package/src/workspace/strategies/index.ts +18 -0
  421. package/src/workspace/strategies/optimistic.ts +136 -0
  422. package/src/workspace/strategies/queue.ts +81 -0
  423. package/src/workspace/strategies/registry.ts +89 -0
  424. package/src/workspace/strategies/trunk.ts +123 -0
  425. package/src/workspace/strategies/types.ts +145 -0
  426. package/src/workspace/types.ts +1 -1
  427. package/src/workspace/workspace-manager.ts +1 -1
  428. package/.claude/settings.local.json +0 -59
  429. package/dist/map/utils/address-translation.d.ts +0 -99
  430. package/dist/map/utils/address-translation.d.ts.map +0 -1
  431. package/dist/map/utils/address-translation.js +0 -285
  432. package/dist/map/utils/address-translation.js.map +0 -1
  433. package/dist/map/utils/index.d.ts +0 -7
  434. package/dist/map/utils/index.d.ts.map +0 -1
  435. package/dist/map/utils/index.js +0 -7
  436. package/dist/map/utils/index.js.map +0 -1
  437. package/openspec/AGENTS.md +0 -456
  438. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/design.md +0 -128
  439. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/proposal.md +0 -49
  440. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/agent-manager/spec.md +0 -150
  441. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/cli-api/spec.md +0 -258
  442. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/event-store/spec.md +0 -160
  443. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/mcp-tools/spec.md +0 -224
  444. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/message-router/spec.md +0 -153
  445. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/specs/task-manager/spec.md +0 -136
  446. package/openspec/changes/archive/2025-12-21-add-mvp-foundation/tasks.md +0 -147
  447. package/openspec/project.md +0 -31
  448. package/openspec/specs/agent-manager/spec.md +0 -154
  449. package/openspec/specs/cli-api/spec.md +0 -262
  450. package/openspec/specs/event-store/spec.md +0 -164
  451. package/openspec/specs/mcp-tools/spec.md +0 -228
  452. package/openspec/specs/message-router/spec.md +0 -157
  453. package/openspec/specs/task-manager/spec.md +0 -140
  454. package/references/acp-factory-ref/CHANGELOG.md +0 -33
  455. package/references/acp-factory-ref/LICENSE +0 -21
  456. package/references/acp-factory-ref/README.md +0 -341
  457. package/references/acp-factory-ref/package-lock.json +0 -3102
  458. package/references/acp-factory-ref/package.json +0 -96
  459. package/references/acp-factory-ref/python/CHANGELOG.md +0 -33
  460. package/references/acp-factory-ref/python/LICENSE +0 -21
  461. package/references/acp-factory-ref/python/Makefile +0 -57
  462. package/references/acp-factory-ref/python/README.md +0 -253
  463. package/references/acp-factory-ref/python/pyproject.toml +0 -73
  464. package/references/acp-factory-ref/python/tests/__init__.py +0 -0
  465. package/references/acp-factory-ref/python/tests/e2e/__init__.py +0 -1
  466. package/references/acp-factory-ref/python/tests/e2e/test_codex_e2e.py +0 -349
  467. package/references/acp-factory-ref/python/tests/e2e/test_gemini_e2e.py +0 -165
  468. package/references/acp-factory-ref/python/tests/e2e/test_opencode_e2e.py +0 -296
  469. package/references/acp-factory-ref/python/tests/test_client_handler.py +0 -543
  470. package/references/acp-factory-ref/python/tests/test_pushable.py +0 -199
  471. package/references/claude-code-acp/.github/workflows/ci.yml +0 -45
  472. package/references/claude-code-acp/.github/workflows/publish.yml +0 -34
  473. package/references/claude-code-acp/.prettierrc.json +0 -4
  474. package/references/claude-code-acp/CHANGELOG.md +0 -249
  475. package/references/claude-code-acp/LICENSE +0 -222
  476. package/references/claude-code-acp/README.md +0 -53
  477. package/references/claude-code-acp/docs/RELEASES.md +0 -24
  478. package/references/claude-code-acp/eslint.config.js +0 -48
  479. package/references/claude-code-acp/package-lock.json +0 -4570
  480. package/references/claude-code-acp/package.json +0 -88
  481. package/references/claude-code-acp/scripts/release.sh +0 -119
  482. package/references/claude-code-acp/src/acp-agent.ts +0 -2065
  483. package/references/claude-code-acp/src/index.ts +0 -26
  484. package/references/claude-code-acp/src/lib.ts +0 -38
  485. package/references/claude-code-acp/src/mcp-server.ts +0 -911
  486. package/references/claude-code-acp/src/settings.ts +0 -522
  487. package/references/claude-code-acp/src/tests/.claude/commands/quick-math.md +0 -5
  488. package/references/claude-code-acp/src/tests/.claude/commands/say-hello.md +0 -6
  489. package/references/claude-code-acp/src/tests/acp-agent-fork.test.ts +0 -479
  490. package/references/claude-code-acp/src/tests/acp-agent.test.ts +0 -1502
  491. package/references/claude-code-acp/src/tests/extract-lines.test.ts +0 -103
  492. package/references/claude-code-acp/src/tests/fork-session.test.ts +0 -335
  493. package/references/claude-code-acp/src/tests/replace-and-calculate-location.test.ts +0 -334
  494. package/references/claude-code-acp/src/tests/settings.test.ts +0 -617
  495. package/references/claude-code-acp/src/tests/skills-options.test.ts +0 -187
  496. package/references/claude-code-acp/src/tests/tools.test.ts +0 -318
  497. package/references/claude-code-acp/src/tests/typescript-declarations.test.ts +0 -558
  498. package/references/claude-code-acp/src/tools.ts +0 -819
  499. package/references/claude-code-acp/src/utils.ts +0 -171
  500. package/references/claude-code-acp/tsconfig.json +0 -18
  501. package/references/claude-code-acp/vitest.config.ts +0 -19
  502. package/references/multi-agent-protocol/.sudocode/issues.jsonl +0 -82
  503. package/references/multi-agent-protocol/.sudocode/specs.jsonl +0 -9
  504. package/references/multi-agent-protocol/LICENSE +0 -21
  505. package/references/multi-agent-protocol/README.md +0 -113
  506. package/references/multi-agent-protocol/docs/00-design-specification.md +0 -460
  507. package/references/multi-agent-protocol/docs/01-open-questions.md +0 -1050
  508. package/references/multi-agent-protocol/docs/02-wire-protocol.md +0 -296
  509. package/references/multi-agent-protocol/docs/03-streaming-semantics.md +0 -252
  510. package/references/multi-agent-protocol/docs/04-error-handling.md +0 -231
  511. package/references/multi-agent-protocol/docs/05-connection-model.md +0 -244
  512. package/references/multi-agent-protocol/docs/06-visibility-permissions.md +0 -243
  513. package/references/multi-agent-protocol/docs/07-federation.md +0 -259
  514. package/references/multi-agent-protocol/docs/08-macro-agent-migration.md +0 -253
  515. package/references/multi-agent-protocol/package-lock.json +0 -3239
  516. package/references/multi-agent-protocol/package.json +0 -56
  517. package/references/multi-agent-protocol/schema/meta.json +0 -337
  518. package/references/multi-agent-protocol/schema/schema.json +0 -1828
@@ -0,0 +1,768 @@
1
+ /**
2
+ * Agent Detector Tests
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
6
+ import { execFile } from "node:child_process";
7
+
8
+ import { AgentDetector, createAgentDetector, parseVersion } from "../detector.js";
9
+ import { AgentDetectionError } from "../types.js";
10
+
11
+ // Mock child_process.execFile
12
+ vi.mock("node:child_process", () => ({
13
+ execFile: vi.fn(),
14
+ }));
15
+
16
+ // Mock node:util to return our mock
17
+ vi.mock("node:util", () => ({
18
+ promisify: (fn: unknown) => {
19
+ // Return a function that calls the mock and wraps in a promise
20
+ return (...args: unknown[]) => {
21
+ return new Promise((resolve, reject) => {
22
+ (fn as Function)(...args, (err: Error | null, result: unknown) => {
23
+ if (err) reject(err);
24
+ else resolve(result);
25
+ });
26
+ });
27
+ };
28
+ },
29
+ }));
30
+
31
+ const mockExecFile = vi.mocked(execFile);
32
+
33
+ function setupExecFileMock(responses: Record<string, { stdout?: string; stderr?: string; error?: Error }>) {
34
+ mockExecFile.mockImplementation(((
35
+ command: string,
36
+ args: string[],
37
+ _options: unknown,
38
+ callback: (err: Error | null, result: { stdout: string; stderr: string }) => void
39
+ ) => {
40
+ // Build a key from the command
41
+ let key: string;
42
+ if (command === "which") {
43
+ key = `which:${args[0]}`;
44
+ } else if (command === "/bin/sh") {
45
+ key = `command-v:${args[1]?.replace("command -v ", "")}`;
46
+ } else {
47
+ key = `version:${command}`;
48
+ }
49
+
50
+ const response = responses[key];
51
+ if (response?.error) {
52
+ callback(response.error, { stdout: "", stderr: "" });
53
+ } else if (response) {
54
+ callback(null, { stdout: response.stdout ?? "", stderr: response.stderr ?? "" });
55
+ } else {
56
+ callback(new Error(`Command not found: ${key}`), { stdout: "", stderr: "" });
57
+ }
58
+ }) as unknown as typeof execFile);
59
+ }
60
+
61
+ describe("AgentDetector", () => {
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ });
65
+
66
+ afterEach(() => {
67
+ vi.restoreAllMocks();
68
+ });
69
+
70
+ // ===========================================================================
71
+ // parseVersion()
72
+ // ===========================================================================
73
+
74
+ describe("parseVersion()", () => {
75
+ it("parses standard semver version", () => {
76
+ expect(parseVersion("1.2.3")).toBe("1.2.3");
77
+ });
78
+
79
+ it("parses version from verbose output", () => {
80
+ expect(parseVersion("claude v1.0.30 (build abc123)")).toBe("1.0.30");
81
+ });
82
+
83
+ it("parses version with only major.minor", () => {
84
+ expect(parseVersion("version 0.82")).toBe("0.82");
85
+ });
86
+
87
+ it("parses version with four parts", () => {
88
+ expect(parseVersion("aider 0.82.1.2")).toBe("0.82.1.2");
89
+ });
90
+
91
+ it("parses version from multiline output", () => {
92
+ expect(parseVersion("Tool name\nVersion: 2.3.4\nBuild: xyz")).toBe("2.3.4");
93
+ });
94
+
95
+ it("returns null for output with no version", () => {
96
+ expect(parseVersion("no version here")).toBeNull();
97
+ });
98
+
99
+ it("returns null for empty output", () => {
100
+ expect(parseVersion("")).toBeNull();
101
+ });
102
+ });
103
+
104
+ // ===========================================================================
105
+ // Constructor & Configuration
106
+ // ===========================================================================
107
+
108
+ describe("constructor", () => {
109
+ it("creates detector with default configuration", () => {
110
+ const detector = new AgentDetector();
111
+ expect(detector.getRegistry().size).toBe(6);
112
+ });
113
+
114
+ it("creates detector with custom agents", () => {
115
+ const detector = new AgentDetector({
116
+ additionalAgents: [
117
+ {
118
+ id: "custom",
119
+ name: "Custom",
120
+ description: "Custom agent",
121
+ binary: "custom",
122
+ versionArgs: ["--version"],
123
+ headless: { promptFlag: "--prompt" },
124
+ vendor: "Custom",
125
+ },
126
+ ],
127
+ });
128
+ expect(detector.getRegistry().size).toBe(7);
129
+ });
130
+
131
+ it("creates detector with disabled agents", () => {
132
+ const detector = new AgentDetector({
133
+ disabledAgents: ["goose", "aider"],
134
+ });
135
+ // Registry still has all agents, but detection will skip disabled ones
136
+ expect(detector.getRegistry().size).toBe(6);
137
+ });
138
+ });
139
+
140
+ // ===========================================================================
141
+ // Detection
142
+ // ===========================================================================
143
+
144
+ describe("detect()", () => {
145
+ it("detects installed agents", async () => {
146
+ setupExecFileMock({
147
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
148
+ "version:claude": { stdout: "claude v1.0.30\n" },
149
+ "which:codex": { error: new Error("not found") },
150
+ "command-v:codex": { error: new Error("not found") },
151
+ "which:gemini": { error: new Error("not found") },
152
+ "command-v:gemini": { error: new Error("not found") },
153
+ "which:opencode": { error: new Error("not found") },
154
+ "command-v:opencode": { error: new Error("not found") },
155
+ "which:aider": { stdout: "/home/user/.local/bin/aider\n" },
156
+ "version:aider": { stdout: "aider v0.82.1\n" },
157
+ "which:goose": { error: new Error("not found") },
158
+ "command-v:goose": { error: new Error("not found") },
159
+ });
160
+
161
+ const detector = createAgentDetector();
162
+ const result = await detector.detect();
163
+
164
+ expect(result.scanned).toBe(6);
165
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
166
+
167
+ const claude = result.agents.find((a) => a.id === "claude-code");
168
+ expect(claude).toBeDefined();
169
+ expect(claude!.installed).toBe(true);
170
+ expect(claude!.version).toBe("1.0.30");
171
+ expect(claude!.path).toBe("/usr/local/bin/claude");
172
+
173
+ const aider = result.agents.find((a) => a.id === "aider");
174
+ expect(aider).toBeDefined();
175
+ expect(aider!.installed).toBe(true);
176
+ expect(aider!.version).toBe("0.82.1");
177
+
178
+ const codex = result.agents.find((a) => a.id === "codex");
179
+ expect(codex).toBeDefined();
180
+ expect(codex!.installed).toBe(false);
181
+ });
182
+
183
+ it("handles version output on stderr", async () => {
184
+ setupExecFileMock({
185
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
186
+ "version:claude": { stdout: "", stderr: "claude version 2.0.0\n" },
187
+ "which:codex": { error: new Error("not found") },
188
+ "command-v:codex": { error: new Error("not found") },
189
+ "which:gemini": { error: new Error("not found") },
190
+ "command-v:gemini": { error: new Error("not found") },
191
+ "which:opencode": { error: new Error("not found") },
192
+ "command-v:opencode": { error: new Error("not found") },
193
+ "which:aider": { error: new Error("not found") },
194
+ "command-v:aider": { error: new Error("not found") },
195
+ "which:goose": { error: new Error("not found") },
196
+ "command-v:goose": { error: new Error("not found") },
197
+ });
198
+
199
+ const detector = createAgentDetector();
200
+ const result = await detector.detect();
201
+
202
+ const claude = result.agents.find((a) => a.id === "claude-code");
203
+ expect(claude!.installed).toBe(true);
204
+ expect(claude!.version).toBe("2.0.0");
205
+ });
206
+
207
+ it("marks agent as installed even if version check fails", async () => {
208
+ setupExecFileMock({
209
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
210
+ "version:claude": { error: new Error("version failed") },
211
+ "which:codex": { error: new Error("not found") },
212
+ "command-v:codex": { error: new Error("not found") },
213
+ "which:gemini": { error: new Error("not found") },
214
+ "command-v:gemini": { error: new Error("not found") },
215
+ "which:opencode": { error: new Error("not found") },
216
+ "command-v:opencode": { error: new Error("not found") },
217
+ "which:aider": { error: new Error("not found") },
218
+ "command-v:aider": { error: new Error("not found") },
219
+ "which:goose": { error: new Error("not found") },
220
+ "command-v:goose": { error: new Error("not found") },
221
+ });
222
+
223
+ const detector = createAgentDetector();
224
+ const result = await detector.detect();
225
+
226
+ const claude = result.agents.find((a) => a.id === "claude-code");
227
+ expect(claude!.installed).toBe(true);
228
+ expect(claude!.version).toBeUndefined();
229
+ });
230
+
231
+ it("skips disabled agents", async () => {
232
+ setupExecFileMock({
233
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
234
+ "version:claude": { stdout: "1.0.0\n" },
235
+ "which:codex": { error: new Error("not found") },
236
+ "command-v:codex": { error: new Error("not found") },
237
+ "which:gemini": { error: new Error("not found") },
238
+ "command-v:gemini": { error: new Error("not found") },
239
+ "which:opencode": { error: new Error("not found") },
240
+ "command-v:opencode": { error: new Error("not found") },
241
+ });
242
+
243
+ const detector = createAgentDetector({
244
+ disabledAgents: ["aider", "goose"],
245
+ });
246
+ const result = await detector.detect();
247
+
248
+ expect(result.scanned).toBe(4);
249
+ expect(result.agents.find((a) => a.id === "aider")).toBeUndefined();
250
+ expect(result.agents.find((a) => a.id === "goose")).toBeUndefined();
251
+ });
252
+
253
+ it("falls back to command -v when which fails", async () => {
254
+ setupExecFileMock({
255
+ "which:claude": { error: new Error("which not found") },
256
+ "command-v:claude": { stdout: "/usr/local/bin/claude\n" },
257
+ "version:claude": { stdout: "1.0.0\n" },
258
+ "which:codex": { error: new Error("not found") },
259
+ "command-v:codex": { error: new Error("not found") },
260
+ "which:gemini": { error: new Error("not found") },
261
+ "command-v:gemini": { error: new Error("not found") },
262
+ "which:opencode": { error: new Error("not found") },
263
+ "command-v:opencode": { error: new Error("not found") },
264
+ "which:aider": { error: new Error("not found") },
265
+ "command-v:aider": { error: new Error("not found") },
266
+ "which:goose": { error: new Error("not found") },
267
+ "command-v:goose": { error: new Error("not found") },
268
+ });
269
+
270
+ const detector = createAgentDetector();
271
+ const result = await detector.detect();
272
+
273
+ const claude = result.agents.find((a) => a.id === "claude-code");
274
+ expect(claude!.installed).toBe(true);
275
+ expect(claude!.path).toBe("/usr/local/bin/claude");
276
+ });
277
+ });
278
+
279
+ // ===========================================================================
280
+ // Caching
281
+ // ===========================================================================
282
+
283
+ describe("caching", () => {
284
+ it("returns cached result on subsequent calls", async () => {
285
+ setupExecFileMock({
286
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
287
+ "version:claude": { stdout: "1.0.0\n" },
288
+ "which:codex": { error: new Error("not found") },
289
+ "command-v:codex": { error: new Error("not found") },
290
+ "which:gemini": { error: new Error("not found") },
291
+ "command-v:gemini": { error: new Error("not found") },
292
+ "which:opencode": { error: new Error("not found") },
293
+ "command-v:opencode": { error: new Error("not found") },
294
+ "which:aider": { error: new Error("not found") },
295
+ "command-v:aider": { error: new Error("not found") },
296
+ "which:goose": { error: new Error("not found") },
297
+ "command-v:goose": { error: new Error("not found") },
298
+ });
299
+
300
+ const detector = createAgentDetector();
301
+ const result1 = await detector.detect();
302
+ const callCount = mockExecFile.mock.calls.length;
303
+
304
+ const result2 = await detector.detect();
305
+ // No additional execFile calls — result was cached
306
+ expect(mockExecFile.mock.calls.length).toBe(callCount);
307
+ expect(result2).toEqual(result1);
308
+ });
309
+
310
+ it("bypasses cache when refresh is true", async () => {
311
+ setupExecFileMock({
312
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
313
+ "version:claude": { stdout: "1.0.0\n" },
314
+ "which:codex": { error: new Error("not found") },
315
+ "command-v:codex": { error: new Error("not found") },
316
+ "which:gemini": { error: new Error("not found") },
317
+ "command-v:gemini": { error: new Error("not found") },
318
+ "which:opencode": { error: new Error("not found") },
319
+ "command-v:opencode": { error: new Error("not found") },
320
+ "which:aider": { error: new Error("not found") },
321
+ "command-v:aider": { error: new Error("not found") },
322
+ "which:goose": { error: new Error("not found") },
323
+ "command-v:goose": { error: new Error("not found") },
324
+ });
325
+
326
+ const detector = createAgentDetector();
327
+ await detector.detect();
328
+ const callCountAfterFirst = mockExecFile.mock.calls.length;
329
+
330
+ await detector.detect({ refresh: true });
331
+ expect(mockExecFile.mock.calls.length).toBeGreaterThan(callCountAfterFirst);
332
+ });
333
+
334
+ it("invalidates cache manually", async () => {
335
+ setupExecFileMock({
336
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
337
+ "version:claude": { stdout: "1.0.0\n" },
338
+ "which:codex": { error: new Error("not found") },
339
+ "command-v:codex": { error: new Error("not found") },
340
+ "which:gemini": { error: new Error("not found") },
341
+ "command-v:gemini": { error: new Error("not found") },
342
+ "which:opencode": { error: new Error("not found") },
343
+ "command-v:opencode": { error: new Error("not found") },
344
+ "which:aider": { error: new Error("not found") },
345
+ "command-v:aider": { error: new Error("not found") },
346
+ "which:goose": { error: new Error("not found") },
347
+ "command-v:goose": { error: new Error("not found") },
348
+ });
349
+
350
+ const detector = createAgentDetector();
351
+ await detector.detect();
352
+ expect(detector.getCachedResult()).not.toBeNull();
353
+
354
+ detector.invalidateCache();
355
+ expect(detector.getCachedResult()).toBeNull();
356
+ });
357
+
358
+ it("expires cache after TTL", async () => {
359
+ setupExecFileMock({
360
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
361
+ "version:claude": { stdout: "1.0.0\n" },
362
+ "which:codex": { error: new Error("not found") },
363
+ "command-v:codex": { error: new Error("not found") },
364
+ "which:gemini": { error: new Error("not found") },
365
+ "command-v:gemini": { error: new Error("not found") },
366
+ "which:opencode": { error: new Error("not found") },
367
+ "command-v:opencode": { error: new Error("not found") },
368
+ "which:aider": { error: new Error("not found") },
369
+ "command-v:aider": { error: new Error("not found") },
370
+ "which:goose": { error: new Error("not found") },
371
+ "command-v:goose": { error: new Error("not found") },
372
+ });
373
+
374
+ const detector = createAgentDetector({ cacheTtlMs: 50 });
375
+ await detector.detect();
376
+ const callCountAfterFirst = mockExecFile.mock.calls.length;
377
+
378
+ // Wait for cache to expire
379
+ await new Promise((resolve) => setTimeout(resolve, 60));
380
+
381
+ await detector.detect();
382
+ expect(mockExecFile.mock.calls.length).toBeGreaterThan(callCountAfterFirst);
383
+ });
384
+ });
385
+
386
+ // ===========================================================================
387
+ // getAvailableAgents()
388
+ // ===========================================================================
389
+
390
+ describe("getAvailableAgents()", () => {
391
+ it("returns only installed agents by default", async () => {
392
+ setupExecFileMock({
393
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
394
+ "version:claude": { stdout: "1.0.0\n" },
395
+ "which:codex": { error: new Error("not found") },
396
+ "command-v:codex": { error: new Error("not found") },
397
+ "which:gemini": { error: new Error("not found") },
398
+ "command-v:gemini": { error: new Error("not found") },
399
+ "which:opencode": { error: new Error("not found") },
400
+ "command-v:opencode": { error: new Error("not found") },
401
+ "which:aider": { error: new Error("not found") },
402
+ "command-v:aider": { error: new Error("not found") },
403
+ "which:goose": { error: new Error("not found") },
404
+ "command-v:goose": { error: new Error("not found") },
405
+ });
406
+
407
+ const detector = createAgentDetector();
408
+ const result = await detector.getAvailableAgents();
409
+
410
+ expect(result.agents).toHaveLength(1);
411
+ expect(result.agents[0].id).toBe("claude-code");
412
+ });
413
+
414
+ it("includes not-installed agents when requested", async () => {
415
+ setupExecFileMock({
416
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
417
+ "version:claude": { stdout: "1.0.0\n" },
418
+ "which:codex": { error: new Error("not found") },
419
+ "command-v:codex": { error: new Error("not found") },
420
+ "which:gemini": { error: new Error("not found") },
421
+ "command-v:gemini": { error: new Error("not found") },
422
+ "which:opencode": { error: new Error("not found") },
423
+ "command-v:opencode": { error: new Error("not found") },
424
+ "which:aider": { error: new Error("not found") },
425
+ "command-v:aider": { error: new Error("not found") },
426
+ "which:goose": { error: new Error("not found") },
427
+ "command-v:goose": { error: new Error("not found") },
428
+ });
429
+
430
+ const detector = createAgentDetector();
431
+ const result = await detector.getAvailableAgents({ includeNotInstalled: true });
432
+
433
+ expect(result.agents).toHaveLength(6);
434
+ });
435
+ });
436
+
437
+ // ===========================================================================
438
+ // getDefinition()
439
+ // ===========================================================================
440
+
441
+ describe("getDefinition()", () => {
442
+ it("returns definition for known agent", () => {
443
+ const detector = createAgentDetector();
444
+ const def = detector.getDefinition("claude-code");
445
+ expect(def.id).toBe("claude-code");
446
+ expect(def.binary).toBe("claude");
447
+ });
448
+
449
+ it("throws AgentDetectionError with UNKNOWN_AGENT code for unknown agent", () => {
450
+ const detector = createAgentDetector();
451
+ try {
452
+ detector.getDefinition("foo-agent");
453
+ expect.fail("Expected AgentDetectionError to be thrown");
454
+ } catch (err) {
455
+ expect(err).toBeInstanceOf(AgentDetectionError);
456
+ const error = err as AgentDetectionError;
457
+ expect(error.code).toBe("UNKNOWN_AGENT");
458
+ expect(error.agentId).toBe("foo-agent");
459
+ expect(error.message).toBe("Unknown agent backend: foo-agent");
460
+ }
461
+ });
462
+ });
463
+
464
+ // ===========================================================================
465
+ // isInstalled()
466
+ // ===========================================================================
467
+
468
+ describe("isInstalled()", () => {
469
+ it("returns true for installed agent", async () => {
470
+ setupExecFileMock({
471
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
472
+ "version:claude": { stdout: "1.0.0\n" },
473
+ "which:codex": { error: new Error("not found") },
474
+ "command-v:codex": { error: new Error("not found") },
475
+ "which:gemini": { error: new Error("not found") },
476
+ "command-v:gemini": { error: new Error("not found") },
477
+ "which:opencode": { error: new Error("not found") },
478
+ "command-v:opencode": { error: new Error("not found") },
479
+ "which:aider": { error: new Error("not found") },
480
+ "command-v:aider": { error: new Error("not found") },
481
+ "which:goose": { error: new Error("not found") },
482
+ "command-v:goose": { error: new Error("not found") },
483
+ });
484
+
485
+ const detector = createAgentDetector();
486
+ expect(await detector.isInstalled("claude-code")).toBe(true);
487
+ });
488
+
489
+ it("returns false for non-installed agent", async () => {
490
+ setupExecFileMock({
491
+ "which:claude": { error: new Error("not found") },
492
+ "command-v:claude": { error: new Error("not found") },
493
+ "which:codex": { error: new Error("not found") },
494
+ "command-v:codex": { error: new Error("not found") },
495
+ "which:gemini": { error: new Error("not found") },
496
+ "command-v:gemini": { error: new Error("not found") },
497
+ "which:opencode": { error: new Error("not found") },
498
+ "command-v:opencode": { error: new Error("not found") },
499
+ "which:aider": { error: new Error("not found") },
500
+ "command-v:aider": { error: new Error("not found") },
501
+ "which:goose": { error: new Error("not found") },
502
+ "command-v:goose": { error: new Error("not found") },
503
+ });
504
+
505
+ const detector = createAgentDetector();
506
+ expect(await detector.isInstalled("codex")).toBe(false);
507
+ });
508
+ });
509
+
510
+ // ===========================================================================
511
+ // getAgent()
512
+ // ===========================================================================
513
+
514
+ describe("getAgent()", () => {
515
+ it("returns a specific detected agent by ID", async () => {
516
+ setupExecFileMock({
517
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
518
+ "version:claude": { stdout: "1.0.0\n" },
519
+ "which:codex": { error: new Error("not found") },
520
+ "command-v:codex": { error: new Error("not found") },
521
+ "which:gemini": { error: new Error("not found") },
522
+ "command-v:gemini": { error: new Error("not found") },
523
+ "which:opencode": { error: new Error("not found") },
524
+ "command-v:opencode": { error: new Error("not found") },
525
+ "which:aider": { error: new Error("not found") },
526
+ "command-v:aider": { error: new Error("not found") },
527
+ "which:goose": { error: new Error("not found") },
528
+ "command-v:goose": { error: new Error("not found") },
529
+ });
530
+
531
+ const detector = createAgentDetector();
532
+ const agent = await detector.getAgent("claude-code");
533
+ expect(agent).toBeDefined();
534
+ expect(agent!.id).toBe("claude-code");
535
+ expect(agent!.installed).toBe(true);
536
+ expect(agent!.version).toBe("1.0.0");
537
+ });
538
+
539
+ it("returns undefined for unknown agent ID", async () => {
540
+ setupExecFileMock({
541
+ "which:claude": { error: new Error("not found") },
542
+ "command-v:claude": { error: new Error("not found") },
543
+ "which:codex": { error: new Error("not found") },
544
+ "command-v:codex": { error: new Error("not found") },
545
+ "which:gemini": { error: new Error("not found") },
546
+ "command-v:gemini": { error: new Error("not found") },
547
+ "which:opencode": { error: new Error("not found") },
548
+ "command-v:opencode": { error: new Error("not found") },
549
+ "which:aider": { error: new Error("not found") },
550
+ "command-v:aider": { error: new Error("not found") },
551
+ "which:goose": { error: new Error("not found") },
552
+ "command-v:goose": { error: new Error("not found") },
553
+ });
554
+
555
+ const detector = createAgentDetector();
556
+ const agent = await detector.getAgent("nonexistent");
557
+ expect(agent).toBeUndefined();
558
+ });
559
+
560
+ it("returns not-installed agent entry by ID", async () => {
561
+ setupExecFileMock({
562
+ "which:claude": { error: new Error("not found") },
563
+ "command-v:claude": { error: new Error("not found") },
564
+ "which:codex": { error: new Error("not found") },
565
+ "command-v:codex": { error: new Error("not found") },
566
+ "which:gemini": { error: new Error("not found") },
567
+ "command-v:gemini": { error: new Error("not found") },
568
+ "which:opencode": { error: new Error("not found") },
569
+ "command-v:opencode": { error: new Error("not found") },
570
+ "which:aider": { error: new Error("not found") },
571
+ "command-v:aider": { error: new Error("not found") },
572
+ "which:goose": { error: new Error("not found") },
573
+ "command-v:goose": { error: new Error("not found") },
574
+ });
575
+
576
+ const detector = createAgentDetector();
577
+ const agent = await detector.getAgent("codex");
578
+ expect(agent).toBeDefined();
579
+ expect(agent!.id).toBe("codex");
580
+ expect(agent!.installed).toBe(false);
581
+ expect(agent!.version).toBeUndefined();
582
+ expect(agent!.path).toBeUndefined();
583
+ });
584
+ });
585
+
586
+ // ===========================================================================
587
+ // All agents not found (no crash)
588
+ // ===========================================================================
589
+
590
+ describe("detection with no agents installed", () => {
591
+ it("handles all agents not found without crashing", async () => {
592
+ setupExecFileMock({
593
+ "which:claude": { error: new Error("not found") },
594
+ "command-v:claude": { error: new Error("not found") },
595
+ "which:codex": { error: new Error("not found") },
596
+ "command-v:codex": { error: new Error("not found") },
597
+ "which:gemini": { error: new Error("not found") },
598
+ "command-v:gemini": { error: new Error("not found") },
599
+ "which:opencode": { error: new Error("not found") },
600
+ "command-v:opencode": { error: new Error("not found") },
601
+ "which:aider": { error: new Error("not found") },
602
+ "command-v:aider": { error: new Error("not found") },
603
+ "which:goose": { error: new Error("not found") },
604
+ "command-v:goose": { error: new Error("not found") },
605
+ });
606
+
607
+ const detector = createAgentDetector();
608
+ const result = await detector.detect();
609
+
610
+ expect(result.scanned).toBe(6);
611
+ expect(result.agents).toHaveLength(6);
612
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
613
+
614
+ // All agents should be marked as not installed
615
+ for (const agent of result.agents) {
616
+ expect(agent.installed).toBe(false);
617
+ expect(agent.version).toBeUndefined();
618
+ expect(agent.path).toBeUndefined();
619
+ expect(agent.definition).toBeDefined();
620
+ expect(agent.detectedAt).toBeGreaterThan(0);
621
+ }
622
+ });
623
+
624
+ it("getAvailableAgents returns empty list when nothing installed", async () => {
625
+ setupExecFileMock({
626
+ "which:claude": { error: new Error("not found") },
627
+ "command-v:claude": { error: new Error("not found") },
628
+ "which:codex": { error: new Error("not found") },
629
+ "command-v:codex": { error: new Error("not found") },
630
+ "which:gemini": { error: new Error("not found") },
631
+ "command-v:gemini": { error: new Error("not found") },
632
+ "which:opencode": { error: new Error("not found") },
633
+ "command-v:opencode": { error: new Error("not found") },
634
+ "which:aider": { error: new Error("not found") },
635
+ "command-v:aider": { error: new Error("not found") },
636
+ "which:goose": { error: new Error("not found") },
637
+ "command-v:goose": { error: new Error("not found") },
638
+ });
639
+
640
+ const detector = createAgentDetector();
641
+ const result = await detector.getAvailableAgents();
642
+ expect(result.agents).toHaveLength(0);
643
+ });
644
+ });
645
+
646
+ // ===========================================================================
647
+ // Concurrent detection deduplication
648
+ // ===========================================================================
649
+
650
+ describe("concurrent detection deduplication", () => {
651
+ it("deduplicates concurrent detect() calls into a single scan", async () => {
652
+ setupExecFileMock({
653
+ "which:claude": { stdout: "/usr/local/bin/claude\n" },
654
+ "version:claude": { stdout: "1.0.0\n" },
655
+ "which:codex": { error: new Error("not found") },
656
+ "command-v:codex": { error: new Error("not found") },
657
+ "which:gemini": { error: new Error("not found") },
658
+ "command-v:gemini": { error: new Error("not found") },
659
+ "which:opencode": { error: new Error("not found") },
660
+ "command-v:opencode": { error: new Error("not found") },
661
+ "which:aider": { error: new Error("not found") },
662
+ "command-v:aider": { error: new Error("not found") },
663
+ "which:goose": { error: new Error("not found") },
664
+ "command-v:goose": { error: new Error("not found") },
665
+ });
666
+
667
+ const detector = createAgentDetector();
668
+
669
+ // Fire two detect() calls concurrently (no await)
670
+ const promise1 = detector.detect({ refresh: true });
671
+ const promise2 = detector.detect({ refresh: true });
672
+
673
+ const [result1, result2] = await Promise.all([promise1, promise2]);
674
+
675
+ // Both should return the same result object
676
+ expect(result1).toEqual(result2);
677
+
678
+ // execFile should only be called once per agent (not doubled),
679
+ // meaning only one scan ran. Each agent needs 1-2 calls (which + maybe version).
680
+ // With 6 agents, a single scan calls which for each (6 calls) plus
681
+ // version for installed ones. Two scans would roughly double the call count.
682
+ // We check that the total is consistent with a single scan.
683
+ const totalCalls = mockExecFile.mock.calls.length;
684
+ // A single scan: 6 which calls + some fallback/version calls.
685
+ // Should be well under 20 for a single scan (two scans would be ~24+).
686
+ expect(totalCalls).toBeLessThanOrEqual(14);
687
+ });
688
+
689
+ it("isDetecting() returns true during active detection", async () => {
690
+ let resolveWhich: ((value: unknown) => void) | undefined;
691
+ const whichPromise = new Promise((resolve) => {
692
+ resolveWhich = resolve;
693
+ });
694
+
695
+ // Make the which call hang so we can check isDetecting()
696
+ mockExecFile.mockImplementation(((
697
+ _command: string,
698
+ _args: string[],
699
+ _options: unknown,
700
+ callback: (err: Error | null, result: { stdout: string; stderr: string }) => void
701
+ ) => {
702
+ whichPromise.then(() => {
703
+ callback(new Error("not found"), { stdout: "", stderr: "" });
704
+ });
705
+ }) as unknown as typeof execFile);
706
+
707
+ const detector = createAgentDetector();
708
+ const detectPromise = detector.detect();
709
+
710
+ // While the mock is still pending, isDetecting should be true
711
+ expect(detector.isDetecting()).toBe(true);
712
+
713
+ // Resolve the hanging mock
714
+ resolveWhich!(undefined);
715
+ await detectPromise;
716
+
717
+ expect(detector.isDetecting()).toBe(false);
718
+ });
719
+ });
720
+
721
+ // ===========================================================================
722
+ // AgentDetectionError
723
+ // ===========================================================================
724
+
725
+ describe("AgentDetectionError", () => {
726
+ it("has correct name property", () => {
727
+ const error = new AgentDetectionError("test", "UNKNOWN_AGENT", "foo");
728
+ expect(error.name).toBe("AgentDetectionError");
729
+ });
730
+
731
+ it("has correct code property", () => {
732
+ const error = new AgentDetectionError("test", "AGENT_NOT_INSTALLED", "bar");
733
+ expect(error.code).toBe("AGENT_NOT_INSTALLED");
734
+ });
735
+
736
+ it("has correct agentId property", () => {
737
+ const error = new AgentDetectionError("test", "UNKNOWN_AGENT", "my-agent");
738
+ expect(error.agentId).toBe("my-agent");
739
+ });
740
+
741
+ it("has correct message", () => {
742
+ const error = new AgentDetectionError("Something went wrong", "DETECTION_FAILED");
743
+ expect(error.message).toBe("Something went wrong");
744
+ });
745
+
746
+ it("is an instance of Error", () => {
747
+ const error = new AgentDetectionError("test", "UNKNOWN_AGENT");
748
+ expect(error).toBeInstanceOf(Error);
749
+ });
750
+
751
+ it("works without agentId", () => {
752
+ const error = new AgentDetectionError("test", "DETECTION_TIMEOUT");
753
+ expect(error.agentId).toBeUndefined();
754
+ expect(error.code).toBe("DETECTION_TIMEOUT");
755
+ });
756
+ });
757
+
758
+ // ===========================================================================
759
+ // isDetecting()
760
+ // ===========================================================================
761
+
762
+ describe("isDetecting()", () => {
763
+ it("returns false when not detecting", () => {
764
+ const detector = createAgentDetector();
765
+ expect(detector.isDetecting()).toBe(false);
766
+ });
767
+ });
768
+ });