@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,146 @@
1
+ /**
2
+ * Scope-violation detection for builder/merger turns (agentplate-9f4d).
3
+ *
4
+ * Surfaces a soft, advisory signal when an agent's modified files exceed its
5
+ * declared FILE_SCOPE without an `expansion_reason:` justification. The
6
+ * turn-runner consumes this on terminal-mail observation; the lead reads the
7
+ * `events.db` record during merge verification.
8
+ *
9
+ * This is observability only — never a hard block. All errors are swallowed.
10
+ */
11
+
12
+ /**
13
+ * Capabilities allowed to modify files. Mirrors the set in
14
+ * `src/agents/hooks-deployer.ts`. Read-only roles (scout, reviewer) do not
15
+ * commit work, so scope detection is a no-op for them. Lead is excluded for
16
+ * the same reason — leads delegate, they don't touch files directly.
17
+ */
18
+ export const IMPLEMENTATION_CAPABILITIES: ReadonlySet<string> = new Set(["builder", "merger"]);
19
+
20
+ /**
21
+ * Synchronous git runner contract. Accepts argv (without the `git` prefix) and
22
+ * a working directory; returns combined stdout. Tests inject a stub; production
23
+ * calls `Bun.spawnSync(["git", ...args], { cwd })`.
24
+ */
25
+ export type GitRunner = (args: string[], cwd: string) => string;
26
+
27
+ const defaultGitRunner: GitRunner = (args, cwd) => {
28
+ const proc = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
29
+ if (proc.exitCode !== 0) {
30
+ return "";
31
+ }
32
+ return proc.stdout.toString();
33
+ };
34
+
35
+ /**
36
+ * Return the subset of `modifiedFiles` not covered by any FILE_SCOPE entry.
37
+ *
38
+ * - Empty `fileScope` is treated as unrestricted: returns `[]`.
39
+ * - A file is in scope when any scope entry matches it literally OR when
40
+ * `new Bun.Glob(entry).match(file)` returns true.
41
+ */
42
+ export function findScopeViolations(modifiedFiles: string[], fileScope: string[]): string[] {
43
+ if (fileScope.length === 0) return [];
44
+
45
+ const globs: Array<{ entry: string; glob: Bun.Glob }> = [];
46
+ for (const entry of fileScope) {
47
+ try {
48
+ globs.push({ entry, glob: new Bun.Glob(entry) });
49
+ } catch {
50
+ // malformed glob — fall back to literal-only match for this entry
51
+ globs.push({ entry, glob: new Bun.Glob(entry.replace(/[*?[\]{}]/g, "\\$&")) });
52
+ }
53
+ }
54
+
55
+ const violations: string[] = [];
56
+ for (const file of modifiedFiles) {
57
+ let matched = false;
58
+ for (const { entry, glob } of globs) {
59
+ if (entry === file) {
60
+ matched = true;
61
+ break;
62
+ }
63
+ try {
64
+ if (glob.match(file)) {
65
+ matched = true;
66
+ break;
67
+ }
68
+ } catch {
69
+ // ignore malformed pattern; continue
70
+ }
71
+ }
72
+ if (!matched) violations.push(file);
73
+ }
74
+ return violations;
75
+ }
76
+
77
+ /**
78
+ * Case-insensitive check for `expansion_reason:` followed by at least one
79
+ * non-whitespace character before the next newline.
80
+ *
81
+ * - Matches `expansion_reason: foo`, `Expansion_Reason: bar`, `EXPANSION_REASON: baz quux`.
82
+ * - Rejects `expansion_reason:` (empty value) and `expansion-reason: foo` (different separator).
83
+ */
84
+ export function hasExpansionReason(message: string): boolean {
85
+ return /expansion_reason:[^\S\n]*\S+/i.test(message);
86
+ }
87
+
88
+ /**
89
+ * Parse `expansion_reason:` values from the output of `git log --format=%B <range>`.
90
+ * Returns each value (trimmed) in encounter order. Commits without the marker
91
+ * are ignored.
92
+ */
93
+ export function parseExpansionReasonsFromGitLog(log: string): string[] {
94
+ const reasons: string[] = [];
95
+ const re = /expansion_reason:[^\S\n]*([^\n]*\S)/gi;
96
+ let m: RegExpExecArray | null = re.exec(log);
97
+ while (m !== null) {
98
+ const value = m[1]?.trim();
99
+ if (value && value.length > 0) reasons.push(value);
100
+ m = re.exec(log);
101
+ }
102
+ return reasons;
103
+ }
104
+
105
+ export interface DetectScopeViolationOpts {
106
+ worktreePath: string;
107
+ baseRef: string;
108
+ fileScope: string[];
109
+ /** Test injection — replaces `Bun.spawnSync` for git calls. */
110
+ gitRunner?: GitRunner;
111
+ }
112
+
113
+ export interface ScopeViolationResult {
114
+ violations: string[];
115
+ expansionReasons: string[];
116
+ }
117
+
118
+ /**
119
+ * Detect scope violations for a worktree at HEAD relative to `baseRef`.
120
+ *
121
+ * - Runs `git diff --name-only baseRef...HEAD` to enumerate modified files.
122
+ * - Runs `git log --format=%B baseRef..HEAD` and parses `expansion_reason:`
123
+ * markers from commit bodies.
124
+ *
125
+ * Always returns. Any failure (git error, parse failure) yields
126
+ * `{ violations: [], expansionReasons: [] }` — this is an advisory signal and
127
+ * must never break the runner.
128
+ */
129
+ export function detectScopeViolation(opts: DetectScopeViolationOpts): ScopeViolationResult {
130
+ const runner = opts.gitRunner ?? defaultGitRunner;
131
+ try {
132
+ const diffOut = runner(["diff", "--name-only", `${opts.baseRef}...HEAD`], opts.worktreePath);
133
+ const modifiedFiles = diffOut
134
+ .split("\n")
135
+ .map((line) => line.trim())
136
+ .filter((line) => line.length > 0);
137
+
138
+ const logOut = runner(["log", "--format=%B", `${opts.baseRef}..HEAD`], opts.worktreePath);
139
+ const expansionReasons = parseExpansionReasonsFromGitLog(logOut);
140
+
141
+ const violations = findScopeViolations(modifiedFiles, opts.fileScope);
142
+ return { violations, expansionReasons };
143
+ } catch {
144
+ return { violations: [], expansionReasons: [] };
145
+ }
146
+ }
@@ -0,0 +1,181 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ _resetInProcessLocks,
7
+ acquireTurnLock,
8
+ readTurnLock,
9
+ turnLockDbPath,
10
+ } from "./turn-lock.ts";
11
+
12
+ describe("turn-lock", () => {
13
+ let agentplateDir: string;
14
+
15
+ beforeEach(async () => {
16
+ agentplateDir = await mkdtemp(join(tmpdir(), "agentplate-turnlock-test-"));
17
+ _resetInProcessLocks();
18
+ });
19
+
20
+ afterEach(async () => {
21
+ _resetInProcessLocks();
22
+ await rm(agentplateDir, { recursive: true, force: true });
23
+ });
24
+
25
+ test("turnLockDbPath joins agentplate dir + turn-locks.db", () => {
26
+ expect(turnLockDbPath("/tmp/agentplate")).toBe("/tmp/agentplate/turn-locks.db");
27
+ });
28
+
29
+ test("acquire creates the row and records holder pid", async () => {
30
+ const handle = await acquireTurnLock({ agentName: "alpha", agentplateDir });
31
+ try {
32
+ const state = readTurnLock(agentplateDir, "alpha");
33
+ expect(state.heldByPid).toBe(process.pid);
34
+ expect(state.acquiredAt).toBeTruthy();
35
+ } finally {
36
+ handle.release();
37
+ }
38
+ });
39
+
40
+ test("release clears the row and is idempotent", async () => {
41
+ const handle = await acquireTurnLock({ agentName: "alpha", agentplateDir });
42
+ handle.release();
43
+ // Calling release a second time must not throw.
44
+ handle.release();
45
+ const state = readTurnLock(agentplateDir, "alpha");
46
+ expect(state.heldByPid).toBeNull();
47
+ expect(state.acquiredAt).toBeNull();
48
+ });
49
+
50
+ test("two acquires for the same agent serialize via in-process queue", async () => {
51
+ // Track entry/exit windows via timestamps. The second call must start
52
+ // AFTER the first releases, never overlap.
53
+ const events: Array<{ id: number; phase: "enter" | "exit"; ts: number }> = [];
54
+
55
+ const work = async (id: number, holdMs: number): Promise<void> => {
56
+ const handle = await acquireTurnLock({ agentName: "shared", agentplateDir });
57
+ events.push({ id, phase: "enter", ts: Date.now() });
58
+ await Bun.sleep(holdMs);
59
+ events.push({ id, phase: "exit", ts: Date.now() });
60
+ handle.release();
61
+ };
62
+
63
+ await Promise.all([work(1, 100), work(2, 50)]);
64
+
65
+ // Sort events by timestamp; verify each acquire's enter follows the
66
+ // previous holder's exit.
67
+ const ordered = [...events].sort((a, b) => a.ts - b.ts);
68
+ expect(ordered.length).toBe(4);
69
+ expect(ordered[0]?.phase).toBe("enter");
70
+ expect(ordered[1]?.phase).toBe("exit");
71
+ expect(ordered[1]?.id).toBe(ordered[0]?.id);
72
+ expect(ordered[2]?.phase).toBe("enter");
73
+ expect(ordered[3]?.phase).toBe("exit");
74
+ expect(ordered[3]?.id).toBe(ordered[2]?.id);
75
+ });
76
+
77
+ test("acquires for different agents proceed concurrently", async () => {
78
+ // Both calls should overlap because the in-process map is keyed per agent.
79
+ let active = 0;
80
+ let maxActive = 0;
81
+ const work = async (name: string): Promise<void> => {
82
+ const handle = await acquireTurnLock({ agentName: name, agentplateDir });
83
+ active++;
84
+ maxActive = Math.max(maxActive, active);
85
+ await Bun.sleep(80);
86
+ active--;
87
+ handle.release();
88
+ };
89
+
90
+ await Promise.all([work("alpha"), work("beta"), work("gamma")]);
91
+ expect(maxActive).toBeGreaterThan(1);
92
+ });
93
+
94
+ test("stale lock (dead pid) is taken over by next acquirer", async () => {
95
+ // Inject _isProcessAlive that says the recorded holder is gone.
96
+ const handle = await acquireTurnLock({
97
+ agentName: "stale",
98
+ agentplateDir,
99
+ ownerPid: 99999, // pretend we are this dead pid
100
+ _isProcessAlive: () => true, // claim alive to plant the lock
101
+ });
102
+ // Don't call release() — we want the row to look orphaned.
103
+
104
+ // Reset in-process locks so the next call is not blocked by the queue
105
+ // from the same Bun process. Cross-process is what we are exercising.
106
+ _resetInProcessLocks();
107
+
108
+ const stolen = await acquireTurnLock({
109
+ agentName: "stale",
110
+ agentplateDir,
111
+ ownerPid: 12345,
112
+ _isProcessAlive: () => false, // prior holder reported dead
113
+ });
114
+ try {
115
+ const state = readTurnLock(agentplateDir, "stale");
116
+ expect(state.heldByPid).toBe(12345);
117
+ } finally {
118
+ stolen.release();
119
+ // release() of the original handle would still be safe because the
120
+ // row pid no longer matches its ownerPid (99999).
121
+ handle.release();
122
+ }
123
+ });
124
+
125
+ test("acquire times out when the lock is held by a live foreign pid", async () => {
126
+ // Plant a lock owned by a different live pid (we say always-alive).
127
+ const planted = await acquireTurnLock({
128
+ agentName: "blocked",
129
+ agentplateDir,
130
+ ownerPid: 77777,
131
+ _isProcessAlive: () => true,
132
+ });
133
+ // Intentionally do NOT release planted — keep the row active.
134
+
135
+ _resetInProcessLocks();
136
+
137
+ const start = Date.now();
138
+ await expect(
139
+ acquireTurnLock({
140
+ agentName: "blocked",
141
+ agentplateDir,
142
+ ownerPid: 88888,
143
+ _isProcessAlive: () => true,
144
+ timeoutMs: 200,
145
+ pollMs: 25,
146
+ }),
147
+ ).rejects.toThrow(/timed out/);
148
+ const elapsed = Date.now() - start;
149
+ expect(elapsed).toBeGreaterThanOrEqual(150);
150
+
151
+ planted.release();
152
+ });
153
+
154
+ test("re-acquire by the same owner pid is allowed (re-entrant by pid)", async () => {
155
+ // First acquire records pid X. A subsequent acquire by the same pid
156
+ // (after in-process queue clears) should succeed without timing out
157
+ // even if the row still names X — this models recovery from a crashed
158
+ // in-process holder where the SQLite row was never released.
159
+ const first = await acquireTurnLock({
160
+ agentName: "reentry",
161
+ agentplateDir,
162
+ ownerPid: 4242,
163
+ });
164
+ // Simulate an in-process crash that lost the in-process tail.
165
+ _resetInProcessLocks();
166
+
167
+ const second = await acquireTurnLock({
168
+ agentName: "reentry",
169
+ agentplateDir,
170
+ ownerPid: 4242,
171
+ timeoutMs: 500,
172
+ });
173
+ try {
174
+ const state = readTurnLock(agentplateDir, "reentry");
175
+ expect(state.heldByPid).toBe(4242);
176
+ } finally {
177
+ second.release();
178
+ first.release();
179
+ }
180
+ });
181
+ });
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Per-agent serialization lock for the spawn-per-turn engine.
3
+ *
4
+ * Ensures that two `runTurn` calls for the same agent never overlap. The lock
5
+ * is layered:
6
+ *
7
+ * 1. **In-process layer** — a module-level `Map<agentName, Promise<void>>`
8
+ * chained via `.then`. Concurrent calls inside the same Bun process queue up.
9
+ *
10
+ * 2. **Cross-process layer** — a SQLite-backed lease at
11
+ * `{agentplateDir}/turn-locks.db`. Each agent has at most one row in
12
+ * `turn_locks(agent_name, held_by_pid, acquired_at)`. Acquire wraps the
13
+ * state change in `BEGIN IMMEDIATE` so two processes cannot claim the same
14
+ * row in the same instant. The transaction itself is short — it does not
15
+ * span the whole turn — so other agents' acquires are not blocked.
16
+ *
17
+ * Stale leases (where `held_by_pid` is no longer alive) are stolen on the
18
+ * next acquire attempt. Both layers are released when `release()` is called.
19
+ */
20
+
21
+ import { Database } from "bun:sqlite";
22
+ import { join } from "node:path";
23
+
24
+ /** In-process serialization tail per agent. Holds the latest queued promise. */
25
+ const inProcessTails = new Map<string, Promise<void>>();
26
+
27
+ export interface TurnLockHandle {
28
+ readonly agentName: string;
29
+ /** Release both layers. Idempotent. */
30
+ release(): void;
31
+ }
32
+
33
+ export interface AcquireTurnLockOpts {
34
+ agentName: string;
35
+ agentplateDir: string;
36
+ /** Process id recorded as the holder. Defaults to `process.pid`. */
37
+ ownerPid?: number;
38
+ /** Maximum time to wait for cross-process acquisition, in ms. Default 60_000. */
39
+ timeoutMs?: number;
40
+ /** Polling interval between cross-process retries when contended. Default 50ms. */
41
+ pollMs?: number;
42
+ /** Test injection: time source. */
43
+ _now?: () => number;
44
+ /** Test injection: liveness check (default uses `process.kill(pid, 0)`). */
45
+ _isProcessAlive?: (pid: number) => boolean;
46
+ /** Test injection: explicit DB path (overrides `{agentplateDir}/turn-locks.db`). */
47
+ _dbPath?: string;
48
+ }
49
+
50
+ const CREATE_TABLE = `
51
+ CREATE TABLE IF NOT EXISTS turn_locks (
52
+ agent_name TEXT PRIMARY KEY,
53
+ held_by_pid INTEGER,
54
+ acquired_at TEXT
55
+ )`;
56
+
57
+ /** Default cross-process database path. */
58
+ export function turnLockDbPath(agentplateDir: string): string {
59
+ return join(agentplateDir, "turn-locks.db");
60
+ }
61
+
62
+ function defaultIsProcessAlive(pid: number): boolean {
63
+ if (!Number.isFinite(pid) || pid <= 0) return false;
64
+ try {
65
+ process.kill(pid, 0);
66
+ return true;
67
+ } catch (err) {
68
+ // EPERM means the process exists but we lack permission to signal it.
69
+ // Treat as alive so we don't steal an active lock.
70
+ const code = (err as NodeJS.ErrnoException).code;
71
+ return code === "EPERM";
72
+ }
73
+ }
74
+
75
+ function openDb(path: string): Database {
76
+ const db = new Database(path);
77
+ db.exec("PRAGMA journal_mode = WAL");
78
+ db.exec("PRAGMA synchronous = NORMAL");
79
+ db.exec("PRAGMA busy_timeout = 5000");
80
+ db.exec(CREATE_TABLE);
81
+ return db;
82
+ }
83
+
84
+ /**
85
+ * Acquire the per-agent turn lock. Resolves once both layers are held.
86
+ *
87
+ * The returned handle MUST be released — failure to do so leaves a stale row
88
+ * that future acquires will treat as held until the holder's pid expires.
89
+ */
90
+ export async function acquireTurnLock(opts: AcquireTurnLockOpts): Promise<TurnLockHandle> {
91
+ const { agentName, agentplateDir } = opts;
92
+ const ownerPid = opts.ownerPid ?? process.pid;
93
+ const now = opts._now ?? (() => Date.now());
94
+ const isProcessAlive = opts._isProcessAlive ?? defaultIsProcessAlive;
95
+ const dbPath = opts._dbPath ?? turnLockDbPath(agentplateDir);
96
+ const timeoutMs = opts.timeoutMs ?? 60_000;
97
+ const pollMs = opts.pollMs ?? 50;
98
+
99
+ // === Layer 1: in-process serialization ===
100
+ const previous = inProcessTails.get(agentName) ?? Promise.resolve();
101
+ let inProcessRelease!: () => void;
102
+ const current = new Promise<void>((resolve) => {
103
+ inProcessRelease = resolve;
104
+ });
105
+ inProcessTails.set(
106
+ agentName,
107
+ previous.then(() => current),
108
+ );
109
+ await previous;
110
+
111
+ // === Layer 2: cross-process SQLite lease ===
112
+ const db = openDb(dbPath);
113
+ const ensureRowStmt = db.prepare<void, { $n: string }>(
114
+ "INSERT OR IGNORE INTO turn_locks (agent_name, held_by_pid, acquired_at) VALUES ($n, NULL, NULL)",
115
+ );
116
+ const selectStmt = db.prepare<
117
+ { held_by_pid: number | null; acquired_at: string | null },
118
+ { $n: string }
119
+ >("SELECT held_by_pid, acquired_at FROM turn_locks WHERE agent_name = $n");
120
+ const claimStmt = db.prepare<void, { $n: string; $p: number; $a: string }>(
121
+ "UPDATE turn_locks SET held_by_pid = $p, acquired_at = $a WHERE agent_name = $n",
122
+ );
123
+ const releaseStmt = db.prepare<void, { $n: string; $p: number }>(
124
+ "UPDATE turn_locks SET held_by_pid = NULL, acquired_at = NULL WHERE agent_name = $n AND held_by_pid = $p",
125
+ );
126
+
127
+ const tearDown = (): void => {
128
+ try {
129
+ db.close();
130
+ } catch {
131
+ // best-effort
132
+ }
133
+ inProcessRelease();
134
+ };
135
+
136
+ const deadline = now() + timeoutMs;
137
+ let acquired = false;
138
+
139
+ while (!acquired) {
140
+ try {
141
+ db.exec("BEGIN IMMEDIATE");
142
+ } catch (err) {
143
+ // busy_timeout exhausted — fall through to retry until our own deadline.
144
+ if (now() >= deadline) {
145
+ tearDown();
146
+ throw err;
147
+ }
148
+ await Bun.sleep(pollMs);
149
+ continue;
150
+ }
151
+
152
+ try {
153
+ ensureRowStmt.run({ $n: agentName });
154
+ const row = selectStmt.get({ $n: agentName });
155
+ const held = row?.held_by_pid ?? null;
156
+ const stale = held !== null && !isProcessAlive(held);
157
+ if (held === null || stale || held === ownerPid) {
158
+ claimStmt.run({
159
+ $n: agentName,
160
+ $p: ownerPid,
161
+ $a: new Date(now()).toISOString(),
162
+ });
163
+ db.exec("COMMIT");
164
+ acquired = true;
165
+ } else {
166
+ db.exec("ROLLBACK");
167
+ }
168
+ } catch (err) {
169
+ try {
170
+ db.exec("ROLLBACK");
171
+ } catch {
172
+ // ignore
173
+ }
174
+ tearDown();
175
+ throw err;
176
+ }
177
+
178
+ if (!acquired) {
179
+ if (now() >= deadline) {
180
+ tearDown();
181
+ throw new Error(
182
+ `turn-lock: timed out after ${timeoutMs}ms acquiring lock for "${agentName}"`,
183
+ );
184
+ }
185
+ await Bun.sleep(pollMs);
186
+ }
187
+ }
188
+
189
+ let released = false;
190
+ return {
191
+ agentName,
192
+ release(): void {
193
+ if (released) return;
194
+ released = true;
195
+ try {
196
+ releaseStmt.run({ $n: agentName, $p: ownerPid });
197
+ } catch {
198
+ // best-effort: SQL failure must not block in-process release.
199
+ }
200
+ try {
201
+ db.close();
202
+ } catch {
203
+ // best-effort
204
+ }
205
+ inProcessRelease();
206
+ },
207
+ };
208
+ }
209
+
210
+ /** Inspect the persisted lock state. Used by tests and diagnostics. */
211
+ export function readTurnLock(
212
+ agentplateDir: string,
213
+ agentName: string,
214
+ dbPath?: string,
215
+ ): { heldByPid: number | null; acquiredAt: string | null } {
216
+ const db = openDb(dbPath ?? turnLockDbPath(agentplateDir));
217
+ try {
218
+ const stmt = db.prepare<
219
+ { held_by_pid: number | null; acquired_at: string | null },
220
+ { $n: string }
221
+ >("SELECT held_by_pid, acquired_at FROM turn_locks WHERE agent_name = $n");
222
+ const row = stmt.get({ $n: agentName });
223
+ return {
224
+ heldByPid: row?.held_by_pid ?? null,
225
+ acquiredAt: row?.acquired_at ?? null,
226
+ };
227
+ } finally {
228
+ db.close();
229
+ }
230
+ }
231
+
232
+ /** Reset the in-process tail map. Used by tests; not exported through index. */
233
+ export function _resetInProcessLocks(): void {
234
+ inProcessTails.clear();
235
+ }