@ag-eco/agentplate-cli 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (455) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +462 -0
  3. package/agents/ap-co-creation.md +90 -0
  4. package/agents/builder.md +144 -0
  5. package/agents/coordinator.md +377 -0
  6. package/agents/lead.md +435 -0
  7. package/agents/merger.md +164 -0
  8. package/agents/monitor.md +214 -0
  9. package/agents/orchestrator.md +239 -0
  10. package/agents/reviewer.md +140 -0
  11. package/agents/scout.md +125 -0
  12. package/agents/supervisor.md +427 -0
  13. package/package.json +66 -0
  14. package/src/agents/capabilities.test.ts +85 -0
  15. package/src/agents/capabilities.ts +125 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +101 -0
  18. package/src/agents/copilot-hooks-deployer.test.ts +162 -0
  19. package/src/agents/copilot-hooks-deployer.ts +93 -0
  20. package/src/agents/guard-rules.test.ts +372 -0
  21. package/src/agents/guard-rules.ts +97 -0
  22. package/src/agents/headless-mail-injector.test.ts +709 -0
  23. package/src/agents/headless-mail-injector.ts +377 -0
  24. package/src/agents/headless-prompt.test.ts +102 -0
  25. package/src/agents/headless-prompt.ts +68 -0
  26. package/src/agents/hooks-deployer.test.ts +3119 -0
  27. package/src/agents/hooks-deployer.ts +804 -0
  28. package/src/agents/identity.test.ts +604 -0
  29. package/src/agents/identity.ts +384 -0
  30. package/src/agents/lifecycle.test.ts +196 -0
  31. package/src/agents/lifecycle.ts +183 -0
  32. package/src/agents/mail-poll-detect.test.ts +153 -0
  33. package/src/agents/mail-poll-detect.ts +73 -0
  34. package/src/agents/manifest.test.ts +1026 -0
  35. package/src/agents/manifest.ts +376 -0
  36. package/src/agents/overlay.test.ts +1058 -0
  37. package/src/agents/overlay.ts +490 -0
  38. package/src/agents/scope-detect.test.ts +190 -0
  39. package/src/agents/scope-detect.ts +146 -0
  40. package/src/agents/turn-lock.test.ts +181 -0
  41. package/src/agents/turn-lock.ts +235 -0
  42. package/src/agents/turn-runner-dispatch.test.ts +182 -0
  43. package/src/agents/turn-runner-dispatch.ts +105 -0
  44. package/src/agents/turn-runner.test.ts +2312 -0
  45. package/src/agents/turn-runner.ts +1383 -0
  46. package/src/beads/client.test.ts +217 -0
  47. package/src/beads/client.ts +230 -0
  48. package/src/beads/molecules.test.ts +338 -0
  49. package/src/beads/molecules.ts +198 -0
  50. package/src/commands/agents.test.ts +328 -0
  51. package/src/commands/agents.ts +299 -0
  52. package/src/commands/clean.test.ts +797 -0
  53. package/src/commands/clean.ts +791 -0
  54. package/src/commands/completions.test.ts +348 -0
  55. package/src/commands/completions.ts +981 -0
  56. package/src/commands/coordinator.test.ts +2975 -0
  57. package/src/commands/coordinator.ts +1841 -0
  58. package/src/commands/costs.test.ts +1183 -0
  59. package/src/commands/costs.ts +599 -0
  60. package/src/commands/dashboard.test.ts +954 -0
  61. package/src/commands/dashboard.ts +1212 -0
  62. package/src/commands/discover.test.ts +288 -0
  63. package/src/commands/discover.ts +202 -0
  64. package/src/commands/doctor.test.ts +303 -0
  65. package/src/commands/doctor.ts +311 -0
  66. package/src/commands/ecosystem.test.ts +226 -0
  67. package/src/commands/ecosystem.ts +248 -0
  68. package/src/commands/errors.test.ts +654 -0
  69. package/src/commands/errors.ts +197 -0
  70. package/src/commands/feed.test.ts +709 -0
  71. package/src/commands/feed.ts +260 -0
  72. package/src/commands/group.test.ts +475 -0
  73. package/src/commands/group.ts +546 -0
  74. package/src/commands/hooks.test.ts +458 -0
  75. package/src/commands/hooks.ts +263 -0
  76. package/src/commands/init.test.ts +1011 -0
  77. package/src/commands/init.ts +967 -0
  78. package/src/commands/inspect.test.ts +1239 -0
  79. package/src/commands/inspect.ts +648 -0
  80. package/src/commands/log.test.ts +1913 -0
  81. package/src/commands/log.ts +958 -0
  82. package/src/commands/logs.test.ts +801 -0
  83. package/src/commands/logs.ts +483 -0
  84. package/src/commands/mail.test.ts +1501 -0
  85. package/src/commands/mail.ts +848 -0
  86. package/src/commands/merge.test.ts +864 -0
  87. package/src/commands/merge.ts +381 -0
  88. package/src/commands/metrics.test.ts +458 -0
  89. package/src/commands/metrics.ts +129 -0
  90. package/src/commands/monitor.test.ts +191 -0
  91. package/src/commands/monitor.ts +409 -0
  92. package/src/commands/nudge.test.ts +579 -0
  93. package/src/commands/nudge.ts +646 -0
  94. package/src/commands/orchestrator.ts +42 -0
  95. package/src/commands/prime.test.ts +612 -0
  96. package/src/commands/prime.ts +359 -0
  97. package/src/commands/replay.test.ts +757 -0
  98. package/src/commands/replay.ts +231 -0
  99. package/src/commands/run.test.ts +469 -0
  100. package/src/commands/run.ts +353 -0
  101. package/src/commands/serve/agent-actions.test.ts +210 -0
  102. package/src/commands/serve/agent-actions.ts +192 -0
  103. package/src/commands/serve/build.test.ts +202 -0
  104. package/src/commands/serve/build.ts +206 -0
  105. package/src/commands/serve/coordinator-actions.test.ts +339 -0
  106. package/src/commands/serve/coordinator-actions.ts +410 -0
  107. package/src/commands/serve/dev.test.ts +168 -0
  108. package/src/commands/serve/dev.ts +117 -0
  109. package/src/commands/serve/mail-actions.test.ts +312 -0
  110. package/src/commands/serve/mail-actions.ts +167 -0
  111. package/src/commands/serve/rest.test.ts +1680 -0
  112. package/src/commands/serve/rest.ts +1130 -0
  113. package/src/commands/serve/static.ts +51 -0
  114. package/src/commands/serve/ws.test.ts +361 -0
  115. package/src/commands/serve/ws.ts +332 -0
  116. package/src/commands/serve.test.ts +459 -0
  117. package/src/commands/serve.ts +654 -0
  118. package/src/commands/sling.test.ts +1583 -0
  119. package/src/commands/sling.ts +1351 -0
  120. package/src/commands/spec.test.ts +179 -0
  121. package/src/commands/spec.ts +105 -0
  122. package/src/commands/status.test.ts +614 -0
  123. package/src/commands/status.ts +403 -0
  124. package/src/commands/stop.test.ts +964 -0
  125. package/src/commands/stop.ts +319 -0
  126. package/src/commands/supervisor.test.ts +185 -0
  127. package/src/commands/supervisor.ts +537 -0
  128. package/src/commands/trace.test.ts +762 -0
  129. package/src/commands/trace.ts +205 -0
  130. package/src/commands/update.test.ts +466 -0
  131. package/src/commands/update.ts +263 -0
  132. package/src/commands/upgrade.test.ts +48 -0
  133. package/src/commands/upgrade.ts +240 -0
  134. package/src/commands/watch.test.ts +257 -0
  135. package/src/commands/watch.ts +308 -0
  136. package/src/commands/worktree.test.ts +1297 -0
  137. package/src/commands/worktree.ts +451 -0
  138. package/src/config.test.ts +1535 -0
  139. package/src/config.ts +1064 -0
  140. package/src/doctor/agents.test.ts +523 -0
  141. package/src/doctor/agents.ts +399 -0
  142. package/src/doctor/config-check.test.ts +191 -0
  143. package/src/doctor/config-check.ts +183 -0
  144. package/src/doctor/consistency.test.ts +807 -0
  145. package/src/doctor/consistency.ts +347 -0
  146. package/src/doctor/databases.test.ts +350 -0
  147. package/src/doctor/databases.ts +243 -0
  148. package/src/doctor/dependencies.test.ts +296 -0
  149. package/src/doctor/dependencies.ts +272 -0
  150. package/src/doctor/ecosystem.test.ts +308 -0
  151. package/src/doctor/ecosystem.ts +156 -0
  152. package/src/doctor/logs.test.ts +253 -0
  153. package/src/doctor/logs.ts +295 -0
  154. package/src/doctor/merge-queue.test.ts +315 -0
  155. package/src/doctor/merge-queue.ts +167 -0
  156. package/src/doctor/providers.test.ts +409 -0
  157. package/src/doctor/providers.ts +250 -0
  158. package/src/doctor/serve.test.ts +95 -0
  159. package/src/doctor/serve.ts +86 -0
  160. package/src/doctor/structure.test.ts +423 -0
  161. package/src/doctor/structure.ts +285 -0
  162. package/src/doctor/types.ts +43 -0
  163. package/src/doctor/version.test.ts +241 -0
  164. package/src/doctor/version.ts +132 -0
  165. package/src/doctor/watchdog.test.ts +167 -0
  166. package/src/doctor/watchdog.ts +214 -0
  167. package/src/e2e/init-sling-lifecycle.test.ts +283 -0
  168. package/src/errors.test.ts +350 -0
  169. package/src/errors.ts +217 -0
  170. package/src/events/store.test.ts +660 -0
  171. package/src/events/store.ts +369 -0
  172. package/src/events/tailer.test.ts +719 -0
  173. package/src/events/tailer.ts +332 -0
  174. package/src/events/tool-filter.test.ts +330 -0
  175. package/src/events/tool-filter.ts +126 -0
  176. package/src/index.ts +533 -0
  177. package/src/insights/analyzer.test.ts +466 -0
  178. package/src/insights/analyzer.ts +203 -0
  179. package/src/insights/quality-gates.test.ts +141 -0
  180. package/src/insights/quality-gates.ts +156 -0
  181. package/src/json.test.ts +72 -0
  182. package/src/json.ts +53 -0
  183. package/src/loam/client.test.ts +752 -0
  184. package/src/loam/client.ts +664 -0
  185. package/src/logging/color.test.ts +252 -0
  186. package/src/logging/color.ts +105 -0
  187. package/src/logging/format.test.ts +110 -0
  188. package/src/logging/format.ts +255 -0
  189. package/src/logging/logger.test.ts +814 -0
  190. package/src/logging/logger.ts +266 -0
  191. package/src/logging/reporter.test.ts +259 -0
  192. package/src/logging/reporter.ts +110 -0
  193. package/src/logging/sanitizer.test.ts +190 -0
  194. package/src/logging/sanitizer.ts +57 -0
  195. package/src/logging/theme.ts +140 -0
  196. package/src/mail/broadcast.test.ts +204 -0
  197. package/src/mail/broadcast.ts +92 -0
  198. package/src/mail/client.test.ts +774 -0
  199. package/src/mail/client.ts +236 -0
  200. package/src/mail/store.test.ts +898 -0
  201. package/src/mail/store.ts +425 -0
  202. package/src/merge/lock.test.ts +149 -0
  203. package/src/merge/lock.ts +140 -0
  204. package/src/merge/predict.test.ts +387 -0
  205. package/src/merge/predict.ts +249 -0
  206. package/src/merge/queue.test.ts +426 -0
  207. package/src/merge/queue.ts +246 -0
  208. package/src/merge/resolver.test.ts +1993 -0
  209. package/src/merge/resolver.ts +926 -0
  210. package/src/metrics/pricing.test.ts +258 -0
  211. package/src/metrics/pricing.ts +135 -0
  212. package/src/metrics/store.test.ts +978 -0
  213. package/src/metrics/store.ts +501 -0
  214. package/src/metrics/summary.test.ts +398 -0
  215. package/src/metrics/summary.ts +178 -0
  216. package/src/metrics/transcript.test.ts +483 -0
  217. package/src/metrics/transcript.ts +114 -0
  218. package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
  219. package/src/runtimes/aider.test.ts +124 -0
  220. package/src/runtimes/aider.ts +147 -0
  221. package/src/runtimes/amp.test.ts +164 -0
  222. package/src/runtimes/amp.ts +154 -0
  223. package/src/runtimes/claude.test.ts +1474 -0
  224. package/src/runtimes/claude.ts +579 -0
  225. package/src/runtimes/codex.test.ts +805 -0
  226. package/src/runtimes/codex.ts +273 -0
  227. package/src/runtimes/connections.test.ts +214 -0
  228. package/src/runtimes/connections.ts +103 -0
  229. package/src/runtimes/copilot.test.ts +707 -0
  230. package/src/runtimes/copilot.ts +316 -0
  231. package/src/runtimes/cursor.test.ts +497 -0
  232. package/src/runtimes/cursor.ts +205 -0
  233. package/src/runtimes/gemini.test.ts +537 -0
  234. package/src/runtimes/gemini.ts +243 -0
  235. package/src/runtimes/goose.test.ts +133 -0
  236. package/src/runtimes/goose.ts +157 -0
  237. package/src/runtimes/headless-connection.test.ts +264 -0
  238. package/src/runtimes/headless-connection.ts +158 -0
  239. package/src/runtimes/opencode.test.ts +325 -0
  240. package/src/runtimes/opencode.ts +188 -0
  241. package/src/runtimes/pi-guards.test.ts +486 -0
  242. package/src/runtimes/pi-guards.ts +367 -0
  243. package/src/runtimes/pi.test.ts +789 -0
  244. package/src/runtimes/pi.ts +305 -0
  245. package/src/runtimes/registry.test.ts +196 -0
  246. package/src/runtimes/registry.ts +99 -0
  247. package/src/runtimes/sapling.test.ts +1267 -0
  248. package/src/runtimes/sapling.ts +710 -0
  249. package/src/runtimes/types.ts +266 -0
  250. package/src/schema-consistency.test.ts +246 -0
  251. package/src/sessions/compat.test.ts +281 -0
  252. package/src/sessions/compat.ts +105 -0
  253. package/src/sessions/store.test.ts +1748 -0
  254. package/src/sessions/store.ts +858 -0
  255. package/src/test-helpers.test.ts +124 -0
  256. package/src/test-helpers.ts +145 -0
  257. package/src/test-setup.test.ts +31 -0
  258. package/src/test-setup.ts +28 -0
  259. package/src/tools/loam/api.ts +368 -0
  260. package/src/tools/loam/cli.ts +278 -0
  261. package/src/tools/loam/commands/add.ts +52 -0
  262. package/src/tools/loam/commands/archive.ts +214 -0
  263. package/src/tools/loam/commands/audit.ts +276 -0
  264. package/src/tools/loam/commands/compact.ts +1062 -0
  265. package/src/tools/loam/commands/completions.ts +79 -0
  266. package/src/tools/loam/commands/config.ts +381 -0
  267. package/src/tools/loam/commands/delete-domain.ts +121 -0
  268. package/src/tools/loam/commands/delete.ts +316 -0
  269. package/src/tools/loam/commands/diff.ts +200 -0
  270. package/src/tools/loam/commands/doctor.ts +1113 -0
  271. package/src/tools/loam/commands/edit.ts +226 -0
  272. package/src/tools/loam/commands/init.ts +31 -0
  273. package/src/tools/loam/commands/learn.ts +179 -0
  274. package/src/tools/loam/commands/move.ts +323 -0
  275. package/src/tools/loam/commands/onboard.ts +374 -0
  276. package/src/tools/loam/commands/outcome.ts +185 -0
  277. package/src/tools/loam/commands/prime.ts +688 -0
  278. package/src/tools/loam/commands/prune.ts +614 -0
  279. package/src/tools/loam/commands/query.ts +218 -0
  280. package/src/tools/loam/commands/rank.ts +180 -0
  281. package/src/tools/loam/commands/ready.ts +189 -0
  282. package/src/tools/loam/commands/record.ts +1210 -0
  283. package/src/tools/loam/commands/restore.ts +166 -0
  284. package/src/tools/loam/commands/search.ts +327 -0
  285. package/src/tools/loam/commands/setup.ts +887 -0
  286. package/src/tools/loam/commands/status.ts +103 -0
  287. package/src/tools/loam/commands/sync.ts +298 -0
  288. package/src/tools/loam/commands/update.ts +19 -0
  289. package/src/tools/loam/commands/upgrade.ts +93 -0
  290. package/src/tools/loam/commands/validate.ts +190 -0
  291. package/src/tools/loam/index.ts +62 -0
  292. package/src/tools/loam/log.ts +127 -0
  293. package/src/tools/loam/registry/builtins.ts +409 -0
  294. package/src/tools/loam/registry/custom.ts +431 -0
  295. package/src/tools/loam/registry/init.ts +55 -0
  296. package/src/tools/loam/registry/template.ts +40 -0
  297. package/src/tools/loam/registry/type-registry.ts +113 -0
  298. package/src/tools/loam/schemas/config-schema.ts +489 -0
  299. package/src/tools/loam/schemas/config.ts +245 -0
  300. package/src/tools/loam/schemas/index.ts +18 -0
  301. package/src/tools/loam/schemas/record-schema.ts +191 -0
  302. package/src/tools/loam/schemas/record.ts +115 -0
  303. package/src/tools/loam/utils/active-work.ts +205 -0
  304. package/src/tools/loam/utils/anchor-validity.ts +80 -0
  305. package/src/tools/loam/utils/archive.ts +146 -0
  306. package/src/tools/loam/utils/audit.ts +667 -0
  307. package/src/tools/loam/utils/bm25.ts +238 -0
  308. package/src/tools/loam/utils/budget.ts +142 -0
  309. package/src/tools/loam/utils/config.ts +344 -0
  310. package/src/tools/loam/utils/dir-anchors.ts +62 -0
  311. package/src/tools/loam/utils/domain-rules.ts +114 -0
  312. package/src/tools/loam/utils/expertise.ts +393 -0
  313. package/src/tools/loam/utils/format-helpers.ts +96 -0
  314. package/src/tools/loam/utils/format.ts +1234 -0
  315. package/src/tools/loam/utils/git-context.ts +50 -0
  316. package/src/tools/loam/utils/git.ts +183 -0
  317. package/src/tools/loam/utils/hooks.ts +299 -0
  318. package/src/tools/loam/utils/index.ts +52 -0
  319. package/src/tools/loam/utils/json-output.ts +13 -0
  320. package/src/tools/loam/utils/lock.ts +76 -0
  321. package/src/tools/loam/utils/markers.ts +48 -0
  322. package/src/tools/loam/utils/numeric-flags.ts +20 -0
  323. package/src/tools/loam/utils/palette.ts +44 -0
  324. package/src/tools/loam/utils/prime-ranking.ts +135 -0
  325. package/src/tools/loam/utils/recipe-discovery.ts +195 -0
  326. package/src/tools/loam/utils/runtime-flags.ts +28 -0
  327. package/src/tools/loam/utils/scoring.ts +94 -0
  328. package/src/tools/loam/utils/version.ts +116 -0
  329. package/src/tools/sprout/commands/block.ts +64 -0
  330. package/src/tools/sprout/commands/blocked.ts +86 -0
  331. package/src/tools/sprout/commands/close.ts +129 -0
  332. package/src/tools/sprout/commands/completions.ts +198 -0
  333. package/src/tools/sprout/commands/config.ts +238 -0
  334. package/src/tools/sprout/commands/create.ts +164 -0
  335. package/src/tools/sprout/commands/dep.ts +148 -0
  336. package/src/tools/sprout/commands/doctor.ts +979 -0
  337. package/src/tools/sprout/commands/init.ts +83 -0
  338. package/src/tools/sprout/commands/label.ts +178 -0
  339. package/src/tools/sprout/commands/list.ts +210 -0
  340. package/src/tools/sprout/commands/migrate.ts +133 -0
  341. package/src/tools/sprout/commands/onboard.ts +207 -0
  342. package/src/tools/sprout/commands/plan-show.ts +278 -0
  343. package/src/tools/sprout/commands/plan.ts +2526 -0
  344. package/src/tools/sprout/commands/prime.ts +399 -0
  345. package/src/tools/sprout/commands/ready.ts +245 -0
  346. package/src/tools/sprout/commands/search.ts +221 -0
  347. package/src/tools/sprout/commands/show.ts +277 -0
  348. package/src/tools/sprout/commands/stats.ts +146 -0
  349. package/src/tools/sprout/commands/sync.ts +134 -0
  350. package/src/tools/sprout/commands/tpl.ts +364 -0
  351. package/src/tools/sprout/commands/unblock.ts +115 -0
  352. package/src/tools/sprout/commands/update.ts +257 -0
  353. package/src/tools/sprout/commands/upgrade.ts +91 -0
  354. package/src/tools/sprout/config-schema.ts +152 -0
  355. package/src/tools/sprout/config.ts +355 -0
  356. package/src/tools/sprout/filter.ts +107 -0
  357. package/src/tools/sprout/format.ts +43 -0
  358. package/src/tools/sprout/id.ts +22 -0
  359. package/src/tools/sprout/index.ts +204 -0
  360. package/src/tools/sprout/log.ts +76 -0
  361. package/src/tools/sprout/markers.ts +22 -0
  362. package/src/tools/sprout/output.ts +121 -0
  363. package/src/tools/sprout/plan-backref.ts +93 -0
  364. package/src/tools/sprout/plan-context.ts +81 -0
  365. package/src/tools/sprout/plan-domain.ts +139 -0
  366. package/src/tools/sprout/plan-lifecycle.ts +65 -0
  367. package/src/tools/sprout/plan-loam.ts +207 -0
  368. package/src/tools/sprout/plan-schema.ts +209 -0
  369. package/src/tools/sprout/sort.ts +31 -0
  370. package/src/tools/sprout/store.ts +172 -0
  371. package/src/tools/sprout/types.ts +118 -0
  372. package/src/tools/sprout/validation.ts +119 -0
  373. package/src/tools/sprout/version.ts +1 -0
  374. package/src/tools/sprout/yaml.ts +387 -0
  375. package/src/tools/trellis/commands/archive.ts +87 -0
  376. package/src/tools/trellis/commands/completions.ts +610 -0
  377. package/src/tools/trellis/commands/config.ts +382 -0
  378. package/src/tools/trellis/commands/create.ts +252 -0
  379. package/src/tools/trellis/commands/diff.ts +150 -0
  380. package/src/tools/trellis/commands/doctor.ts +771 -0
  381. package/src/tools/trellis/commands/emit.ts +365 -0
  382. package/src/tools/trellis/commands/history.ts +83 -0
  383. package/src/tools/trellis/commands/import.ts +198 -0
  384. package/src/tools/trellis/commands/init.ts +81 -0
  385. package/src/tools/trellis/commands/list.ts +103 -0
  386. package/src/tools/trellis/commands/onboard.ts +156 -0
  387. package/src/tools/trellis/commands/pin.ts +172 -0
  388. package/src/tools/trellis/commands/prime.ts +193 -0
  389. package/src/tools/trellis/commands/render.ts +122 -0
  390. package/src/tools/trellis/commands/schema.ts +353 -0
  391. package/src/tools/trellis/commands/show.ts +115 -0
  392. package/src/tools/trellis/commands/stats.ts +65 -0
  393. package/src/tools/trellis/commands/sync.ts +112 -0
  394. package/src/tools/trellis/commands/tree.ts +123 -0
  395. package/src/tools/trellis/commands/update.ts +330 -0
  396. package/src/tools/trellis/commands/upgrade.ts +95 -0
  397. package/src/tools/trellis/commands/validate.ts +166 -0
  398. package/src/tools/trellis/config-schema.ts +81 -0
  399. package/src/tools/trellis/config.ts +108 -0
  400. package/src/tools/trellis/frontmatter.ts +348 -0
  401. package/src/tools/trellis/id.ts +24 -0
  402. package/src/tools/trellis/index.ts +209 -0
  403. package/src/tools/trellis/markers.ts +28 -0
  404. package/src/tools/trellis/output.ts +84 -0
  405. package/src/tools/trellis/render.ts +212 -0
  406. package/src/tools/trellis/store.ts +144 -0
  407. package/src/tools/trellis/types.ts +82 -0
  408. package/src/tools/trellis/validate.ts +199 -0
  409. package/src/tools/trellis/yaml.ts +309 -0
  410. package/src/tracker/beads.test.ts +454 -0
  411. package/src/tracker/beads.ts +56 -0
  412. package/src/tracker/factory.test.ts +90 -0
  413. package/src/tracker/factory.ts +65 -0
  414. package/src/tracker/sprout.test.ts +461 -0
  415. package/src/tracker/sprout.ts +182 -0
  416. package/src/tracker/types.ts +52 -0
  417. package/src/trellis/client.test.ts +107 -0
  418. package/src/trellis/client.ts +179 -0
  419. package/src/types.ts +970 -0
  420. package/src/utils/bin.test.ts +10 -0
  421. package/src/utils/bin.ts +37 -0
  422. package/src/utils/browser.test.ts +49 -0
  423. package/src/utils/browser.ts +48 -0
  424. package/src/utils/fs.test.ts +119 -0
  425. package/src/utils/fs.ts +62 -0
  426. package/src/utils/pid.test.ts +152 -0
  427. package/src/utils/pid.ts +130 -0
  428. package/src/utils/process-scan.test.ts +53 -0
  429. package/src/utils/process-scan.ts +76 -0
  430. package/src/utils/time.test.ts +43 -0
  431. package/src/utils/time.ts +37 -0
  432. package/src/utils/version.test.ts +33 -0
  433. package/src/utils/version.ts +70 -0
  434. package/src/version.ts +5 -0
  435. package/src/watchdog/daemon.test.ts +3721 -0
  436. package/src/watchdog/daemon.ts +1257 -0
  437. package/src/watchdog/health.test.ts +830 -0
  438. package/src/watchdog/health.ts +434 -0
  439. package/src/watchdog/triage.test.ts +205 -0
  440. package/src/watchdog/triage.ts +205 -0
  441. package/src/worktree/manager.test.ts +720 -0
  442. package/src/worktree/manager.ts +405 -0
  443. package/src/worktree/process.test.ts +172 -0
  444. package/src/worktree/process.ts +131 -0
  445. package/src/worktree/tmux.test.ts +1616 -0
  446. package/src/worktree/tmux.ts +721 -0
  447. package/templates/CLAUDE.md.tmpl +100 -0
  448. package/templates/copilot-hooks.json.tmpl +13 -0
  449. package/templates/hooks.json.tmpl +109 -0
  450. package/templates/overlay.md.tmpl +88 -0
  451. package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
  452. package/ui/dist/chunk-8s31f05k.css +1 -0
  453. package/ui/dist/chunk-vm5rz679.js +300 -0
  454. package/ui/dist/favicon-nzb39vza.svg +4 -0
  455. package/ui/dist/index.html +17 -0
@@ -0,0 +1,579 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync } from "node:fs";
3
+ import { mkdtemp } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { createEventStore } from "../events/store.ts";
7
+ import { removeConnection, setConnection } from "../runtimes/connections.ts";
8
+ import type { NudgeableConnection, NudgeResult } from "../runtimes/headless-connection.ts";
9
+ import { createSessionStore } from "../sessions/store.ts";
10
+ import { cleanupTempDir } from "../test-helpers.ts";
11
+ import type { AgentSession, StoredEvent } from "../types.ts";
12
+
13
+ /**
14
+ * Tests for the nudge command's debounce and session lookup logic.
15
+ *
16
+ * We test the pure/file-based functions directly rather than the full
17
+ * nudgeCommand (which requires real tmux sessions). Tmux interaction
18
+ * is tested via E2E.
19
+ */
20
+
21
+ let tempDir: string;
22
+
23
+ beforeEach(async () => {
24
+ tempDir = await mkdtemp(join(tmpdir(), "nudge-test-"));
25
+ });
26
+
27
+ afterEach(async () => {
28
+ await cleanupTempDir(tempDir);
29
+ });
30
+
31
+ /**
32
+ * Helper to write sessions to SessionStore (sessions.db) for testing.
33
+ */
34
+ function writeSessionsToStore(projectRoot: string, sessions: AgentSession[]): void {
35
+ const dir = join(projectRoot, ".agentplate");
36
+ mkdirSync(dir, { recursive: true });
37
+ const dbPath = join(dir, "sessions.db");
38
+ const store = createSessionStore(dbPath);
39
+ for (const session of sessions) {
40
+ store.upsert(session);
41
+ }
42
+ store.close();
43
+ }
44
+
45
+ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
46
+ return {
47
+ id: "session-123-test-agent",
48
+ agentName: "test-agent",
49
+ capability: "builder",
50
+ worktreePath: "/tmp/wt",
51
+ branchName: "agentplate/test-agent/task-1",
52
+ taskId: "task-1",
53
+ tmuxSession: "agentplate-test-agent",
54
+ state: "working",
55
+ pid: 12345,
56
+ parentAgent: null,
57
+ depth: 0,
58
+ runId: null,
59
+ startedAt: new Date().toISOString(),
60
+ lastActivity: new Date().toISOString(),
61
+ escalationLevel: 0,
62
+ stalledSince: null,
63
+ transcriptPath: null,
64
+ ...overrides,
65
+ };
66
+ }
67
+
68
+ describe("paneAppearsBusy", () => {
69
+ test("flags Claude Code mid-think pane as busy", async () => {
70
+ const { paneAppearsBusy } = await import("./nudge.ts");
71
+ const sample = [
72
+ "╭───────────────────────────────────────────╮",
73
+ "│ ✻ Cooking… (5s · ↓ 0 tokens · esc to interrupt)",
74
+ "╰───────────────────────────────────────────╯",
75
+ " ⏵⏵ bypass permissions on (alt+m to cycle)",
76
+ ].join("\n");
77
+ expect(paneAppearsBusy(sample)).toBe(true);
78
+ });
79
+
80
+ test("treats idle pane (no esc-to-interrupt) as not busy", async () => {
81
+ const { paneAppearsBusy } = await import("./nudge.ts");
82
+ const sample = [
83
+ "$ ❯ ls",
84
+ "src/",
85
+ "╭───────────────────────────────────────────╮",
86
+ "│ > _ │",
87
+ "╰───────────────────────────────────────────╯",
88
+ " ⏵⏵ bypass permissions on (alt+m to cycle)",
89
+ ].join("\n");
90
+ expect(paneAppearsBusy(sample)).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe("nudgeAgent", () => {
95
+ // We dynamically import to avoid circular issues
96
+ async function importNudge() {
97
+ return await import("./nudge.ts");
98
+ }
99
+
100
+ test("returns error when no active session exists", async () => {
101
+ writeSessionsToStore(tempDir, []);
102
+ const { nudgeAgent } = await importNudge();
103
+ const result = await nudgeAgent(tempDir, "nonexistent-agent");
104
+ expect(result.delivered).toBe(false);
105
+ expect(result.reason).toContain("No active session");
106
+ });
107
+
108
+ test("returns error with recovery hint when agent is zombie", async () => {
109
+ writeSessionsToStore(tempDir, [
110
+ makeSession({ state: "zombie", capability: "lead", taskId: "task-42" }),
111
+ ]);
112
+ const { nudgeAgent } = await importNudge();
113
+ const result = await nudgeAgent(tempDir, "test-agent");
114
+ expect(result.delivered).toBe(false);
115
+ expect(result.reason).toContain("No active session");
116
+ expect(result.reason).toContain("state: zombie");
117
+ expect(result.reason).toContain("ap sling task-42 --capability lead --recover");
118
+ });
119
+
120
+ test("returns error with recovery hint when agent is completed", async () => {
121
+ writeSessionsToStore(tempDir, [
122
+ makeSession({ state: "completed", capability: "lead", taskId: "task-42" }),
123
+ ]);
124
+ const { nudgeAgent } = await importNudge();
125
+ const result = await nudgeAgent(tempDir, "test-agent");
126
+ expect(result.delivered).toBe(false);
127
+ expect(result.reason).toContain("No active session");
128
+ expect(result.reason).toContain("state: completed");
129
+ expect(result.reason).toContain("ap sling task-42 --capability lead --recover");
130
+ });
131
+
132
+ test("finds active agent in working state", async () => {
133
+ writeSessionsToStore(tempDir, [makeSession({ state: "working" })]);
134
+ const { nudgeAgent } = await importNudge();
135
+ // This will fail on sendKeys (no real tmux) but should get past session lookup
136
+ const result = await nudgeAgent(tempDir, "test-agent");
137
+ // Will fail because tmux session doesn't exist, but we validated session lookup works
138
+ expect(result.delivered).toBe(false);
139
+ expect(result.reason).toContain("not alive");
140
+ });
141
+
142
+ test("finds active agent in booting state", async () => {
143
+ writeSessionsToStore(tempDir, [makeSession({ state: "booting" })]);
144
+ const { nudgeAgent } = await importNudge();
145
+ const result = await nudgeAgent(tempDir, "test-agent");
146
+ expect(result.delivered).toBe(false);
147
+ expect(result.reason).toContain("not alive");
148
+ });
149
+
150
+ test("handles missing sessions.db gracefully", async () => {
151
+ // Create .agentplate dir but no sessions.db — SessionStore will be created empty
152
+ mkdirSync(join(tempDir, ".agentplate"), { recursive: true });
153
+ const { nudgeAgent } = await importNudge();
154
+ const result = await nudgeAgent(tempDir, "test-agent");
155
+ expect(result.delivered).toBe(false);
156
+ expect(result.reason).toContain("No active session");
157
+ });
158
+
159
+ test("resolves orchestrator from orchestrator-tmux.json fallback", async () => {
160
+ // No sessions.db, but orchestrator-tmux.json exists
161
+ const { mkdir } = await import("node:fs/promises");
162
+ await mkdir(join(tempDir, ".agentplate"), { recursive: true });
163
+ await Bun.write(
164
+ join(tempDir, ".agentplate", "orchestrator-tmux.json"),
165
+ `${JSON.stringify({ tmuxSession: "my-session", registeredAt: new Date().toISOString() }, null, "\t")}\n`,
166
+ );
167
+
168
+ const { nudgeAgent } = await importNudge();
169
+ const result = await nudgeAgent(tempDir, "orchestrator");
170
+ // Will fail at tmux alive check (no real tmux), but should get past resolution
171
+ expect(result.delivered).toBe(false);
172
+ expect(result.reason).toContain("not alive");
173
+ });
174
+
175
+ test("returns error when orchestrator has no tmux registration", async () => {
176
+ const { mkdir } = await import("node:fs/promises");
177
+ await mkdir(join(tempDir, ".agentplate"), { recursive: true });
178
+ // No orchestrator-tmux.json and no sessions.db entry
179
+ const { nudgeAgent } = await importNudge();
180
+ const result = await nudgeAgent(tempDir, "orchestrator");
181
+ expect(result.delivered).toBe(false);
182
+ expect(result.reason).toContain("No active session");
183
+ });
184
+
185
+ test("prefers sessions.db over orchestrator-tmux.json for orchestrator", async () => {
186
+ // If orchestrator somehow appears in sessions.db, use that
187
+ writeSessionsToStore(tempDir, [
188
+ makeSession({
189
+ agentName: "orchestrator",
190
+ tmuxSession: "agentplate-orchestrator",
191
+ state: "working",
192
+ }),
193
+ ]);
194
+ await Bun.write(
195
+ join(tempDir, ".agentplate", "orchestrator-tmux.json"),
196
+ `${JSON.stringify({ tmuxSession: "fallback-session" }, null, "\t")}\n`,
197
+ );
198
+
199
+ const { nudgeAgent } = await importNudge();
200
+ const result = await nudgeAgent(tempDir, "orchestrator");
201
+ // Should use sessions.db entry, fail at tmux alive check
202
+ expect(result.delivered).toBe(false);
203
+ expect(result.reason).toContain("agentplate-orchestrator");
204
+ });
205
+
206
+ test("records nudge event to EventStore after delivery attempt", async () => {
207
+ // Agent exists in SessionStore but tmux is not alive — nudge fails
208
+ // but the event should still be recorded
209
+ writeSessionsToStore(tempDir, [makeSession({ state: "working" })]);
210
+
211
+ const { nudgeAgent } = await importNudge();
212
+ const result = await nudgeAgent(tempDir, "test-agent");
213
+ // Nudge fails because tmux session is not alive
214
+ expect(result.delivered).toBe(false);
215
+
216
+ // Verify event was recorded to events.db
217
+ const eventsDbPath = join(tempDir, ".agentplate", "events.db");
218
+ const store = createEventStore(eventsDbPath);
219
+ try {
220
+ const events: StoredEvent[] = store.getTimeline({
221
+ since: "2000-01-01T00:00:00Z",
222
+ });
223
+ const nudgeEvent = events.find((e) => {
224
+ if (!e.data) return false;
225
+ const data = JSON.parse(e.data) as Record<string, unknown>;
226
+ return data.type === "nudge";
227
+ });
228
+ expect(nudgeEvent).toBeDefined();
229
+ expect(nudgeEvent?.eventType).toBe("custom");
230
+ expect(nudgeEvent?.level).toBe("info");
231
+ expect(nudgeEvent?.agentName).toBe("test-agent");
232
+
233
+ const data = JSON.parse(nudgeEvent?.data ?? "{}") as Record<string, unknown>;
234
+ expect(data.delivered).toBe(false);
235
+ expect(data.from).toBe("orchestrator");
236
+ } finally {
237
+ store.close();
238
+ }
239
+ });
240
+
241
+ test("nudge event includes run_id when current-run.txt exists", async () => {
242
+ writeSessionsToStore(tempDir, [makeSession({ state: "working" })]);
243
+
244
+ // Write a current-run.txt
245
+ const runId = "run-test-123";
246
+ await Bun.write(join(tempDir, ".agentplate", "current-run.txt"), runId);
247
+
248
+ const { nudgeAgent } = await importNudge();
249
+ await nudgeAgent(tempDir, "test-agent");
250
+
251
+ const eventsDbPath = join(tempDir, ".agentplate", "events.db");
252
+ const store = createEventStore(eventsDbPath);
253
+ try {
254
+ const events: StoredEvent[] = store.getTimeline({
255
+ since: "2000-01-01T00:00:00Z",
256
+ });
257
+ const nudgeEvent = events.find((e) => {
258
+ if (!e.data) return false;
259
+ const data = JSON.parse(e.data) as Record<string, unknown>;
260
+ return data.type === "nudge";
261
+ });
262
+ expect(nudgeEvent).toBeDefined();
263
+ expect(nudgeEvent?.runId).toBe(runId);
264
+ } finally {
265
+ store.close();
266
+ }
267
+ });
268
+ });
269
+
270
+ describe("nudgeAgent with headless connection", () => {
271
+ async function importNudge() {
272
+ return await import("./nudge.ts");
273
+ }
274
+
275
+ /** Build a NudgeableConnection stub that records calls. */
276
+ function makeNudgeableConn(
277
+ result: NudgeResult = { status: "Queued" },
278
+ onNudge?: (text: string) => void,
279
+ ): NudgeableConnection {
280
+ return {
281
+ sendPrompt: async () => {},
282
+ followUp: async () => {},
283
+ abort: async () => {},
284
+ getState: async () => ({ status: "idle" as const }),
285
+ close: () => {},
286
+ nudge: async (text: string) => {
287
+ if (onNudge) onNudge(text);
288
+ return result;
289
+ },
290
+ };
291
+ }
292
+
293
+ afterEach(() => {
294
+ removeConnection("headless-test-agent");
295
+ });
296
+
297
+ test("routes nudge through connection.nudge() when connection exists", async () => {
298
+ let capturedText = "";
299
+ setConnection(
300
+ "headless-test-agent",
301
+ makeNudgeableConn({ status: "Queued" }, (t) => {
302
+ capturedText = t;
303
+ }),
304
+ );
305
+
306
+ const { nudgeAgent } = await importNudge();
307
+ const result = await nudgeAgent(tempDir, "headless-test-agent", "ping", true);
308
+
309
+ expect(result.delivered).toBe(true);
310
+ expect(result.queued).toBe(true);
311
+ expect(capturedText).toBe("ping");
312
+ });
313
+
314
+ test("queued=false when connection returns Delivered", async () => {
315
+ setConnection("headless-test-agent", makeNudgeableConn({ status: "Delivered" }));
316
+
317
+ const { nudgeAgent } = await importNudge();
318
+ const result = await nudgeAgent(tempDir, "headless-test-agent", "ping", true);
319
+
320
+ expect(result.delivered).toBe(true);
321
+ expect(result.queued).toBe(false);
322
+ });
323
+
324
+ test("falls back to tmux path when connection has no nudge() method", async () => {
325
+ // Register a plain RuntimeConnection (no nudge method)
326
+ setConnection("headless-test-agent", {
327
+ sendPrompt: async () => {},
328
+ followUp: async () => {},
329
+ abort: async () => {},
330
+ getState: async () => ({ status: "idle" as const }),
331
+ close: () => {},
332
+ });
333
+ // Also add a sessions.db entry so resolveTargetSession can find something
334
+ writeSessionsToStore(tempDir, [makeSession({ agentName: "headless-test-agent" })]);
335
+
336
+ const { nudgeAgent } = await importNudge();
337
+ const result = await nudgeAgent(tempDir, "headless-test-agent");
338
+ // Falls through to tmux — tmux session not alive
339
+ expect(result.delivered).toBe(false);
340
+ expect(result.reason).toContain("not alive");
341
+ // No queued field when tmux path runs
342
+ expect(result.queued).toBeUndefined();
343
+ });
344
+
345
+ test("debounce applies to headless nudges", async () => {
346
+ let nudgeCount = 0;
347
+ setConnection(
348
+ "headless-test-agent",
349
+ makeNudgeableConn({ status: "Queued" }, () => {
350
+ nudgeCount++;
351
+ }),
352
+ );
353
+
354
+ const { nudgeAgent } = await importNudge();
355
+ // First nudge — forced to bypass debounce and prime the state
356
+ await nudgeAgent(tempDir, "headless-test-agent", "first", true);
357
+ // Second nudge immediately — should be debounced (within 500ms window)
358
+ const second = await nudgeAgent(tempDir, "headless-test-agent", "second");
359
+
360
+ expect(nudgeCount).toBe(1);
361
+ expect(second.delivered).toBe(false);
362
+ expect(second.reason).toContain("Debounced");
363
+ });
364
+
365
+ test("records nudge event for headless delivery", async () => {
366
+ setConnection("headless-test-agent", makeNudgeableConn({ status: "Queued" }));
367
+
368
+ const { nudgeAgent } = await importNudge();
369
+ await nudgeAgent(tempDir, "headless-test-agent", "event test", true);
370
+
371
+ const eventsDbPath = join(tempDir, ".agentplate", "events.db");
372
+ const store = createEventStore(eventsDbPath);
373
+ try {
374
+ const events: StoredEvent[] = store.getTimeline({ since: "2000-01-01T00:00:00Z" });
375
+ const nudgeEvent = events.find((e) => {
376
+ if (!e.data) return false;
377
+ const data = JSON.parse(e.data) as Record<string, unknown>;
378
+ return data.type === "nudge";
379
+ });
380
+ expect(nudgeEvent).toBeDefined();
381
+ expect(nudgeEvent?.agentName).toBe("headless-test-agent");
382
+ const data = JSON.parse(nudgeEvent?.data ?? "{}") as Record<string, unknown>;
383
+ expect(data.delivered).toBe(true);
384
+ } finally {
385
+ store.close();
386
+ }
387
+ });
388
+
389
+ test("tmux path: send-keys path invoked for agent with no connection", async () => {
390
+ writeSessionsToStore(tempDir, [makeSession({ state: "working" })]);
391
+
392
+ const { nudgeAgent } = await importNudge();
393
+ const result = await nudgeAgent(tempDir, "test-agent");
394
+ // No connection registered → tmux path → tmux session not alive
395
+ expect(result.delivered).toBe(false);
396
+ expect(result.reason).toContain("not alive");
397
+ expect(result.queued).toBeUndefined();
398
+ });
399
+ });
400
+
401
+ describe("nudgeAgent spawn-per-turn dispatch", () => {
402
+ async function importNudge() {
403
+ return await import("./nudge.ts");
404
+ }
405
+
406
+ function fakeLoadConfig(): typeof import("../config.ts").loadConfig {
407
+ return (async (root: string) => ({
408
+ project: { name: "test", root, canonicalBranch: "main" },
409
+ agents: {
410
+ baseDir: "agents",
411
+ manifestPath: ".agentplate/agent-manifest.json",
412
+ maxConcurrent: 5,
413
+ maxSessionsPerRun: 0,
414
+ maxAgentsPerLead: 5,
415
+ maxDepth: 2,
416
+ staggerDelayMs: 0,
417
+ autoNudgeOnMail: false,
418
+ },
419
+ worktrees: { baseDir: ".agentplate/worktrees" },
420
+ merge: { mode: "manual" },
421
+ loam: { enabled: false, domains: {} },
422
+ trellis: { enabled: false },
423
+ taskTracker: { backend: "sprout", enabled: true },
424
+ watchdog: {
425
+ tier0Enabled: false,
426
+ tier0IntervalMs: 30_000,
427
+ tier1Enabled: false,
428
+ maxEscalationLevel: 3,
429
+ },
430
+ models: {},
431
+ logging: { verbose: false, redactSecrets: true },
432
+ runtime: { default: "claude" },
433
+ providers: {},
434
+ })) as unknown as typeof import("../config.ts").loadConfig;
435
+ }
436
+
437
+ async function writeManifest(projectRoot: string): Promise<void> {
438
+ mkdirSync(join(projectRoot, ".agentplate"), { recursive: true });
439
+ mkdirSync(join(projectRoot, "agents"), { recursive: true });
440
+ await Bun.write(join(projectRoot, "agents", "builder.md"), "# Builder\n");
441
+ await Bun.write(
442
+ join(projectRoot, ".agentplate", "agent-manifest.json"),
443
+ JSON.stringify(
444
+ {
445
+ version: "1",
446
+ agents: {
447
+ builder: {
448
+ file: "builder.md",
449
+ model: "claude-sonnet",
450
+ tools: [],
451
+ capabilities: ["build"],
452
+ canSpawn: false,
453
+ constraints: [],
454
+ },
455
+ },
456
+ },
457
+ null,
458
+ "\t",
459
+ ),
460
+ );
461
+ }
462
+
463
+ test("routes builder nudge through runTurn when flag is on", async () => {
464
+ writeSessionsToStore(tempDir, [makeSession({ state: "working", capability: "builder" })]);
465
+ await writeManifest(tempDir);
466
+
467
+ const calls: Array<{ userTurnNdjson: string }> = [];
468
+ const stubRunTurn = async (opts: import("../agents/turn-runner.ts").RunTurnOpts) => {
469
+ calls.push({ userTurnNdjson: opts.userTurnNdjson });
470
+ return {
471
+ exitCode: 0,
472
+ cleanResult: true,
473
+ newSessionId: null,
474
+ resumeMismatch: false,
475
+ terminalMailObserved: false,
476
+ durationMs: 1,
477
+ initialState: "booting" as const,
478
+ finalState: "working" as const,
479
+ stallAborted: false,
480
+ terminalMailMissing: false,
481
+ };
482
+ };
483
+
484
+ const { nudgeAgent } = await importNudge();
485
+ const result = await nudgeAgent(tempDir, "test-agent", "please pivot", true, {
486
+ _loadConfig: fakeLoadConfig(),
487
+ _runTurnFn: stubRunTurn,
488
+ });
489
+
490
+ expect(result.delivered).toBe(true);
491
+ expect(calls.length).toBe(1);
492
+ const parsed = JSON.parse(calls[0]?.userTurnNdjson?.trimEnd() ?? "");
493
+ expect(parsed.type).toBe("user");
494
+ expect(parsed.message.content[0].text).toBe("please pivot");
495
+ });
496
+
497
+ test("task-scoped non-builder capability (scout) IS routed to spawn-per-turn", async () => {
498
+ writeSessionsToStore(tempDir, [
499
+ makeSession({ state: "working", capability: "scout", agentName: "scout-1" }),
500
+ ]);
501
+ await writeManifest(tempDir);
502
+
503
+ let runTurnCalled = false;
504
+ const stubRunTurn = async () => {
505
+ runTurnCalled = true;
506
+ return {
507
+ exitCode: 0,
508
+ cleanResult: true,
509
+ newSessionId: null,
510
+ resumeMismatch: false,
511
+ terminalMailObserved: false,
512
+ durationMs: 1,
513
+ initialState: "booting" as const,
514
+ finalState: "working" as const,
515
+ stallAborted: false,
516
+ terminalMailMissing: false,
517
+ };
518
+ };
519
+
520
+ const { nudgeAgent } = await importNudge();
521
+ await nudgeAgent(tempDir, "scout-1", "ping", true, {
522
+ _loadConfig: fakeLoadConfig(),
523
+ _runTurnFn: stubRunTurn,
524
+ });
525
+
526
+ expect(runTurnCalled).toBe(true);
527
+ });
528
+
529
+ test("persistent capability (coordinator) is NOT routed to spawn-per-turn", async () => {
530
+ writeSessionsToStore(tempDir, [
531
+ makeSession({ state: "working", capability: "coordinator", agentName: "coord-1" }),
532
+ ]);
533
+ await writeManifest(tempDir);
534
+
535
+ let runTurnCalled = false;
536
+ const stubRunTurn = async () => {
537
+ runTurnCalled = true;
538
+ return {
539
+ exitCode: 0,
540
+ cleanResult: true,
541
+ newSessionId: null,
542
+ resumeMismatch: false,
543
+ terminalMailObserved: false,
544
+ durationMs: 1,
545
+ initialState: "booting" as const,
546
+ finalState: "working" as const,
547
+ stallAborted: false,
548
+ terminalMailMissing: false,
549
+ };
550
+ };
551
+
552
+ const { nudgeAgent } = await importNudge();
553
+ await nudgeAgent(tempDir, "coord-1", "ping", true, {
554
+ _loadConfig: fakeLoadConfig(),
555
+ _runTurnFn: stubRunTurn,
556
+ });
557
+
558
+ expect(runTurnCalled).toBe(false);
559
+ });
560
+
561
+ test("returns delivery error when runTurn throws", async () => {
562
+ writeSessionsToStore(tempDir, [makeSession({ state: "working", capability: "builder" })]);
563
+ await writeManifest(tempDir);
564
+
565
+ const stubRunTurn = async (): Promise<never> => {
566
+ throw new Error("simulated spawn failure");
567
+ };
568
+
569
+ const { nudgeAgent } = await importNudge();
570
+ const result = await nudgeAgent(tempDir, "test-agent", "ping", true, {
571
+ _loadConfig: fakeLoadConfig(),
572
+ _runTurnFn: stubRunTurn,
573
+ });
574
+
575
+ expect(result.delivered).toBe(false);
576
+ expect(result.reason).toContain("Spawn-per-turn dispatch failed");
577
+ expect(result.reason).toContain("simulated spawn failure");
578
+ });
579
+ });