@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,24 @@
1
+ import { randomBytes } from "node:crypto";
2
+
3
+ export function generateId(prefix: string, existingIds: string[]): string {
4
+ const existingSet = new Set(existingIds);
5
+
6
+ for (let attempt = 0; attempt < 100; attempt++) {
7
+ const hex = randomBytes(2).toString("hex");
8
+ const id = `${prefix}-${hex}`;
9
+ if (!existingSet.has(id)) {
10
+ return id;
11
+ }
12
+ }
13
+
14
+ // Fallback to 8 hex chars after 100 collisions
15
+ for (let attempt = 0; attempt < 1000; attempt++) {
16
+ const hex = randomBytes(4).toString("hex");
17
+ const id = `${prefix}-${hex}`;
18
+ if (!existingSet.has(id)) {
19
+ return id;
20
+ }
21
+ }
22
+
23
+ throw new Error(`Failed to generate unique ID with prefix "${prefix}"`);
24
+ }
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env bun
2
+ import chalk from "chalk";
3
+ import { Command, Help } from "commander";
4
+ import { VERSION } from "../../version.ts";
5
+ import { errorOut, isJsonMode, jsonOut, palette, setQuiet } from "./output.ts";
6
+ import { ExitError } from "./types.ts";
7
+
8
+ export { VERSION };
9
+
10
+ const t0 = performance.now();
11
+
12
+ const rawArgs = process.argv.slice(2);
13
+
14
+ // Apply quiet mode early (before any output)
15
+ if (rawArgs.includes("--quiet") || rawArgs.includes("-q")) {
16
+ setQuiet(true);
17
+ }
18
+
19
+ // --version --json: rich metadata output (before Commander processes version flag)
20
+ if ((rawArgs.includes("-v") || rawArgs.includes("--version")) && rawArgs.includes("--json")) {
21
+ const platform = `${process.platform}-${process.arch}`;
22
+ jsonOut({ name: "@ag-eco/trellis-cli", version: VERSION, runtime: "bun", platform });
23
+ if (rawArgs.includes("--timing")) {
24
+ const elapsed = Math.round(performance.now() - t0);
25
+ process.stderr.write(`[timing] ${elapsed}ms\n`);
26
+ }
27
+ process.exit();
28
+ }
29
+
30
+ const program = new Command();
31
+ program
32
+ .name("tl")
33
+ .description("Prompt management & composition")
34
+ .version(VERSION, "-v, --version", "Show version")
35
+ .option("-q, --quiet", "Suppress non-error output")
36
+ .option("--verbose", "Extra diagnostic output")
37
+ .option("--timing", "Show command execution time")
38
+ .option("--json", "Output as JSON")
39
+ .addHelpCommand(false)
40
+ .configureHelp({
41
+ formatHelp(cmd: Command, helper: Help): string {
42
+ if (cmd.parent) {
43
+ return Help.prototype.formatHelp.call(helper, cmd, helper);
44
+ }
45
+ const header = `${palette.brand(chalk.bold("trellis"))} ${palette.muted(`v${VERSION}`)} — Prompt management & composition\n\nUsage: tl <command> [options]`;
46
+
47
+ const cmdLines: string[] = ["\nCommands:"];
48
+ for (const sub of cmd.commands) {
49
+ const name = sub.name();
50
+ const argStr = sub.registeredArguments
51
+ .map((a) => (a.required ? `<${a.name()}>` : `[${a.name()}]`))
52
+ .join(" ");
53
+ const rawEntry = argStr ? `${name} ${argStr}` : name;
54
+ const colored = argStr ? `${chalk.green(name)} ${chalk.dim(argStr)}` : chalk.green(name);
55
+ const pad = " ".repeat(Math.max(18 - rawEntry.length, 2));
56
+ cmdLines.push(` ${colored}${pad}${sub.description()}`);
57
+ }
58
+
59
+ const opts: [string, string][] = [
60
+ ["-h, --help", "Show help"],
61
+ ["-v, --version", "Show version"],
62
+ ["--json", "Output as JSON"],
63
+ ["-q, --quiet", "Suppress non-error output"],
64
+ ["--verbose", "Extra diagnostic output"],
65
+ ["--timing", "Show command execution time"],
66
+ ];
67
+ const optLines: string[] = ["\nOptions:"];
68
+ for (const [flag, desc] of opts) {
69
+ const pad = " ".repeat(Math.max(18 - flag.length, 2));
70
+ optLines.push(` ${chalk.dim(flag)}${pad}${desc}`);
71
+ }
72
+
73
+ const footer = `\nRun '${chalk.dim("tl")} <command> --help' for command-specific help.`;
74
+
75
+ return `${[header, ...cmdLines, ...optLines, footer].join("\n")}\n`;
76
+ },
77
+ });
78
+
79
+ const { registerInitCommand } = await import("./commands/init.ts");
80
+ const { registerShowCommand } = await import("./commands/show.ts");
81
+ const { registerListCommand } = await import("./commands/list.ts");
82
+ const { registerArchiveCommand } = await import("./commands/archive.ts");
83
+ const { registerHistoryCommand } = await import("./commands/history.ts");
84
+ const { registerTreeCommand } = await import("./commands/tree.ts");
85
+ const { registerStatsCommand } = await import("./commands/stats.ts");
86
+ const { registerSyncCommand } = await import("./commands/sync.ts");
87
+ const { registerDiffCommand } = await import("./commands/diff.ts");
88
+ const { registerRenderCommand } = await import("./commands/render.ts");
89
+ const { registerCreateCommand } = await import("./commands/create.ts");
90
+ const { registerUpdateCommand } = await import("./commands/update.ts");
91
+ const { registerEmitCommand } = await import("./commands/emit.ts");
92
+ const { registerSchemaCommand } = await import("./commands/schema.ts");
93
+ const { registerValidateCommand } = await import("./commands/validate.ts");
94
+ const { registerConfigCommand } = await import("./commands/config.ts");
95
+ const { registerImportCommand } = await import("./commands/import.ts");
96
+ const { registerPrimeCommand } = await import("./commands/prime.ts");
97
+ const { registerOnboardCommand } = await import("./commands/onboard.ts");
98
+ const { registerPinCommand } = await import("./commands/pin.ts");
99
+ const { registerDoctorCommand } = await import("./commands/doctor.ts");
100
+ const { registerUpgradeCommand } = await import("./commands/upgrade.ts");
101
+ const { registerCompletionsCommand } = await import("./commands/completions.ts");
102
+
103
+ registerInitCommand(program);
104
+ registerShowCommand(program);
105
+ registerListCommand(program);
106
+ registerArchiveCommand(program);
107
+ registerHistoryCommand(program);
108
+ registerTreeCommand(program);
109
+ registerStatsCommand(program);
110
+ registerSyncCommand(program);
111
+ registerDiffCommand(program);
112
+ registerRenderCommand(program);
113
+ registerCreateCommand(program);
114
+ registerUpdateCommand(program);
115
+ registerEmitCommand(program);
116
+ registerSchemaCommand(program);
117
+ registerValidateCommand(program);
118
+ registerConfigCommand(program);
119
+ registerImportCommand(program);
120
+ registerPrimeCommand(program);
121
+ registerOnboardCommand(program);
122
+ registerPinCommand(program); // registers both pin and unpin
123
+ registerDoctorCommand(program);
124
+ registerUpgradeCommand(program);
125
+ registerCompletionsCommand(program);
126
+
127
+ // --- Typo suggestions via Levenshtein distance ---
128
+
129
+ function editDistance(a: string, b: string): number {
130
+ const m = a.length;
131
+ const n = b.length;
132
+ const dp = new Array<number>((m + 1) * (n + 1)).fill(0);
133
+ const idx = (i: number, j: number) => i * (n + 1) + j;
134
+ for (let i = 0; i <= m; i++) dp[idx(i, 0)] = i;
135
+ for (let j = 0; j <= n; j++) dp[idx(0, j)] = j;
136
+ for (let i = 1; i <= m; i++) {
137
+ for (let j = 1; j <= n; j++) {
138
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
139
+ const del = (dp[idx(i - 1, j)] ?? 0) + 1;
140
+ const ins = (dp[idx(i, j - 1)] ?? 0) + 1;
141
+ const sub = (dp[idx(i - 1, j - 1)] ?? 0) + cost;
142
+ dp[idx(i, j)] = Math.min(del, ins, sub);
143
+ }
144
+ }
145
+ return dp[idx(m, n)] ?? 0;
146
+ }
147
+
148
+ function suggestCommand(input: string): string | undefined {
149
+ const commands = program.commands.map((c) => c.name());
150
+ let bestMatch: string | undefined;
151
+ let bestDist = 3; // Only suggest if distance <= 2
152
+ for (const cmd of commands) {
153
+ const dist = editDistance(input, cmd);
154
+ if (dist < bestDist) {
155
+ bestDist = dist;
156
+ bestMatch = cmd;
157
+ }
158
+ }
159
+ return bestMatch;
160
+ }
161
+
162
+ program.on("command:*", (operands) => {
163
+ const unknown = operands[0] ?? "";
164
+ const json = isJsonMode(rawArgs);
165
+ const suggestion = suggestCommand(unknown);
166
+ if (json) {
167
+ jsonOut({
168
+ success: false,
169
+ command: unknown,
170
+ error: `Unknown command: ${unknown}`,
171
+ suggestion: suggestion ?? undefined,
172
+ });
173
+ } else {
174
+ process.stderr.write(`Unknown command: ${unknown}\n`);
175
+ if (suggestion) {
176
+ process.stderr.write(`Did you mean '${suggestion}'?\n`);
177
+ }
178
+ process.stderr.write("Run 'tl --help' for usage.\n");
179
+ }
180
+ process.exitCode = 1;
181
+ });
182
+
183
+ program
184
+ .parseAsync(process.argv)
185
+ .then(() => {
186
+ if (program.opts().timing) {
187
+ const elapsed = Math.round(performance.now() - t0);
188
+ process.stderr.write(`[timing] ${elapsed}ms\n`);
189
+ }
190
+ })
191
+ .catch((err: unknown) => {
192
+ if (program.opts().timing) {
193
+ const elapsed = Math.round(performance.now() - t0);
194
+ process.stderr.write(`[timing] ${elapsed}ms\n`);
195
+ }
196
+ if (err instanceof ExitError) {
197
+ process.exitCode = err.exitCode;
198
+ return;
199
+ }
200
+ const msg = err instanceof Error ? err.message : String(err);
201
+ const command = process.argv[2] ?? "";
202
+ const json = isJsonMode(process.argv.slice(2));
203
+ if (json) {
204
+ jsonOut({ success: false, command, error: msg });
205
+ } else {
206
+ errorOut(`Error: ${msg}`);
207
+ }
208
+ process.exitCode = 1;
209
+ });
@@ -0,0 +1,28 @@
1
+ export const START_MARKER = "<!-- trellis:start -->";
2
+ export const END_MARKER = "<!-- trellis:end -->";
3
+
4
+ export const ONBOARD_VERSION = 2;
5
+ export const VERSION_MARKER = `<!-- trellis-onboard-v:${String(ONBOARD_VERSION)} -->`;
6
+
7
+ export function hasMarkerSection(content: string): boolean {
8
+ return content.includes(START_MARKER) && content.includes(END_MARKER);
9
+ }
10
+
11
+ export function detectStatus(content: string): "missing" | "current" | "outdated" {
12
+ if (!hasMarkerSection(content)) return "missing";
13
+ if (content.includes(VERSION_MARKER)) return "current";
14
+ return "outdated";
15
+ }
16
+
17
+ export function replaceMarkerSection(content: string, newSection: string): string | null {
18
+ const startIdx = content.indexOf(START_MARKER);
19
+ const endIdx = content.indexOf(END_MARKER);
20
+ if (startIdx === -1 || endIdx === -1) return null;
21
+ const before = content.slice(0, startIdx);
22
+ const after = content.slice(endIdx + END_MARKER.length);
23
+ return before + wrapInMarkers(newSection) + after;
24
+ }
25
+
26
+ export function wrapInMarkers(section: string): string {
27
+ return `${START_MARKER}\n${section}\n${END_MARKER}`;
28
+ }
@@ -0,0 +1,84 @@
1
+ import chalk from "chalk";
2
+
3
+ let _quiet = false;
4
+
5
+ export function setQuiet(v: boolean): void {
6
+ _quiet = v;
7
+ }
8
+
9
+ export function isQuiet(): boolean {
10
+ return _quiet;
11
+ }
12
+
13
+ export function jsonOut(data: unknown): void {
14
+ if (_quiet) {
15
+ const isError =
16
+ data != null &&
17
+ typeof data === "object" &&
18
+ "success" in data &&
19
+ (data as Record<string, unknown>).success === false;
20
+ if (!isError) return;
21
+ }
22
+ console.log(JSON.stringify(data, null, 2));
23
+ }
24
+
25
+ export function humanOut(text: string): void {
26
+ if (_quiet) return;
27
+ console.log(text);
28
+ }
29
+
30
+ export function errorOut(msg: string): void {
31
+ console.error(msg);
32
+ }
33
+
34
+ export function isJsonMode(args: string[]): boolean {
35
+ return args.includes("--json");
36
+ }
37
+
38
+ // Brand palette — chalk instances (supports chaining e.g. palette.brand.bold(...))
39
+ // chalk handles NO_COLOR and TTY detection automatically
40
+ export const palette = {
41
+ brand: chalk.rgb(56, 142, 60), // Trellis deep green
42
+ accent: chalk.rgb(255, 183, 77), // amber — IDs and accents
43
+ muted: chalk.rgb(120, 120, 110), // stone gray — metadata
44
+ };
45
+
46
+ // Color helpers
47
+ export const c = {
48
+ bold: (s: string) => chalk.bold(s),
49
+ dim: (s: string) => chalk.dim(s),
50
+ green: (s: string) => palette.brand(s),
51
+ red: (s: string) => chalk.red(s),
52
+ yellow: (s: string) => palette.accent(s),
53
+ cyan: (s: string) => chalk.cyan(s),
54
+ blue: (s: string) => chalk.blue(s),
55
+ };
56
+
57
+ // Status icons: Set D (minimal, maximum terminal compatibility)
58
+ // Use these for list status indicators, not message prefixes
59
+ export const icons = {
60
+ pending: chalk.green("-"), // open / pending
61
+ active: chalk.cyan(">"), // in_progress / active
62
+ done: chalk.dim("x"), // closed / done
63
+ blocked: chalk.yellow("!"), // blocked / warning
64
+ };
65
+
66
+ // Message format helpers per visual-spec.md
67
+ export const fmt = {
68
+ // brand bold ✓ + brand message text
69
+ success: (msg: string) => `${palette.brand.bold("✓")} ${palette.brand(msg)}`,
70
+ // highlight an ID or reference in accent (amber)
71
+ id: (id: string) => palette.accent(id),
72
+ // yellow bold ! + yellow message + optional dim hint
73
+ warning: (msg: string, hint?: string) =>
74
+ hint
75
+ ? `${chalk.yellow.bold("!")} ${chalk.yellow(msg)} ${chalk.dim(hint)}`
76
+ : `${chalk.yellow.bold("!")} ${chalk.yellow(msg)}`,
77
+ // red bold ✗ + red message + optional dim hint
78
+ error: (msg: string, hint?: string) =>
79
+ hint
80
+ ? `${chalk.red.bold("✗")} ${chalk.red(msg)} ${chalk.dim(hint)}`
81
+ : `${chalk.red.bold("✗")} ${chalk.red(msg)}`,
82
+ // dim indented info/hint text
83
+ info: (msg: string) => chalk.dim(` ${msg}`),
84
+ };
@@ -0,0 +1,212 @@
1
+ import type { LoamBlock, Prompt, Section } from "./types.ts";
2
+ import { MAX_INHERIT_DEPTH } from "./types.ts";
3
+
4
+ export interface RenderResult {
5
+ sections: Section[];
6
+ frontmatter: Record<string, unknown>;
7
+ resolvedFrom: string[];
8
+ version: number;
9
+ loam?: LoamBlock;
10
+ }
11
+
12
+ /**
13
+ * Resolve a prompt's full section list by walking the inheritance chain
14
+ * and applying mixins. Resolution order:
15
+ * 1. Resolve extends chain (parent first)
16
+ * 2. For each mixin (left-to-right), resolve it fully
17
+ * 3. Merge: base → mixin₁ → mixin₂ → … → focal prompt
18
+ * Later entries override earlier on section name conflicts.
19
+ */
20
+ export function resolvePrompt(name: string, prompts: Prompt[], version?: number): RenderResult {
21
+ const visited: string[] = [];
22
+ return resolveInner(name, prompts, version, visited);
23
+ }
24
+
25
+ function resolveInner(
26
+ name: string,
27
+ prompts: Prompt[],
28
+ version: number | undefined,
29
+ visited: string[],
30
+ ): RenderResult {
31
+ if (visited.includes(name)) {
32
+ throw new Error(`Circular inheritance: ${[...visited, name].join(" → ")}`);
33
+ }
34
+ if (visited.length >= MAX_INHERIT_DEPTH) {
35
+ throw new Error(
36
+ `Inheritance depth limit (${MAX_INHERIT_DEPTH}) exceeded at "${name}". Chain: ${visited.join(" → ")}`,
37
+ );
38
+ }
39
+
40
+ // Find the prompt
41
+ const prompt = findPrompt(prompts, name, version);
42
+ if (!prompt) {
43
+ const versionStr = version !== undefined ? `@${version}` : "";
44
+ throw new Error(`Prompt "${name}${versionStr}" not found`);
45
+ }
46
+
47
+ visited.push(name);
48
+
49
+ // No parent and no mixins — return own sections (excluding empty-body removals)
50
+ if (!prompt.extends && (!prompt.mixins || prompt.mixins.length === 0)) {
51
+ const sections = prompt.sections.filter((s) => s.body !== "");
52
+ return withLoam(
53
+ {
54
+ sections,
55
+ frontmatter: prompt.frontmatter ?? {},
56
+ resolvedFrom: [name],
57
+ version: prompt.version,
58
+ },
59
+ prompt.loam,
60
+ );
61
+ }
62
+
63
+ // Start with parent chain if exists
64
+ let baseSections: Section[] = [];
65
+ let baseFrontmatter: Record<string, unknown> = {};
66
+ let baseResolvedFrom: string[] = [];
67
+ let baseLoam: LoamBlock | undefined;
68
+ const mixinLoames: (LoamBlock | undefined)[] = [];
69
+
70
+ if (prompt.extends) {
71
+ const parentResult = resolveInner(prompt.extends, prompts, undefined, visited);
72
+ baseSections = parentResult.sections;
73
+ baseFrontmatter = parentResult.frontmatter;
74
+ baseResolvedFrom = parentResult.resolvedFrom;
75
+ baseLoam = parentResult.loam;
76
+ }
77
+
78
+ // Apply each mixin left-to-right on top of the base
79
+ if (prompt.mixins && prompt.mixins.length > 0) {
80
+ for (const mixinName of prompt.mixins) {
81
+ // Each mixin resolves with its own visited-branch to allow
82
+ // the same ancestor to appear via extends AND a mixin (diamond).
83
+ // But we must still detect cycles involving the focal prompt.
84
+ const mixinVisited = [...visited];
85
+ const mixinResult = resolveInner(mixinName, prompts, undefined, mixinVisited);
86
+ baseSections = mergeSections(baseSections, mixinResult.sections);
87
+ baseFrontmatter = { ...baseFrontmatter, ...mixinResult.frontmatter };
88
+ baseResolvedFrom = [...baseResolvedFrom, ...mixinResult.resolvedFrom];
89
+ mixinLoames.push(mixinResult.loam);
90
+ }
91
+ }
92
+
93
+ // Finally apply the focal prompt's own sections on top
94
+ const merged = mergeSections(baseSections, prompt.sections);
95
+
96
+ // Resolve loam with override-vs-merge semantics gated by extends_loam.
97
+ // extends_loam=true: union parent + each mixin's resolved loam + focal.loam
98
+ // (domains/files unioned, budget/on_empty last-wins).
99
+ // extends_loam=false (default): focal.loam wholesale overrides — no inheritance,
100
+ // not even from mixins. If focal has no loam block, result is undefined.
101
+ let resolvedLoam: LoamBlock | undefined;
102
+ if (prompt.extends_loam) {
103
+ resolvedLoam = baseLoam;
104
+ for (const m of mixinLoames) {
105
+ resolvedLoam = mergeLoam(resolvedLoam, m);
106
+ }
107
+ resolvedLoam = mergeLoam(resolvedLoam, prompt.loam);
108
+ } else {
109
+ resolvedLoam = prompt.loam;
110
+ }
111
+
112
+ return withLoam(
113
+ {
114
+ sections: merged,
115
+ frontmatter: { ...baseFrontmatter, ...(prompt.frontmatter ?? {}) },
116
+ resolvedFrom: [...baseResolvedFrom, name],
117
+ version: prompt.version,
118
+ },
119
+ resolvedLoam,
120
+ );
121
+ }
122
+
123
+ function withLoam(result: RenderResult, loam: LoamBlock | undefined): RenderResult {
124
+ if (loam === undefined) return result;
125
+ return { ...result, loam };
126
+ }
127
+
128
+ function mergeLoam(
129
+ base: LoamBlock | undefined,
130
+ overlay: LoamBlock | undefined,
131
+ ): LoamBlock | undefined {
132
+ if (base === undefined) return overlay;
133
+ if (overlay === undefined) return base;
134
+
135
+ const merged: LoamBlock = {};
136
+
137
+ const baseDomains = base.prime?.domains;
138
+ const overlayDomains = overlay.prime?.domains;
139
+ const baseFiles = base.prime?.files;
140
+ const overlayFiles = overlay.prime?.files;
141
+ const domains =
142
+ baseDomains || overlayDomains ? unionPreserveOrder(baseDomains, overlayDomains) : undefined;
143
+ const files = baseFiles || overlayFiles ? unionPreserveOrder(baseFiles, overlayFiles) : undefined;
144
+ if (domains !== undefined || files !== undefined) {
145
+ merged.prime = {};
146
+ if (domains !== undefined) merged.prime.domains = domains;
147
+ if (files !== undefined) merged.prime.files = files;
148
+ }
149
+
150
+ const budget = overlay.budget !== undefined ? overlay.budget : base.budget;
151
+ if (budget !== undefined) merged.budget = budget;
152
+
153
+ const on_empty = overlay.on_empty !== undefined ? overlay.on_empty : base.on_empty;
154
+ if (on_empty !== undefined) merged.on_empty = on_empty;
155
+
156
+ return merged;
157
+ }
158
+
159
+ function unionPreserveOrder(a: string[] | undefined, b: string[] | undefined): string[] {
160
+ const seen = new Set<string>();
161
+ const out: string[] = [];
162
+ for (const item of a ?? []) {
163
+ if (!seen.has(item)) {
164
+ seen.add(item);
165
+ out.push(item);
166
+ }
167
+ }
168
+ for (const item of b ?? []) {
169
+ if (!seen.has(item)) {
170
+ seen.add(item);
171
+ out.push(item);
172
+ }
173
+ }
174
+ return out;
175
+ }
176
+
177
+ function findPrompt(prompts: Prompt[], name: string, version?: number): Prompt | undefined {
178
+ if (version !== undefined) {
179
+ return prompts.find((p) => p.name === name && p.version === version);
180
+ }
181
+ // Get latest version for this name
182
+ const candidates = prompts.filter((p) => p.name === name);
183
+ if (candidates.length === 0) return undefined;
184
+ return candidates.reduce((best, p) => (p.version > best.version ? p : best));
185
+ }
186
+
187
+ function mergeSections(parentSections: Section[], childSections: Section[]): Section[] {
188
+ // Start with parent sections
189
+ const result: Section[] = [...parentSections];
190
+
191
+ for (const childSection of childSections) {
192
+ const parentIdx = result.findIndex((s) => s.name === childSection.name);
193
+
194
+ if (childSection.body === "") {
195
+ // Empty body = remove the section
196
+ if (parentIdx !== -1) {
197
+ result.splice(parentIdx, 1);
198
+ }
199
+ continue;
200
+ }
201
+
202
+ if (parentIdx !== -1) {
203
+ // Override parent section
204
+ result[parentIdx] = childSection;
205
+ } else {
206
+ // Append new section
207
+ result.push(childSection);
208
+ }
209
+ }
210
+
211
+ return result;
212
+ }
@@ -0,0 +1,144 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { closeSync, constants, openSync, renameSync, statSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { LOCK_RETRY_MS, LOCK_STALE_MS, LOCK_TIMEOUT_MS } from "./types.ts";
5
+
6
+ function lockPath(filePath: string): string {
7
+ return `${filePath}.lock`;
8
+ }
9
+
10
+ export async function acquireLock(filePath: string): Promise<void> {
11
+ const lock = lockPath(filePath);
12
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
13
+
14
+ while (Date.now() < deadline) {
15
+ try {
16
+ // O_CREAT | O_EXCL — atomic, fails if exists
17
+ const fd = openSync(lock, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
18
+ closeSync(fd);
19
+ return;
20
+ } catch (err: unknown) {
21
+ if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
22
+
23
+ // Check if stale
24
+ try {
25
+ const stat = statSync(lock);
26
+ const age = Date.now() - stat.mtimeMs;
27
+ if (age > LOCK_STALE_MS) {
28
+ unlinkSync(lock);
29
+ continue;
30
+ }
31
+ } catch {
32
+ // Lock was removed between our check and stat — retry
33
+ continue;
34
+ }
35
+
36
+ await Bun.sleep(LOCK_RETRY_MS);
37
+ }
38
+ }
39
+
40
+ throw new Error(`Timeout acquiring lock on ${filePath} after ${LOCK_TIMEOUT_MS}ms`);
41
+ }
42
+
43
+ export function releaseLock(filePath: string): void {
44
+ try {
45
+ unlinkSync(lockPath(filePath));
46
+ } catch {
47
+ // Best-effort release
48
+ }
49
+ }
50
+
51
+ export async function readJsonl<T>(filePath: string): Promise<T[]> {
52
+ try {
53
+ const text = await Bun.file(filePath).text();
54
+ const records: T[] = [];
55
+
56
+ for (const line of text.split("\n")) {
57
+ const trimmed = line.trim();
58
+ if (!trimmed) continue;
59
+ try {
60
+ records.push(JSON.parse(trimmed) as T);
61
+ } catch {
62
+ // Skip malformed lines
63
+ }
64
+ }
65
+
66
+ return records;
67
+ } catch {
68
+ return [];
69
+ }
70
+ }
71
+
72
+ export async function writeJsonl<T>(filePath: string, records: T[]): Promise<void> {
73
+ const dir = filePath.substring(0, filePath.lastIndexOf("/"));
74
+ const tmp = join(dir, `.jsonl.tmp.${randomBytes(4).toString("hex")}`);
75
+
76
+ const lines = records.map((r) => JSON.stringify(r)).join("\n");
77
+ const content = lines ? `${lines}\n` : "";
78
+
79
+ await Bun.write(tmp, content);
80
+ renameSync(tmp, filePath);
81
+ }
82
+
83
+ export async function appendJsonl<T>(filePath: string, record: T): Promise<void> {
84
+ const line = `${JSON.stringify(record)}\n`;
85
+
86
+ // Check if file exists and has content
87
+ let existing = "";
88
+ try {
89
+ existing = await Bun.file(filePath).text();
90
+ } catch {
91
+ existing = "";
92
+ }
93
+
94
+ const content = existing ? existing + line : line;
95
+
96
+ const dir = filePath.substring(0, filePath.lastIndexOf("/"));
97
+ const tmp = join(dir, `.jsonl.tmp.${randomBytes(4).toString("hex")}`);
98
+
99
+ await Bun.write(tmp, content);
100
+ renameSync(tmp, filePath);
101
+ }
102
+
103
+ /**
104
+ * Dedup records by ID + version — last occurrence wins.
105
+ * For getting current state, return only highest version per ID.
106
+ */
107
+ export function dedupById<T extends { id: string; version: number }>(records: T[]): T[] {
108
+ const map = new Map<string, T>();
109
+ for (const record of records) {
110
+ const existing = map.get(record.id);
111
+ if (!existing || record.version >= existing.version) {
112
+ map.set(record.id, record);
113
+ }
114
+ }
115
+ return Array.from(map.values());
116
+ }
117
+
118
+ /**
119
+ * Dedup records by ID — last occurrence wins (no version required).
120
+ * Use this for record types without version fields (e.g., schemas).
121
+ */
122
+ export function dedupByIdLast<T extends { id: string }>(records: T[]): T[] {
123
+ const map = new Map<string, T>();
124
+ for (const record of records) {
125
+ map.set(record.id, record);
126
+ }
127
+ return Array.from(map.values());
128
+ }
129
+
130
+ /**
131
+ * Get all versions for a specific ID.
132
+ */
133
+ export function getVersions<T extends { id: string; version: number }>(
134
+ records: T[],
135
+ id: string,
136
+ ): T[] {
137
+ const seen = new Map<number, T>();
138
+ for (const record of records) {
139
+ if (record.id === id) {
140
+ seen.set(record.version, record);
141
+ }
142
+ }
143
+ return Array.from(seen.values()).sort((a, b) => a.version - b.version);
144
+ }