@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,50 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ export function getCurrentCommit(cwd?: string): string | undefined {
4
+ try {
5
+ return execFileSync("git", ["rev-parse", "HEAD"], {
6
+ cwd: cwd ?? process.cwd(),
7
+ encoding: "utf-8",
8
+ stdio: ["pipe", "pipe", "pipe"],
9
+ }).trim();
10
+ } catch {
11
+ return undefined;
12
+ }
13
+ }
14
+
15
+ export function getContextFiles(cwd?: string): string[] {
16
+ const dir = cwd ?? process.cwd();
17
+ const files = new Set<string>();
18
+
19
+ try {
20
+ const staged = execFileSync("git", ["diff", "--name-only", "--cached"], {
21
+ cwd: dir,
22
+ encoding: "utf-8",
23
+ stdio: ["pipe", "pipe", "pipe"],
24
+ }).trim();
25
+ if (staged) {
26
+ for (const f of staged.split("\n")) {
27
+ if (f) files.add(f);
28
+ }
29
+ }
30
+ } catch {
31
+ // not a git repo or no staged changes
32
+ }
33
+
34
+ try {
35
+ const unstaged = execFileSync("git", ["diff", "--name-only"], {
36
+ cwd: dir,
37
+ encoding: "utf-8",
38
+ stdio: ["pipe", "pipe", "pipe"],
39
+ }).trim();
40
+ if (unstaged) {
41
+ for (const f of unstaged.split("\n")) {
42
+ if (f) files.add(f);
43
+ }
44
+ }
45
+ } catch {
46
+ // ignore
47
+ }
48
+
49
+ return [...files].sort();
50
+ }
@@ -0,0 +1,183 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import type { ExpertiseRecord } from "../schemas/record.ts";
3
+ import type { TrackerName } from "./active-work.ts";
4
+ import { fileLivesUnderDir } from "./dir-anchors.ts";
5
+
6
+ export function isGitRepo(cwd: string): boolean {
7
+ try {
8
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
9
+ cwd,
10
+ stdio: "pipe",
11
+ });
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ export function getChangedFiles(cwd: string, since: string): string[] {
19
+ const files = new Set<string>();
20
+
21
+ // Committed changes (since ref)
22
+ try {
23
+ const committed = execFileSync("git", ["diff", "--name-only", since], {
24
+ cwd,
25
+ encoding: "utf-8",
26
+ stdio: ["pipe", "pipe", "pipe"],
27
+ }).trim();
28
+ if (committed) {
29
+ for (const f of committed.split("\n")) {
30
+ if (f) files.add(f);
31
+ }
32
+ }
33
+ } catch {
34
+ // ref might not exist (e.g., first commit) — fall through
35
+ }
36
+
37
+ // Staged but uncommitted changes
38
+ try {
39
+ const staged = execFileSync("git", ["diff", "--name-only", "--cached"], {
40
+ cwd,
41
+ encoding: "utf-8",
42
+ stdio: ["pipe", "pipe", "pipe"],
43
+ }).trim();
44
+ if (staged) {
45
+ for (const f of staged.split("\n")) {
46
+ if (f) files.add(f);
47
+ }
48
+ }
49
+ } catch {
50
+ // ignore
51
+ }
52
+
53
+ // Unstaged working tree changes
54
+ try {
55
+ const unstaged = execFileSync("git", ["diff", "--name-only"], {
56
+ cwd,
57
+ encoding: "utf-8",
58
+ stdio: ["pipe", "pipe", "pipe"],
59
+ }).trim();
60
+ if (unstaged) {
61
+ for (const f of unstaged.split("\n")) {
62
+ if (f) files.add(f);
63
+ }
64
+ }
65
+ } catch {
66
+ // ignore
67
+ }
68
+
69
+ return [...files].sort();
70
+ }
71
+
72
+ // Files reported by `git status --porcelain` — staged, unstaged, and untracked
73
+ // (excluding ignored). Includes rename destinations rather than sources. Used
74
+ // by prime's auto-context-scope (slice 2) which mirrors V1_PLAN's "what the
75
+ // agent is about to work on" framing rather than `git diff HEAD~1`'s "what
76
+ // recently shipped."
77
+ export function getActiveFiles(cwd: string): string[] {
78
+ const files = new Set<string>();
79
+ try {
80
+ // `-uall` lists every untracked file individually instead of collapsing
81
+ // directories to a single `??` entry — the prime context-scope needs
82
+ // per-file granularity to match record `files`/`dir_anchors`.
83
+ const out = execFileSync("git", ["status", "--porcelain", "-uall"], {
84
+ cwd,
85
+ encoding: "utf-8",
86
+ stdio: ["pipe", "pipe", "pipe"],
87
+ });
88
+ for (const raw of out.split("\n")) {
89
+ if (!raw) continue;
90
+ // Porcelain v1 lines are `XY <path>` where XY is two status chars.
91
+ // Renames are `R old -> new`; we keep the destination only.
92
+ const path = raw.slice(3);
93
+ const arrow = path.indexOf(" -> ");
94
+ if (arrow >= 0) {
95
+ files.add(path.slice(arrow + 4));
96
+ } else if (path) {
97
+ files.add(path);
98
+ }
99
+ }
100
+ } catch {
101
+ // not a repo, git missing, or status failed — caller treats empty as
102
+ // "no signal" and falls back to unscoped output.
103
+ }
104
+ return [...files].sort();
105
+ }
106
+
107
+ export function fileMatchesAny(file: string, changedFiles: string[]): boolean {
108
+ return changedFiles.some(
109
+ (changed) => changed === file || changed.endsWith(file) || file.endsWith(changed),
110
+ );
111
+ }
112
+
113
+ export function filterByContext(
114
+ records: ExpertiseRecord[],
115
+ changedFiles: string[],
116
+ ): ExpertiseRecord[] {
117
+ return records.filter((r) => {
118
+ const hasFiles = "files" in r && Array.isArray(r.files) && r.files.length > 0;
119
+ const hasDirAnchors = Array.isArray(r.dir_anchors) && r.dir_anchors.length > 0;
120
+
121
+ // No anchors at all → always relevant (conventions, decisions, failures,
122
+ // guides; or named records with no scoping declared).
123
+ if (!hasFiles && !hasDirAnchors) return true;
124
+
125
+ if (hasFiles && r.files !== undefined && r.files.some((f) => fileMatchesAny(f, changedFiles))) {
126
+ return true;
127
+ }
128
+ if (hasDirAnchors && r.dir_anchors !== undefined) {
129
+ for (const dir of r.dir_anchors) {
130
+ if (changedFiles.some((cf) => fileLivesUnderDir(cf, dir))) return true;
131
+ }
132
+ }
133
+ return false;
134
+ });
135
+ }
136
+
137
+ export type ActiveTrackers = Partial<Record<TrackerName, string>>;
138
+
139
+ export interface ActiveContext {
140
+ changedFiles: string[];
141
+ trackers: ActiveTrackers;
142
+ }
143
+
144
+ export function activeContextHasSignal(ctx: ActiveContext): boolean {
145
+ if (ctx.changedFiles.length > 0) return true;
146
+ for (const v of Object.values(ctx.trackers)) {
147
+ if (typeof v === "string" && v !== "") return true;
148
+ }
149
+ return false;
150
+ }
151
+
152
+ // Slice-2 auto-scope: union of filterByContext + evidence-tracker match. A
153
+ // record with no file/dir anchors AND no tracker anchors is treated as
154
+ // universal (applies everywhere); anchored records require a match on at
155
+ // least one signal.
156
+ export function filterByActiveContext(
157
+ records: ExpertiseRecord[],
158
+ ctx: ActiveContext,
159
+ ): ExpertiseRecord[] {
160
+ const { changedFiles, trackers } = ctx;
161
+ return records.filter((r) => {
162
+ const hasFiles = "files" in r && Array.isArray(r.files) && r.files.length > 0;
163
+ const hasDirAnchors = Array.isArray(r.dir_anchors) && r.dir_anchors.length > 0;
164
+ const ev = r.evidence;
165
+ const sproutMatch = !!(ev?.sprout && trackers.sprout && ev.sprout === trackers.sprout);
166
+ const ghMatch = !!(ev?.gh && trackers.gh && ev.gh === trackers.gh);
167
+ const linearMatch = !!(ev?.linear && trackers.linear && ev.linear === trackers.linear);
168
+ const beadMatch = !!(ev?.bead && trackers.bead && ev.bead === trackers.bead);
169
+ if (sproutMatch || ghMatch || linearMatch || beadMatch) return true;
170
+
171
+ if (!hasFiles && !hasDirAnchors) return true;
172
+
173
+ if (hasFiles && r.files !== undefined && r.files.some((f) => fileMatchesAny(f, changedFiles))) {
174
+ return true;
175
+ }
176
+ if (hasDirAnchors && r.dir_anchors !== undefined) {
177
+ for (const dir of r.dir_anchors) {
178
+ if (changedFiles.some((cf) => fileLivesUnderDir(cf, dir))) return true;
179
+ }
180
+ }
181
+ return false;
182
+ });
183
+ }
@@ -0,0 +1,299 @@
1
+ import { spawn } from "node:child_process";
2
+ import { log } from "../log.ts";
3
+ import {
4
+ DEFAULT_HOOK_TIMEOUT_MS,
5
+ HOOK_EVENTS,
6
+ type HookEvent,
7
+ type LoamConfig,
8
+ } from "../schemas/config.ts";
9
+ import { readConfig } from "./config.ts";
10
+
11
+ // Events whose hooks may mutate the payload via stdout JSON. `pre-record` and
12
+ // `pre-prime` carry the original R-02 mutable payloads; `pre-compact` lets a
13
+ // hook substitute the merged replacement record (loam-184b). `pre-prune` is
14
+ // block-or-allow only (a hook can prevent the prune but not reshape the
15
+ // candidate set).
16
+ const MUTABLE_EVENTS: ReadonlySet<HookEvent> = new Set<HookEvent>([
17
+ "pre-record",
18
+ "pre-prime",
19
+ "pre-compact",
20
+ ]);
21
+
22
+ const BLOCKING_EVENTS: ReadonlySet<HookEvent> = new Set<HookEvent>([
23
+ "pre-record",
24
+ "pre-prime",
25
+ "pre-prune",
26
+ "pre-compact",
27
+ ]);
28
+
29
+ export interface HookExecution {
30
+ command: string;
31
+ exitCode: number;
32
+ stderr: string;
33
+ stdout: string;
34
+ durationMs: number;
35
+ timedOut: boolean;
36
+ }
37
+
38
+ export interface HookResult<T> {
39
+ // Whether at least one hook script was executed.
40
+ ranAny: boolean;
41
+ // True iff a `pre-*` hook exited non-zero (or timed out). The caller MUST
42
+ // abort the parent command when blocked is true.
43
+ blocked: boolean;
44
+ // Human-readable reason when blocked (script command + exit code/stderr).
45
+ blockReason?: string;
46
+ // Warnings collected from post-* failures or pre-* hooks whose stdout
47
+ // could not be parsed as JSON. Callers should surface these to the user.
48
+ warnings: string[];
49
+ // Possibly-mutated payload. For non-mutable events or when no hook altered
50
+ // stdout, this equals the input payload.
51
+ payload: T;
52
+ // Per-script execution diagnostics (in invocation order).
53
+ executions: HookExecution[];
54
+ }
55
+
56
+ interface HookSettings {
57
+ timeoutMs: number;
58
+ }
59
+
60
+ function resolveSettings(config: LoamConfig): HookSettings {
61
+ const raw = config.hook_settings?.timeout_ms;
62
+ const timeoutMs = typeof raw === "number" && raw > 0 ? raw : DEFAULT_HOOK_TIMEOUT_MS;
63
+ return { timeoutMs };
64
+ }
65
+
66
+ function getHookList(config: LoamConfig, event: HookEvent): string[] {
67
+ const list = config.hooks?.[event] ?? [];
68
+ return Array.isArray(list)
69
+ ? list.filter((s) => typeof s === "string" && s.trim().length > 0)
70
+ : [];
71
+ }
72
+
73
+ interface RunOneResult {
74
+ exitCode: number;
75
+ stdout: string;
76
+ stderr: string;
77
+ durationMs: number;
78
+ timedOut: boolean;
79
+ }
80
+
81
+ async function runOne(
82
+ command: string,
83
+ stdinJson: string,
84
+ cwd: string,
85
+ timeoutMs: number,
86
+ ): Promise<RunOneResult> {
87
+ const start = Date.now();
88
+ // Run the hook in its own process group (POSIX session) so a timeout can
89
+ // SIGKILL every descendant — not just the immediate `sh`. Bun.spawn's
90
+ // `timeout` option only signals the direct child; a forked exec like
91
+ // `sleep 30 & wait` orphans the sleep, which keeps stdout/stderr open and
92
+ // hangs `Promise.all([Response.text(), Response.text(), proc.exited])`
93
+ // indefinitely (R-02 stress finding). Node's child_process supports
94
+ // `detached: true` (calls setsid on POSIX, putting the child in a new pgid
95
+ // equal to its pid), and `process.kill(-pid, "SIGKILL")` reaches the whole
96
+ // group. Bun re-exports node:child_process so this works on the Bun runtime.
97
+ const child = spawn("sh", ["-c", command], {
98
+ cwd,
99
+ stdio: ["pipe", "pipe", "pipe"],
100
+ env: { ...process.env, LOAM_HOOK: "1" },
101
+ detached: true,
102
+ });
103
+
104
+ let timedOut = false;
105
+ const timer = setTimeout(() => {
106
+ timedOut = true;
107
+ if (typeof child.pid === "number") {
108
+ try {
109
+ // Negative pid → process group. SIGKILL is unblockable so this
110
+ // reaches every descendant even if the hook script ignores SIGTERM.
111
+ process.kill(-child.pid, "SIGKILL");
112
+ } catch {
113
+ // ESRCH if the group already died; ignore.
114
+ }
115
+ }
116
+ }, timeoutMs);
117
+
118
+ let stdout = "";
119
+ let stderr = "";
120
+ child.stdout?.on("data", (b: Buffer) => {
121
+ stdout += b.toString();
122
+ });
123
+ child.stderr?.on("data", (b: Buffer) => {
124
+ stderr += b.toString();
125
+ });
126
+
127
+ if (child.stdin) {
128
+ try {
129
+ child.stdin.write(stdinJson);
130
+ child.stdin.end();
131
+ } catch {
132
+ // Script may have closed stdin early — not fatal.
133
+ }
134
+ }
135
+
136
+ const exitCode = await new Promise<number>((resolve) => {
137
+ child.on("close", (code: number | null) => resolve(code ?? 0));
138
+ child.on("error", () => resolve(1));
139
+ });
140
+ clearTimeout(timer);
141
+
142
+ return {
143
+ exitCode: timedOut ? 124 : exitCode,
144
+ stdout,
145
+ stderr,
146
+ durationMs: Date.now() - start,
147
+ timedOut,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Run all hook scripts registered for `event` in declaration order. Each script
153
+ * receives the current payload as JSON on stdin. For mutable events
154
+ * (`pre-record`, `pre-prime`), a script may print modified JSON on stdout; that
155
+ * JSON becomes the input to the next script and the final `result.payload`
156
+ * returned to the caller. `pre-prune` is block-or-allow only — its stdout is
157
+ * ignored even though it is a `pre-*` event, so a hook cannot reshape the
158
+ * candidate set.
159
+ *
160
+ * Semantics summary:
161
+ * - `pre-*` non-zero exit (or timeout) → blocks: subsequent scripts skipped,
162
+ * caller must abort. `result.blocked` is true with a reason string.
163
+ * - `post-*` non-zero exit → warning only, all scripts still run.
164
+ * - Empty / whitespace stdout from a mutable hook → payload unchanged.
165
+ * - Non-JSON stdout from a mutable hook → payload unchanged + warning.
166
+ * - Stderr from any script is forwarded to the user's stderr unless captured.
167
+ */
168
+ export async function runHooks<T>(
169
+ event: HookEvent,
170
+ payload: T,
171
+ opts: { cwd?: string; config?: LoamConfig; forwardStderr?: boolean } = {},
172
+ ): Promise<HookResult<T>> {
173
+ if (!HOOK_EVENTS.includes(event)) {
174
+ throw new Error(`Unknown hook event: "${event}".`);
175
+ }
176
+
177
+ const cwd = opts.cwd ?? process.cwd();
178
+
179
+ let config: LoamConfig;
180
+ try {
181
+ config = opts.config ?? (await readConfig(cwd));
182
+ } catch {
183
+ // No config available (e.g., before `lm init`). Treat as no hooks.
184
+ return {
185
+ ranAny: false,
186
+ blocked: false,
187
+ warnings: [],
188
+ payload,
189
+ executions: [],
190
+ };
191
+ }
192
+
193
+ const scripts = getHookList(config, event);
194
+ if (scripts.length === 0) {
195
+ return {
196
+ ranAny: false,
197
+ blocked: false,
198
+ warnings: [],
199
+ payload,
200
+ executions: [],
201
+ };
202
+ }
203
+
204
+ const settings = resolveSettings(config);
205
+ const isMutable = MUTABLE_EVENTS.has(event);
206
+ const isBlocking = BLOCKING_EVENTS.has(event);
207
+ const forwardStderr = opts.forwardStderr !== false;
208
+
209
+ const warnings: string[] = [];
210
+ const executions: HookExecution[] = [];
211
+ let currentPayload: T = payload;
212
+
213
+ for (const command of scripts) {
214
+ const stdinJson = JSON.stringify({ event, payload: currentPayload });
215
+ const res = await runOne(command, stdinJson, cwd, settings.timeoutMs);
216
+
217
+ executions.push({
218
+ command,
219
+ exitCode: res.exitCode,
220
+ stderr: res.stderr,
221
+ stdout: res.stdout,
222
+ durationMs: res.durationMs,
223
+ timedOut: res.timedOut,
224
+ });
225
+
226
+ // Diagnostic trace of hook execution. Silent at the default `info`
227
+ // level; surfaces under LOAM_DEBUG. Runs outside the expertise-file
228
+ // write lock (record.ts runs pre-/post-hooks around the locked write),
229
+ // so logging here can never contend with a concurrent writer.
230
+ log.debug(
231
+ {
232
+ event,
233
+ command,
234
+ exitCode: res.exitCode,
235
+ durationMs: res.durationMs,
236
+ timedOut: res.timedOut,
237
+ },
238
+ "hook executed",
239
+ );
240
+
241
+ if (forwardStderr && res.stderr.length > 0) {
242
+ process.stderr.write(res.stderr);
243
+ if (!res.stderr.endsWith("\n")) process.stderr.write("\n");
244
+ }
245
+
246
+ if (res.exitCode !== 0) {
247
+ const reason = res.timedOut
248
+ ? `hook \`${command}\` timed out after ${settings.timeoutMs}ms`
249
+ : `hook \`${command}\` exited with code ${res.exitCode}`;
250
+ log.warn(
251
+ { event, command, exitCode: res.exitCode, timedOut: res.timedOut, blocking: isBlocking },
252
+ reason,
253
+ );
254
+ if (isBlocking) {
255
+ return {
256
+ ranAny: true,
257
+ blocked: true,
258
+ blockReason: reason,
259
+ warnings,
260
+ payload: currentPayload,
261
+ executions,
262
+ };
263
+ }
264
+ warnings.push(reason);
265
+ continue;
266
+ }
267
+
268
+ if (isMutable) {
269
+ const trimmed = res.stdout.trim();
270
+ if (trimmed.length === 0) continue;
271
+ try {
272
+ const parsed = JSON.parse(trimmed) as { payload?: T } | T;
273
+ if (
274
+ parsed !== null &&
275
+ typeof parsed === "object" &&
276
+ "payload" in (parsed as Record<string, unknown>)
277
+ ) {
278
+ currentPayload = (parsed as { payload: T }).payload;
279
+ } else {
280
+ currentPayload = parsed as T;
281
+ }
282
+ } catch (err) {
283
+ warnings.push(
284
+ `hook \`${command}\` printed non-JSON on stdout (mutation ignored): ${
285
+ err instanceof Error ? err.message : String(err)
286
+ }`,
287
+ );
288
+ }
289
+ }
290
+ }
291
+
292
+ return {
293
+ ranAny: true,
294
+ blocked: false,
295
+ warnings,
296
+ payload: currentPayload,
297
+ executions,
298
+ };
299
+ }
@@ -0,0 +1,52 @@
1
+ export {
2
+ getConfigPath,
3
+ getExpertiseDir,
4
+ getExpertisePath,
5
+ getLoamDir,
6
+ initLoamDir,
7
+ readConfig,
8
+ writeConfig,
9
+ } from "./config.ts";
10
+
11
+ export {
12
+ appendRecord,
13
+ countRecords,
14
+ createExpertiseFile,
15
+ filterByType,
16
+ generateRecordId,
17
+ getFileModTime,
18
+ readExpertiseFile,
19
+ } from "./expertise.ts";
20
+
21
+ export {
22
+ formatDomainExpertise,
23
+ formatPrimeOutput,
24
+ formatStatusOutput,
25
+ formatTimeAgo,
26
+ getRecordSummary,
27
+ } from "./format.ts";
28
+ export {
29
+ fileMatchesAny,
30
+ filterByContext,
31
+ getChangedFiles,
32
+ isGitRepo,
33
+ } from "./git.ts";
34
+ export {
35
+ outputJson,
36
+ outputJsonError,
37
+ } from "./json-output.ts";
38
+
39
+ export {
40
+ hasMarkerSection,
41
+ MARKER_END,
42
+ MARKER_START,
43
+ removeMarkerSection,
44
+ replaceMarkerSection,
45
+ wrapInMarkers,
46
+ } from "./markers.ts";
47
+
48
+ export {
49
+ compareSemver,
50
+ getCurrentVersion,
51
+ getLatestVersion,
52
+ } from "./version.ts";
@@ -0,0 +1,13 @@
1
+ export interface JsonResult {
2
+ success: boolean;
3
+ command: string;
4
+ [key: string]: unknown;
5
+ }
6
+
7
+ export function outputJson(result: JsonResult): void {
8
+ console.log(JSON.stringify(result, null, 2));
9
+ }
10
+
11
+ export function outputJsonError(command: string, error: string): void {
12
+ console.error(JSON.stringify({ success: false, command, error }, null, 2));
13
+ }
@@ -0,0 +1,76 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, unlink } from "node:fs/promises";
3
+
4
+ const LOCK_STALE_MS = 30_000; // 30 seconds
5
+ const LOCK_RETRY_INTERVAL_MS = 50;
6
+ const LOCK_TIMEOUT_MS = 5_000; // 5 seconds
7
+
8
+ /**
9
+ * Advisory file-level lock using O_CREAT | O_EXCL.
10
+ * Wraps an async function with lock acquisition and guaranteed cleanup.
11
+ */
12
+ export async function withFileLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
13
+ const lockPath = `${filePath}.lock`;
14
+ await acquireLock(lockPath);
15
+ try {
16
+ return await fn();
17
+ } finally {
18
+ await releaseLock(lockPath);
19
+ }
20
+ }
21
+
22
+ async function acquireLock(lockPath: string): Promise<void> {
23
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
24
+
25
+ while (true) {
26
+ try {
27
+ const fd = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
28
+ await fd.close();
29
+ return;
30
+ } catch (err) {
31
+ if ((err as NodeJS.ErrnoException).code !== "EEXIST") {
32
+ throw err;
33
+ }
34
+
35
+ // Lock file exists — check if it's stale
36
+ if (await isStaleLock(lockPath)) {
37
+ try {
38
+ await unlink(lockPath);
39
+ } catch {
40
+ // Another process may have already removed it
41
+ }
42
+ continue;
43
+ }
44
+
45
+ if (Date.now() >= deadline) {
46
+ throw new Error(
47
+ `Timed out waiting for lock on ${lockPath}. If no other loam process is running, delete the lock file manually.`,
48
+ );
49
+ }
50
+
51
+ await sleep(LOCK_RETRY_INTERVAL_MS);
52
+ }
53
+ }
54
+ }
55
+
56
+ async function isStaleLock(lockPath: string): Promise<boolean> {
57
+ try {
58
+ const stats = await lstat(lockPath);
59
+ return Date.now() - stats.mtimeMs > LOCK_STALE_MS;
60
+ } catch {
61
+ // Lock file disappeared between check — not stale, just gone
62
+ return false;
63
+ }
64
+ }
65
+
66
+ async function releaseLock(lockPath: string): Promise<void> {
67
+ try {
68
+ await unlink(lockPath);
69
+ } catch {
70
+ // Lock file already gone — acceptable
71
+ }
72
+ }
73
+
74
+ function sleep(ms: number): Promise<void> {
75
+ return new Promise((resolve) => setTimeout(resolve, ms));
76
+ }