@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,434 @@
1
+ /**
2
+ * Health check state machine and evaluation logic for agent monitoring.
3
+ *
4
+ * ZFC Principle (Zero Failure Crash)
5
+ * ==================================
6
+ * Observable state is the source of truth, not recorded state.
7
+ *
8
+ * Signal priority (highest to lowest):
9
+ * 1. tmux session liveness — Is the tmux session actually running?
10
+ * 2. Process liveness (pid) — Is the Claude Code process still alive?
11
+ * 3. Recorded state — What does sessions.json claim?
12
+ *
13
+ * When signals conflict, always trust what you can observe:
14
+ * - tmux dead + sessions.json says "working" → mark zombie immediately.
15
+ * The recorded state is stale; the process is gone.
16
+ * - tmux alive + sessions.json says "zombie" → investigate, don't auto-kill.
17
+ * Something marked it zombie but the process recovered or was misclassified.
18
+ * - pid dead + tmux alive → the pane's shell survived but the agent process
19
+ * exited. Treat as zombie (the agent is not doing work).
20
+ * - pid alive + tmux dead → should not happen (tmux owns the pid), but if it
21
+ * does, trust tmux (the session is gone).
22
+ *
23
+ * Headless agents (tmuxSession === ''):
24
+ * Headless agents have no tmux session. For these, PID is the PRIMARY liveness
25
+ * signal. The tmuxAlive parameter is meaningless and ignored. ZFC rules are
26
+ * applied using PID liveness instead of tmux liveness.
27
+ *
28
+ * The rationale: sessions.json is updated asynchronously by hooks and can become
29
+ * stale if the agent crashes between hook invocations. tmux and the OS process
30
+ * table are always up-to-date because they reflect real kernel state.
31
+ */
32
+
33
+ import { isPersistentCapability } from "../agents/capabilities.ts";
34
+ import type { AgentSession, AgentState, HealthCheck } from "../types.ts";
35
+
36
+ /**
37
+ * Numeric ordering for forward-only state transitions.
38
+ *
39
+ * `in_turn` and `between_turns` share the `working` rank (1) because, from
40
+ * the watchdog's perspective, all three are "agent is alive and active" —
41
+ * they only differ in whether the spawn-per-turn worker is currently
42
+ * mid-execution or idling between mail batches (agentplate-3087). Same rank
43
+ * means a healthy-classification check (`check.state === "working"`) will
44
+ * not stomp on the more specific in_turn/between_turns states the
45
+ * turn-runner has already written.
46
+ */
47
+ const STATE_ORDER: Record<AgentState, number> = {
48
+ booting: 0,
49
+ working: 1,
50
+ in_turn: 1,
51
+ between_turns: 1,
52
+ completed: 2,
53
+ stalled: 3,
54
+ zombie: 4,
55
+ };
56
+
57
+ /**
58
+ * Check whether a process with the given PID is still running.
59
+ *
60
+ * Uses signal 0 which does not kill the process — it only checks
61
+ * whether it exists and we have permission to signal it.
62
+ *
63
+ * @param pid - The process ID to check
64
+ * @returns true if the process exists, false otherwise
65
+ */
66
+ export function isProcessRunning(pid: number): boolean {
67
+ try {
68
+ // Signal 0 doesn't kill the process — just checks if it exists
69
+ process.kill(pid, 0);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Detect whether a session is a long-lived headless agent.
78
+ *
79
+ * Long-lived headless agents (coordinator, orchestrator, monitor, sapling, etc.)
80
+ * have no tmux session (tmuxSession === '') but do have a persistent process —
81
+ * so `session.pid` is non-null and PID is the primary liveness signal.
82
+ */
83
+ function isHeadlessSession(session: AgentSession): boolean {
84
+ return session.tmuxSession === "" && session.pid !== null;
85
+ }
86
+
87
+ /**
88
+ * Detect whether a session is a spawn-per-turn worker between turns.
89
+ *
90
+ * Spawn-per-turn workers (task-scoped capabilities under the new headless
91
+ * default — builder/scout/reviewer/lead/merger) have no tmux session AND no
92
+ * persistent process: `tmuxSession === ''` and `session.pid === null` from
93
+ * sling onward. The per-turn claude PID lives in
94
+ * `.agentplate/agents/<name>/turn.pid` only while a turn is in flight.
95
+ *
96
+ * "No process" is the normal state between turns, so neither tmux liveness nor
97
+ * pid liveness can be used as a death signal — only `lastActivity` recency
98
+ * (refreshed by the turn-runner on every event and by the watchdog from
99
+ * events.db) can. (agentplate-7a34)
100
+ */
101
+ export function isSpawnPerTurnSession(session: AgentSession): boolean {
102
+ return session.tmuxSession === "" && session.pid === null;
103
+ }
104
+
105
+ /**
106
+ * Evaluate time-based health (persistent capability exemptions, stale, zombie thresholds,
107
+ * booting→working transition). Called after liveness is confirmed for both TUI and headless paths.
108
+ *
109
+ * Assumes that by the time this is called:
110
+ * - The agent is not completed
111
+ * - The agent is not in a liveness-based zombie state
112
+ * - The agent is not in a zombie state that needs investigation
113
+ */
114
+ function evaluateTimeBased(
115
+ session: AgentSession,
116
+ base: Pick<HealthCheck, "agentName" | "timestamp" | "tmuxAlive" | "pidAlive" | "lastActivity">,
117
+ elapsedMs: number,
118
+ thresholds: { staleMs: number; zombieMs: number },
119
+ ): HealthCheck {
120
+ // Persistent capabilities (coordinator, monitor) are expected to have long idle
121
+ // periods waiting for mail/events. Skip time-based stale/zombie detection for
122
+ // them — only tmux/pid liveness matters (checked above).
123
+ if (isPersistentCapability(session.capability)) {
124
+ // Transition booting → working if we reach here (process alive)
125
+ const state = session.state === "booting" ? "working" : session.state;
126
+ return {
127
+ ...base,
128
+ processAlive: true,
129
+ state: state === "stalled" ? "working" : state,
130
+ action: "none",
131
+ reconciliationNote:
132
+ session.state === "stalled"
133
+ ? `Persistent capability "${session.capability}" exempted from stale detection — resetting to working`
134
+ : null,
135
+ };
136
+ }
137
+
138
+ // lastActivity older than zombieMs → zombie
139
+ if (elapsedMs > thresholds.zombieMs) {
140
+ return {
141
+ ...base,
142
+ processAlive: true,
143
+ state: "zombie",
144
+ action: "terminate",
145
+ reconciliationNote: null,
146
+ };
147
+ }
148
+
149
+ // lastActivity older than staleMs → stalled
150
+ if (elapsedMs > thresholds.staleMs) {
151
+ return {
152
+ ...base,
153
+ processAlive: true,
154
+ state: "stalled",
155
+ action: "escalate",
156
+ reconciliationNote: null,
157
+ };
158
+ }
159
+
160
+ // Spawn-per-turn workers (agentplate-3087): healthy classification reports
161
+ // `between_turns` instead of `working`, including the booting → healthy
162
+ // transition. The turn-runner authoritatively writes `in_turn` /
163
+ // `between_turns` while a turn is alive; in_turn is preserved here when
164
+ // already set so a watchdog tick mid-turn does not overwrite it.
165
+ const isSpawnPerTurn = isSpawnPerTurnSession(session);
166
+
167
+ // booting → transition to the healthy state once there's recent activity.
168
+ if (session.state === "booting") {
169
+ return {
170
+ ...base,
171
+ processAlive: true,
172
+ state: isSpawnPerTurn ? "between_turns" : "working",
173
+ action: "none",
174
+ reconciliationNote: null,
175
+ };
176
+ }
177
+
178
+ // Default: healthy active state. For spawn-per-turn workers report the
179
+ // existing in_turn/between_turns substate; for tmux/long-lived agents
180
+ // report `working`. The turn-runner is authoritative for in_turn ↔
181
+ // between_turns transitions, so the watchdog must not stomp the more
182
+ // specific state — same rank in STATE_ORDER ensures `transitionState`
183
+ // also leaves the row alone.
184
+ let healthyState: AgentState;
185
+ if (session.state === "in_turn" || session.state === "between_turns") {
186
+ healthyState = session.state;
187
+ } else if (isSpawnPerTurn) {
188
+ healthyState = "between_turns";
189
+ } else {
190
+ healthyState = "working";
191
+ }
192
+ return {
193
+ ...base,
194
+ processAlive: true,
195
+ state: healthyState,
196
+ action: "none",
197
+ reconciliationNote: null,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Evaluate the health of an agent session.
203
+ *
204
+ * Implements the ZFC principle: observable state (tmux liveness, pid liveness)
205
+ * takes priority over recorded state (sessions.json fields).
206
+ *
207
+ * Decision logic (in priority order):
208
+ *
209
+ * 1. Completed agents skip monitoring entirely.
210
+ * 2. Spawn-per-turn workers (tmuxSession === '' && pid === null): no
211
+ * persistent process between turns — fall straight through to time-based
212
+ * checks driven by lastActivity. PID/tmux liveness are meaningless here.
213
+ * 3. Headless agents with persistent process (tmuxSession === '' && pid !== null):
214
+ * PID is primary liveness signal.
215
+ * - pid dead → zombie, terminate.
216
+ * - pid alive + state zombie → investigate.
217
+ * - pid alive → fall through to time-based checks.
218
+ * 4. tmux dead → zombie, terminate (regardless of what sessions.json says).
219
+ * 5. tmux alive + sessions.json says zombie → investigate (don't auto-kill).
220
+ * Something external marked this zombie, but the process is still running.
221
+ * 6. pid dead + tmux alive → zombie, terminate. The agent process exited but
222
+ * the tmux pane shell survived. The agent is not doing work.
223
+ * 7. lastActivity older than zombieMs → zombie, terminate.
224
+ * 8. lastActivity older than staleMs → stalled, escalate.
225
+ * 9. booting with recent activity → working.
226
+ * 10. Otherwise → working, healthy.
227
+ *
228
+ * @param session - The agent session to evaluate
229
+ * @param tmuxAlive - Whether the agent's tmux session is still running
230
+ * (ignored for headless agents where tmuxSession === '')
231
+ * @param thresholds - Staleness and zombie time thresholds in milliseconds
232
+ * @returns A HealthCheck describing the agent's current state and recommended action
233
+ */
234
+ export function evaluateHealth(
235
+ session: AgentSession,
236
+ tmuxAlive: boolean,
237
+ thresholds: { staleMs: number; zombieMs: number },
238
+ ): HealthCheck {
239
+ const now = new Date();
240
+ const lastActivityTime = new Date(session.lastActivity).getTime();
241
+ const elapsedMs = now.getTime() - lastActivityTime;
242
+
243
+ // Check pid liveness as secondary signal (null if pid unavailable)
244
+ const pidAlive = session.pid !== null ? isProcessRunning(session.pid) : null;
245
+
246
+ // Headless agents have no tmux session; tmuxAlive is always false for them.
247
+ const effectiveTmuxAlive = isHeadlessSession(session) ? false : tmuxAlive;
248
+
249
+ const base: Pick<
250
+ HealthCheck,
251
+ "agentName" | "timestamp" | "tmuxAlive" | "pidAlive" | "lastActivity"
252
+ > = {
253
+ agentName: session.agentName,
254
+ timestamp: now.toISOString(),
255
+ tmuxAlive: effectiveTmuxAlive,
256
+ pidAlive,
257
+ lastActivity: session.lastActivity,
258
+ };
259
+
260
+ // Completed agents don't need health monitoring
261
+ if (session.state === "completed") {
262
+ return {
263
+ ...base,
264
+ processAlive: effectiveTmuxAlive,
265
+ state: "completed",
266
+ action: "none",
267
+ reconciliationNote: null,
268
+ };
269
+ }
270
+
271
+ // === Spawn-per-turn path: no persistent process between turns ===
272
+ // For these workers (agentplate-7a34) `session.pid` is null by design and
273
+ // there is no tmux session. Liveness signals reduce to lastActivity
274
+ // recency: the turn-runner updates it on every parser event during a
275
+ // turn, and the watchdog refreshes it from events.db between turns. PID
276
+ // and tmux checks would always say "dead" and false-positive every fresh
277
+ // agent as zombie within seconds of sling.
278
+ if (isSpawnPerTurnSession(session)) {
279
+ return evaluateTimeBased(session, base, elapsedMs, thresholds);
280
+ }
281
+
282
+ // === Headless path: PID is the primary liveness signal ===
283
+ if (isHeadlessSession(session)) {
284
+ // pid dead: zombie OR completed-with-missed-signal.
285
+ // Distinguish by lastActivity age — recent activity means the agent
286
+ // crashed mid-work (true zombie); stale activity means it likely
287
+ // finished naturally and only the session-end hook didn't deliver
288
+ // (treat as completed). (agentplate-e74b)
289
+ if (pidAlive === false) {
290
+ if (
291
+ elapsedMs > thresholds.staleMs &&
292
+ (session.state === "working" || session.state === "booting" || session.state === "stalled")
293
+ ) {
294
+ return {
295
+ ...base,
296
+ processAlive: false,
297
+ state: "completed",
298
+ action: "complete",
299
+ reconciliationNote: `ZFC: headless pid ${session.pid} dead + stale lastActivity (${Math.round(elapsedMs / 1000)}s ago) — assumed completed (missed session-end signal)`,
300
+ };
301
+ }
302
+ return {
303
+ ...base,
304
+ processAlive: false,
305
+ state: "zombie",
306
+ action: "terminate",
307
+ reconciliationNote: `ZFC: headless agent pid ${session.pid} dead — marking zombie`,
308
+ };
309
+ }
310
+
311
+ // pid alive + state zombie → investigate (equivalent to ZFC Rule 2 for headless)
312
+ if (session.state === "zombie") {
313
+ return {
314
+ ...base,
315
+ processAlive: true,
316
+ state: "zombie",
317
+ action: "investigate",
318
+ reconciliationNote:
319
+ "ZFC: headless pid alive but sessions.json says zombie — investigation needed (don't auto-kill)",
320
+ };
321
+ }
322
+
323
+ // pid alive → fall through to time-based checks
324
+ return evaluateTimeBased(session, base, elapsedMs, thresholds);
325
+ }
326
+
327
+ // === TUI/tmux path ===
328
+
329
+ // ZFC Rule 1: tmux dead → zombie OR completed-with-missed-signal.
330
+ // Distinguish by lastActivity age — recent activity means the agent
331
+ // crashed mid-work (true zombie); stale activity means it likely
332
+ // finished naturally and only the session-end hook didn't deliver
333
+ // (treat as completed). (agentplate-e74b)
334
+ if (!tmuxAlive) {
335
+ if (
336
+ elapsedMs > thresholds.staleMs &&
337
+ (session.state === "working" || session.state === "booting" || session.state === "stalled")
338
+ ) {
339
+ return {
340
+ ...base,
341
+ processAlive: false,
342
+ state: "completed",
343
+ action: "complete",
344
+ reconciliationNote: `ZFC: tmux dead + stale lastActivity (${Math.round(elapsedMs / 1000)}s ago) — assumed completed (missed session-end signal)`,
345
+ };
346
+ }
347
+
348
+ const note =
349
+ session.state === "working" || session.state === "booting"
350
+ ? `ZFC: tmux dead but sessions.json says "${session.state}" — marking zombie (observable state wins)`
351
+ : null;
352
+
353
+ return {
354
+ ...base,
355
+ processAlive: false,
356
+ state: "zombie",
357
+ action: "terminate",
358
+ reconciliationNote: note,
359
+ };
360
+ }
361
+
362
+ // ZFC Rule 2: tmux alive but sessions.json says zombie → investigate.
363
+ // Something marked it zombie but the process is still running. Don't auto-kill;
364
+ // a human or higher-tier agent should decide.
365
+ if (session.state === "zombie") {
366
+ return {
367
+ ...base,
368
+ processAlive: true,
369
+ state: "zombie",
370
+ action: "investigate",
371
+ reconciliationNote:
372
+ "ZFC: tmux alive but sessions.json says zombie — investigation needed (don't auto-kill)",
373
+ };
374
+ }
375
+
376
+ // ZFC Rule 3: pid dead but tmux alive → the agent process exited but the
377
+ // tmux pane shell survived. The agent is not doing work.
378
+ if (pidAlive === false) {
379
+ return {
380
+ ...base,
381
+ processAlive: false,
382
+ state: "zombie",
383
+ action: "terminate",
384
+ reconciliationNote: `ZFC: pid ${session.pid} dead but tmux alive — agent process exited, shell survived`,
385
+ };
386
+ }
387
+
388
+ // Time-based checks (both tmux and pid confirmed alive, or pid unavailable)
389
+ return evaluateTimeBased(session, base, elapsedMs, thresholds);
390
+ }
391
+
392
+ /**
393
+ * Compute the next agent state based on a health check.
394
+ *
395
+ * State transitions are strictly forward-only using the ordering:
396
+ * booting(0) → working(1) → stalled(2) → zombie(3)
397
+ *
398
+ * A state can only advance forward, never move backwards.
399
+ * For example, a zombie can never become working again.
400
+ *
401
+ * Exception (ZFC): When the health check action is "investigate", the state
402
+ * is NOT advanced. This allows a human or higher-tier agent to review the
403
+ * conflicting signals before making a state change.
404
+ *
405
+ * @param currentState - The agent's current state
406
+ * @param check - The latest health check result
407
+ * @returns The new state (always >= currentState in ordering)
408
+ */
409
+ export function transitionState(currentState: AgentState, check: HealthCheck): AgentState {
410
+ // ZFC: investigate means signals conflict — hold state until reviewed
411
+ if (check.action === "investigate") {
412
+ return currentState;
413
+ }
414
+
415
+ // `complete` is a terminal classification triggered when observable state
416
+ // proves the agent finished naturally (missed session-end signal —
417
+ // agentplate-e74b). It bypasses the forward-only STATE_ORDER guard because
418
+ // `completed` (order 2) sits before `stalled` (order 3) and would
419
+ // otherwise be blocked from advancing the recorded state. The matrix in
420
+ // SessionStore.tryTransitionState still gates the actual write.
421
+ if (check.action === "complete") {
422
+ return check.state;
423
+ }
424
+
425
+ const currentOrder = STATE_ORDER[currentState];
426
+ const checkOrder = STATE_ORDER[check.state];
427
+
428
+ // Only move forward — never regress
429
+ if (checkOrder > currentOrder) {
430
+ return check.state;
431
+ }
432
+
433
+ return currentState;
434
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Tests for Tier 1 AI-assisted triage.
3
+ * classifyResponse and buildTriagePrompt are pure functions — tested directly.
4
+ * triageAgent uses real filesystem (temp dirs). Claude spawn is expected to
5
+ * fail in test environments, exercising the fallback-to-extend path.
6
+ * spawnClaude is NOT mocked — we rely on it failing naturally in tests.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
10
+ import { mkdir, mkdtemp } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { cleanupTempDir } from "../test-helpers.ts";
14
+ import { buildTriagePrompt, classifyResponse, triageAgent } from "./triage.ts";
15
+
16
+ describe("classifyResponse", () => {
17
+ test("returns 'retry' when response contains 'retry'", () => {
18
+ const result = classifyResponse("The operation should retry.");
19
+ expect(result).toBe("retry");
20
+ });
21
+
22
+ test("returns 'retry' when response contains 'recoverable'", () => {
23
+ const result = classifyResponse("This error is recoverable.");
24
+ expect(result).toBe("retry");
25
+ });
26
+
27
+ test("returns 'terminate' when response contains 'terminate'", () => {
28
+ const result = classifyResponse("You should terminate the agent.");
29
+ expect(result).toBe("terminate");
30
+ });
31
+
32
+ test("returns 'terminate' when response contains 'fatal'", () => {
33
+ const result = classifyResponse("This is a fatal error.");
34
+ expect(result).toBe("terminate");
35
+ });
36
+
37
+ test("returns 'terminate' when response contains 'failed'", () => {
38
+ const result = classifyResponse("The operation has failed.");
39
+ expect(result).toBe("terminate");
40
+ });
41
+
42
+ test("handles mixed case (e.g., 'RETRY', 'Fatal')", () => {
43
+ expect(classifyResponse("RETRY this operation")).toBe("retry");
44
+ expect(classifyResponse("Fatal error occurred")).toBe("terminate");
45
+ expect(classifyResponse("RecOverAble issue")).toBe("retry");
46
+ });
47
+
48
+ test("returns 'extend' when response contains none of the keywords", () => {
49
+ const result = classifyResponse("The agent is processing data.");
50
+ expect(result).toBe("extend");
51
+ });
52
+
53
+ test("returns 'extend' for empty string", () => {
54
+ const result = classifyResponse("");
55
+ expect(result).toBe("extend");
56
+ });
57
+
58
+ test("first match wins when response has multiple keywords", () => {
59
+ // 'retry' is checked before 'terminate'
60
+ const result = classifyResponse("retry this but it may terminate later");
61
+ expect(result).toBe("retry");
62
+ });
63
+ });
64
+
65
+ describe("buildTriagePrompt", () => {
66
+ test("contains agent name in output", () => {
67
+ const prompt = buildTriagePrompt("test-agent", "2026-02-13T10:00:00Z", "log content");
68
+ expect(prompt).toContain("test-agent");
69
+ });
70
+
71
+ test("contains lastActivity timestamp in output", () => {
72
+ const timestamp = "2026-02-13T10:00:00Z";
73
+ const prompt = buildTriagePrompt("test-agent", timestamp, "log content");
74
+ expect(prompt).toContain(timestamp);
75
+ });
76
+
77
+ test("contains log content wrapped in code fences", () => {
78
+ const logContent = "Error: something went wrong\nat line 42";
79
+ const prompt = buildTriagePrompt("test-agent", "2026-02-13T10:00:00Z", logContent);
80
+ expect(prompt).toContain("```");
81
+ expect(prompt).toContain(logContent);
82
+ expect(prompt.split("```").length).toBeGreaterThanOrEqual(3); // Opening and closing fences
83
+ });
84
+
85
+ test("contains classification instructions (retry/terminate/extend)", () => {
86
+ const prompt = buildTriagePrompt("test-agent", "2026-02-13T10:00:00Z", "log content");
87
+ expect(prompt).toContain("retry");
88
+ expect(prompt).toContain("terminate");
89
+ expect(prompt).toContain("extend");
90
+ });
91
+ });
92
+
93
+ describe("triageAgent", () => {
94
+ let tempRoot: string;
95
+
96
+ beforeEach(async () => {
97
+ tempRoot = await mkdtemp(join(tmpdir(), "triage-test-"));
98
+ });
99
+
100
+ afterEach(async () => {
101
+ await cleanupTempDir(tempRoot);
102
+ });
103
+
104
+ test("returns fallback TriageResult when no logs directory exists", async () => {
105
+ const result = await triageAgent({
106
+ agentName: "test-agent",
107
+ root: tempRoot,
108
+ lastActivity: "2026-02-13T10:00:00Z",
109
+ });
110
+ expect(result.verdict).toBe("extend");
111
+ expect(result.fallback).toBe(true);
112
+ expect(result.reason).toBe("No logs available");
113
+ });
114
+
115
+ test("returns fallback TriageResult when logs directory exists but is empty", async () => {
116
+ const logsDir = join(tempRoot, ".agentplate", "logs", "test-agent");
117
+ await mkdir(logsDir, { recursive: true });
118
+
119
+ const result = await triageAgent({
120
+ agentName: "test-agent",
121
+ root: tempRoot,
122
+ lastActivity: "2026-02-13T10:00:00Z",
123
+ });
124
+ expect(result.verdict).toBe("extend");
125
+ expect(result.fallback).toBe(true);
126
+ });
127
+
128
+ test("returns fallback TriageResult when logs directory has session dir but no session.log", async () => {
129
+ const logsDir = join(tempRoot, ".agentplate", "logs", "test-agent", "2026-02-13T10-00-00");
130
+ await Bun.write(join(logsDir, ".gitkeep"), "");
131
+
132
+ const result = await triageAgent({
133
+ agentName: "test-agent",
134
+ root: tempRoot,
135
+ lastActivity: "2026-02-13T10:00:00Z",
136
+ });
137
+ expect(result.verdict).toBe("extend");
138
+ expect(result.fallback).toBe(true);
139
+ });
140
+
141
+ test("returns fallback TriageResult when session.log exists but claude binary fails", async () => {
142
+ const timestamp = "2026-02-13T10-00-00";
143
+ const sessionLogPath = join(
144
+ tempRoot,
145
+ ".agentplate",
146
+ "logs",
147
+ "test-agent",
148
+ timestamp,
149
+ "session.log",
150
+ );
151
+
152
+ // Create session.log with some content
153
+ await Bun.write(
154
+ sessionLogPath,
155
+ "Agent started\nProcessing data\nError: something went wrong\n",
156
+ );
157
+
158
+ // triageAgent will try to spawn claude which should fail or be killed by timeout.
159
+ // Short timeout ensures the test doesn't hang even if the claude binary
160
+ // exists on the system (e.g., inside a Claude Code session).
161
+ const result = await triageAgent({
162
+ agentName: "test-agent",
163
+ root: tempRoot,
164
+ lastActivity: "2026-02-13T10:00:00Z",
165
+ timeoutMs: 500,
166
+ });
167
+ expect(result.verdict).toBe("extend");
168
+ expect(result.fallback).toBe(true);
169
+ expect(result.reason).toBe("Claude unavailable");
170
+ });
171
+
172
+ test("writes stderr warning when claude is unavailable (fallback path)", async () => {
173
+ const timestamp = "2026-02-13T10-00-00";
174
+ const sessionLogPath = join(
175
+ tempRoot,
176
+ ".agentplate",
177
+ "logs",
178
+ "test-agent",
179
+ timestamp,
180
+ "session.log",
181
+ );
182
+ await Bun.write(sessionLogPath, "some log content\n");
183
+
184
+ const written: string[] = [];
185
+ const spy = spyOn(process.stderr, "write").mockImplementation((chunk: unknown) => {
186
+ written.push(String(chunk));
187
+ return true;
188
+ });
189
+
190
+ try {
191
+ await triageAgent({
192
+ agentName: "test-agent",
193
+ root: tempRoot,
194
+ lastActivity: "2026-02-13T10:00:00Z",
195
+ timeoutMs: 500,
196
+ });
197
+ } finally {
198
+ spy.mockRestore();
199
+ }
200
+
201
+ expect(written.some((s) => s.includes("triage fallback") && s.includes("test-agent"))).toBe(
202
+ true,
203
+ );
204
+ });
205
+ });