@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,124 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { cleanupTempDir, commitFile, createTempGitRepo } from "./test-helpers.ts";
6
+
7
+ describe("createTempGitRepo", () => {
8
+ let repoDir: string | undefined;
9
+
10
+ afterEach(async () => {
11
+ if (repoDir) {
12
+ await cleanupTempDir(repoDir);
13
+ repoDir = undefined;
14
+ }
15
+ });
16
+
17
+ test("creates a directory with an initialized git repo", async () => {
18
+ repoDir = await createTempGitRepo();
19
+
20
+ expect(existsSync(join(repoDir, ".git"))).toBe(true);
21
+ });
22
+
23
+ test("repo has at least one commit (HEAD exists)", async () => {
24
+ repoDir = await createTempGitRepo();
25
+
26
+ const proc = Bun.spawn(["git", "rev-parse", "HEAD"], {
27
+ cwd: repoDir,
28
+ stdout: "pipe",
29
+ stderr: "pipe",
30
+ });
31
+ const exitCode = await proc.exited;
32
+
33
+ expect(exitCode).toBe(0);
34
+ });
35
+
36
+ test("repo is on a branch (not detached HEAD)", async () => {
37
+ repoDir = await createTempGitRepo();
38
+
39
+ const proc = Bun.spawn(["git", "symbolic-ref", "HEAD"], {
40
+ cwd: repoDir,
41
+ stdout: "pipe",
42
+ stderr: "pipe",
43
+ });
44
+ const stdout = await new Response(proc.stdout).text();
45
+ const exitCode = await proc.exited;
46
+
47
+ expect(exitCode).toBe(0);
48
+ expect(stdout.trim()).toMatch(/^refs\/heads\//);
49
+ });
50
+ });
51
+
52
+ describe("commitFile", () => {
53
+ let repoDir: string | undefined;
54
+
55
+ afterEach(async () => {
56
+ if (repoDir) {
57
+ await cleanupTempDir(repoDir);
58
+ repoDir = undefined;
59
+ }
60
+ });
61
+
62
+ test("creates file and commits it", async () => {
63
+ repoDir = await createTempGitRepo();
64
+
65
+ await commitFile(repoDir, "hello.txt", "world");
66
+
67
+ // File exists with correct content
68
+ const content = await readFile(join(repoDir, "hello.txt"), "utf-8");
69
+ expect(content).toBe("world");
70
+
71
+ // Git log shows the commit
72
+ const proc = Bun.spawn(["git", "log", "--oneline"], {
73
+ cwd: repoDir,
74
+ stdout: "pipe",
75
+ stderr: "pipe",
76
+ });
77
+ const stdout = await new Response(proc.stdout).text();
78
+ await proc.exited;
79
+
80
+ expect(stdout).toContain("add hello.txt");
81
+ });
82
+
83
+ test("creates nested directories as needed", async () => {
84
+ repoDir = await createTempGitRepo();
85
+
86
+ await commitFile(repoDir, "src/deep/nested/file.ts", "export const x = 1;");
87
+
88
+ expect(existsSync(join(repoDir, "src/deep/nested/file.ts"))).toBe(true);
89
+ });
90
+
91
+ test("uses custom commit message when provided", async () => {
92
+ repoDir = await createTempGitRepo();
93
+
94
+ await commitFile(repoDir, "readme.md", "# Hi", "docs: add readme");
95
+
96
+ const proc = Bun.spawn(["git", "log", "--oneline", "-1"], {
97
+ cwd: repoDir,
98
+ stdout: "pipe",
99
+ stderr: "pipe",
100
+ });
101
+ const stdout = await new Response(proc.stdout).text();
102
+ await proc.exited;
103
+
104
+ expect(stdout).toContain("docs: add readme");
105
+ });
106
+ });
107
+
108
+ describe("cleanupTempDir", () => {
109
+ test("removes directory and all contents", async () => {
110
+ const repoDir = await createTempGitRepo();
111
+ await commitFile(repoDir, "file.txt", "data");
112
+
113
+ expect(existsSync(repoDir)).toBe(true);
114
+
115
+ await cleanupTempDir(repoDir);
116
+
117
+ expect(existsSync(repoDir)).toBe(false);
118
+ });
119
+
120
+ test("does not throw when directory does not exist", async () => {
121
+ await cleanupTempDir("/tmp/agentplate-nonexistent-test-dir-12345");
122
+ // No error thrown = pass
123
+ });
124
+ });
@@ -0,0 +1,145 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ /**
6
+ * Git environment variables for test repos.
7
+ * Using env vars instead of per-repo `git config` eliminates 2 subprocess
8
+ * spawns per repo creation.
9
+ */
10
+ const GIT_TEST_ENV = {
11
+ GIT_AUTHOR_NAME: "Agentplate Test",
12
+ GIT_AUTHOR_EMAIL: "test@agentplate.dev",
13
+ GIT_COMMITTER_NAME: "Agentplate Test",
14
+ GIT_COMMITTER_EMAIL: "test@agentplate.dev",
15
+ };
16
+
17
+ /** Cached template repo path. Created lazily on first call. */
18
+ let _templateDir: string | null = null;
19
+
20
+ /**
21
+ * Get or create a template git repo with an initial commit.
22
+ * All test repos clone from this template (1 subprocess instead of 5).
23
+ */
24
+ async function getTemplateRepo(): Promise<string> {
25
+ if (_templateDir) return _templateDir;
26
+
27
+ const dir = await mkdtemp(join(tmpdir(), "agentplate-template-"));
28
+ await runGitInDir(dir, ["init", "-b", "main"]);
29
+ await Bun.write(join(dir, ".gitkeep"), "");
30
+ await runGitInDir(dir, ["add", ".gitkeep"]);
31
+ await runGitInDir(dir, ["commit", "-m", "initial commit"]);
32
+
33
+ _templateDir = dir;
34
+ return dir;
35
+ }
36
+
37
+ /**
38
+ * Create a temporary directory with a real git repo initialized.
39
+ * Includes an initial commit so branches can be created immediately.
40
+ *
41
+ * Uses a cached template repo + `git clone --local` for speed:
42
+ * 1 subprocess per call instead of 5.
43
+ *
44
+ * @returns The absolute path to the temp git repo.
45
+ */
46
+ export async function createTempGitRepo(): Promise<string> {
47
+ const template = await getTemplateRepo();
48
+ const dir = await mkdtemp(join(tmpdir(), "agentplate-test-"));
49
+ // Clone into the empty dir. Avoid --local (hardlinks trigger EFAULT in Bun's rm).
50
+ await runGitInDir(".", ["clone", template, dir]);
51
+ // Set git identity at repo level so code that doesn't use GIT_TEST_ENV
52
+ // (e.g., resolver's runGit) can still commit. Locally this is covered by
53
+ // ~/.gitconfig, but CI runners have no global git identity.
54
+ await runGitInDir(dir, ["config", "user.name", "Agentplate Test"]);
55
+ await runGitInDir(dir, ["config", "user.email", "test@agentplate.dev"]);
56
+ return dir;
57
+ }
58
+
59
+ /**
60
+ * Add and commit a file to a git repo.
61
+ *
62
+ * @param repoDir - Absolute path to the git repo
63
+ * @param filePath - Relative path within the repo (e.g. "src/foo.ts")
64
+ * @param content - File content to write
65
+ * @param message - Commit message (defaults to "add {filePath}")
66
+ */
67
+ export async function commitFile(
68
+ repoDir: string,
69
+ filePath: string,
70
+ content: string,
71
+ message?: string,
72
+ ): Promise<void> {
73
+ const fullPath = join(repoDir, filePath);
74
+
75
+ // Ensure parent directories exist
76
+ const parentDir = join(fullPath, "..");
77
+ const { mkdir } = await import("node:fs/promises");
78
+ await mkdir(parentDir, { recursive: true });
79
+
80
+ await Bun.write(fullPath, content);
81
+ await runGitInDir(repoDir, ["add", filePath]);
82
+ await runGitInDir(repoDir, ["commit", "-m", message ?? `add ${filePath}`]);
83
+ }
84
+
85
+ /**
86
+ * Get the default branch name of a git repo (e.g., "main" or "master").
87
+ * Uses `git symbolic-ref --short HEAD` to read the current branch.
88
+ *
89
+ * Useful in tests to avoid hardcoding "main" -- CI runners may default to "master".
90
+ */
91
+ export async function getDefaultBranch(repoDir: string): Promise<string> {
92
+ const stdout = await runGitInDir(repoDir, ["symbolic-ref", "--short", "HEAD"]);
93
+ return stdout.trim();
94
+ }
95
+
96
+ /**
97
+ * Remove a temp directory. Safe to call even if the directory doesn't exist.
98
+ *
99
+ * On Windows, SQLite WAL/SHM file handles may linger briefly after db.close(),
100
+ * causing EBUSY errors on immediate rm(). Retries with exponential backoff
101
+ * (up to ~1.5s total) to handle this OS-level timing issue.
102
+ */
103
+ export async function cleanupTempDir(dir: string): Promise<void> {
104
+ const maxRetries = process.platform === "win32" ? 5 : 0;
105
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
106
+ try {
107
+ await rm(dir, { recursive: true, force: true });
108
+ return;
109
+ } catch (err: unknown) {
110
+ const code = (err as NodeJS.ErrnoException).code;
111
+ if (code === "EBUSY" && attempt < maxRetries) {
112
+ // Exponential backoff: 50, 100, 200, 400, 800ms
113
+ await Bun.sleep(50 * 2 ** attempt);
114
+ continue;
115
+ }
116
+ // Non-EBUSY or final attempt: swallow (temp dirs are cleaned by OS anyway)
117
+ if (code !== "ENOENT") return;
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Run a git command in the given directory. Throws on non-zero exit.
124
+ * Passes GIT_AUTHOR/COMMITTER env vars so repos don't need per-repo config.
125
+ */
126
+ export async function runGitInDir(cwd: string, args: string[]): Promise<string> {
127
+ const proc = Bun.spawn(["git", ...args], {
128
+ cwd,
129
+ stdout: "pipe",
130
+ stderr: "pipe",
131
+ env: { ...process.env, ...GIT_TEST_ENV },
132
+ });
133
+
134
+ const [stdout, stderr, exitCode] = await Promise.all([
135
+ new Response(proc.stdout).text(),
136
+ new Response(proc.stderr).text(),
137
+ proc.exited,
138
+ ]);
139
+
140
+ if (exitCode !== 0) {
141
+ throw new Error(`git ${args.join(" ")} failed (exit ${exitCode}): ${stderr.trim()}`);
142
+ }
143
+
144
+ return stdout;
145
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Regression test for agentplate-6d42: bun test must not be redirectable to a
3
+ * real .agentplate/ via inherited AGENTPLATE_PROJECT_ROOT (or sibling) env vars.
4
+ *
5
+ * The preload in bunfig.toml runs src/test-setup.ts before any test loads,
6
+ * deleting AGENTPLATE_* env vars and clearing the project-root override. By
7
+ * the time this test executes, those values must already be gone — even if a
8
+ * worker agent's environment had them set when bun test was invoked.
9
+ */
10
+
11
+ import { expect, test } from "bun:test";
12
+ import { getProjectRootOverride } from "./config.ts";
13
+
14
+ const ENV_KEYS = [
15
+ "AGENTPLATE_PROJECT_ROOT",
16
+ "AGENTPLATE_AGENT_NAME",
17
+ "AGENTPLATE_WORKTREE_PATH",
18
+ "AGENTPLATE_TASK_ID",
19
+ "AGENTPLATE_PROFILE",
20
+ "AGENTPLATE_RUN_ID",
21
+ ] as const;
22
+
23
+ for (const key of ENV_KEYS) {
24
+ test(`${key} is unset by the test preload`, () => {
25
+ expect(process.env[key]).toBeUndefined();
26
+ });
27
+ }
28
+
29
+ test("project-root override is cleared by the test preload", () => {
30
+ expect(getProjectRootOverride()).toBeUndefined();
31
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Global test preload (referenced from bunfig.toml [test] preload).
3
+ *
4
+ * Prevents test runs from leaking into a real .agentplate/ when bun test is
5
+ * executed inside an agent worktree (where ap sling injects AGENTPLATE_PROJECT_ROOT
6
+ * into the spawned process — see src/commands/sling.ts:928).
7
+ *
8
+ * Without this preload, resolveProjectRoot() short-circuits to the env var
9
+ * before consulting the per-test temp dir, so tests calling cleanCommand,
10
+ * coordinatorCommand, mailCommand, etc. silently target the live project.
11
+ * That's how agentplate-6d42 contamination occurred: a worker agent ran
12
+ * bun test, clean.test.ts wiped the live .agentplate/, coordinator.test.ts
13
+ * left dozens of bogus runs, and mail.test.ts inserted fixture messages.
14
+ *
15
+ * Tests that need AGENTPLATE_PROJECT_ROOT set (e.g. config.test.ts) set it
16
+ * explicitly inside the test body and restore it in afterEach.
17
+ */
18
+
19
+ import { clearProjectRootOverride } from "./config.ts";
20
+
21
+ delete process.env.AGENTPLATE_PROJECT_ROOT;
22
+ delete process.env.AGENTPLATE_AGENT_NAME;
23
+ delete process.env.AGENTPLATE_WORKTREE_PATH;
24
+ delete process.env.AGENTPLATE_TASK_ID;
25
+ delete process.env.AGENTPLATE_PROFILE;
26
+ delete process.env.AGENTPLATE_RUN_ID;
27
+
28
+ clearProjectRootOverride();
@@ -0,0 +1,368 @@
1
+ import type { Classification, ExpertiseRecord, Outcome } from "./schemas/record.ts";
2
+ import { getExpertisePath, readConfig } from "./utils/config.ts";
3
+ import {
4
+ appendRecord,
5
+ filterByClassification,
6
+ filterByFile,
7
+ filterByType,
8
+ findDuplicate,
9
+ readExpertiseFile,
10
+ resolveRecordId,
11
+ searchRecords,
12
+ writeExpertiseFile,
13
+ } from "./utils/expertise.ts";
14
+ import { withFileLock } from "./utils/lock.ts";
15
+
16
+ export interface RecordOptions {
17
+ force?: boolean;
18
+ cwd?: string;
19
+ }
20
+
21
+ export interface RecordResult {
22
+ action: "created" | "updated" | "skipped";
23
+ record: ExpertiseRecord;
24
+ }
25
+
26
+ export interface SearchOptions {
27
+ domain?: string;
28
+ type?: string;
29
+ tag?: string;
30
+ classification?: string;
31
+ file?: string;
32
+ cwd?: string;
33
+ }
34
+
35
+ export interface SearchResult {
36
+ domain: string;
37
+ records: ExpertiseRecord[];
38
+ }
39
+
40
+ export interface QueryOptions {
41
+ type?: string;
42
+ classification?: string;
43
+ file?: string;
44
+ cwd?: string;
45
+ }
46
+
47
+ export interface EditOptions {
48
+ cwd?: string;
49
+ }
50
+
51
+ export interface OutcomeOptions {
52
+ cwd?: string;
53
+ }
54
+
55
+ export interface AppendOutcomeResult {
56
+ record: ExpertiseRecord;
57
+ outcome: Outcome;
58
+ total_outcomes: number;
59
+ }
60
+
61
+ export interface RecordUpdates {
62
+ classification?: Classification;
63
+ tags?: string[];
64
+ relates_to?: string[];
65
+ supersedes?: string[];
66
+ outcomes?: Outcome[];
67
+ // type-specific fields
68
+ content?: string;
69
+ name?: string;
70
+ description?: string;
71
+ resolution?: string;
72
+ title?: string;
73
+ rationale?: string;
74
+ files?: string[];
75
+ }
76
+
77
+ /**
78
+ * Record an expertise record in the given domain.
79
+ * Named record types (pattern, decision, reference, guide) are upserted on
80
+ * duplicate key; convention and failure duplicates are skipped unless force=true.
81
+ */
82
+ export async function recordExpertise(
83
+ domain: string,
84
+ record: ExpertiseRecord,
85
+ options: RecordOptions = {},
86
+ ): Promise<RecordResult> {
87
+ const { force = false, cwd } = options;
88
+
89
+ const config = await readConfig(cwd);
90
+ if (!(domain in config.domains)) {
91
+ throw new Error(
92
+ `Domain "${domain}" not found in config. Available domains: ${Object.keys(config.domains).join(", ") || "(none)"}`,
93
+ );
94
+ }
95
+
96
+ const filePath = getExpertisePath(domain, cwd);
97
+
98
+ return withFileLock(filePath, async () => {
99
+ const existing = await readExpertiseFile(filePath);
100
+ const dup = findDuplicate(existing, record);
101
+
102
+ if (dup && !force) {
103
+ const isNamed =
104
+ record.type === "pattern" ||
105
+ record.type === "decision" ||
106
+ record.type === "reference" ||
107
+ record.type === "guide";
108
+
109
+ if (isNamed) {
110
+ existing[dup.index] = record;
111
+ await writeExpertiseFile(filePath, existing);
112
+ return { action: "updated" as const, record };
113
+ }
114
+ return { action: "skipped" as const, record: dup.record };
115
+ }
116
+ await appendRecord(filePath, record);
117
+ return { action: "created" as const, record };
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Search expertise records across domains using BM25 ranking.
123
+ * Returns domains with at least one matching record.
124
+ */
125
+ export async function searchExpertise(
126
+ query: string,
127
+ options: SearchOptions = {},
128
+ ): Promise<SearchResult[]> {
129
+ const { cwd } = options;
130
+ const config = await readConfig(cwd);
131
+
132
+ let domainsToSearch: string[];
133
+ if (options.domain) {
134
+ if (!(options.domain in config.domains)) {
135
+ throw new Error(
136
+ `Domain "${options.domain}" not found in config. Available domains: ${Object.keys(config.domains).join(", ")}`,
137
+ );
138
+ }
139
+ domainsToSearch = [options.domain];
140
+ } else {
141
+ domainsToSearch = Object.keys(config.domains);
142
+ }
143
+
144
+ const results: SearchResult[] = [];
145
+
146
+ for (const d of domainsToSearch) {
147
+ const filePath = getExpertisePath(d, cwd);
148
+ let records = await readExpertiseFile(filePath);
149
+
150
+ if (options.type) {
151
+ records = filterByType(records, options.type);
152
+ }
153
+ if (options.tag) {
154
+ const tagLower = options.tag.toLowerCase();
155
+ records = records.filter((r) => r.tags?.some((t) => t.toLowerCase() === tagLower));
156
+ }
157
+ if (options.classification) {
158
+ records = filterByClassification(records, options.classification);
159
+ }
160
+ if (options.file) {
161
+ records = filterByFile(records, options.file);
162
+ }
163
+
164
+ const matches = searchRecords(records, query);
165
+ if (matches.length > 0) {
166
+ results.push({ domain: d, records: matches });
167
+ }
168
+ }
169
+
170
+ return results;
171
+ }
172
+
173
+ /**
174
+ * Query all records in a domain with optional filtering.
175
+ */
176
+ export async function queryDomain(
177
+ domain: string,
178
+ options: QueryOptions = {},
179
+ ): Promise<ExpertiseRecord[]> {
180
+ const { cwd } = options;
181
+ const config = await readConfig(cwd);
182
+
183
+ if (!(domain in config.domains)) {
184
+ throw new Error(
185
+ `Domain "${domain}" not found in config. Available domains: ${Object.keys(config.domains).join(", ") || "(none)"}`,
186
+ );
187
+ }
188
+
189
+ const filePath = getExpertisePath(domain, cwd);
190
+ let records = await readExpertiseFile(filePath);
191
+
192
+ if (options.type) {
193
+ records = filterByType(records, options.type);
194
+ }
195
+ if (options.classification) {
196
+ records = filterByClassification(records, options.classification);
197
+ }
198
+ if (options.file) {
199
+ records = filterByFile(records, options.file);
200
+ }
201
+
202
+ return records;
203
+ }
204
+
205
+ /**
206
+ * Edit an existing record by ID in the given domain.
207
+ * Only provided fields in updates are modified; all other fields are preserved.
208
+ */
209
+ export async function editRecord(
210
+ domain: string,
211
+ id: string,
212
+ updates: RecordUpdates,
213
+ options: EditOptions = {},
214
+ ): Promise<ExpertiseRecord> {
215
+ const { cwd } = options;
216
+ const config = await readConfig(cwd);
217
+
218
+ if (!(domain in config.domains)) {
219
+ throw new Error(
220
+ `Domain "${domain}" not found in config. Available domains: ${Object.keys(config.domains).join(", ") || "(none)"}`,
221
+ );
222
+ }
223
+
224
+ const filePath = getExpertisePath(domain, cwd);
225
+
226
+ return withFileLock(filePath, async () => {
227
+ const records = await readExpertiseFile(filePath);
228
+ const resolved = resolveRecordId(records, id);
229
+
230
+ if (!resolved.ok) {
231
+ throw new Error(resolved.error);
232
+ }
233
+
234
+ const targetIndex = resolved.index;
235
+ const original = records[targetIndex];
236
+ if (!original) {
237
+ throw new Error(`Record at index ${targetIndex} not found`);
238
+ }
239
+ const record: ExpertiseRecord = { ...original };
240
+
241
+ // Apply common updates
242
+ if (updates.classification !== undefined) {
243
+ record.classification = updates.classification;
244
+ }
245
+ if (updates.tags !== undefined) {
246
+ record.tags = updates.tags;
247
+ }
248
+ if (updates.relates_to !== undefined) {
249
+ record.relates_to = updates.relates_to;
250
+ }
251
+ if (updates.supersedes !== undefined) {
252
+ record.supersedes = updates.supersedes;
253
+ }
254
+ if (updates.outcomes !== undefined) {
255
+ record.outcomes = updates.outcomes;
256
+ }
257
+
258
+ // Apply type-specific updates
259
+ switch (record.type) {
260
+ case "convention":
261
+ if (updates.content !== undefined) {
262
+ record.content = updates.content;
263
+ }
264
+ break;
265
+ case "pattern":
266
+ if (updates.name !== undefined) {
267
+ record.name = updates.name;
268
+ }
269
+ if (updates.description !== undefined) {
270
+ record.description = updates.description;
271
+ }
272
+ if (updates.files !== undefined) {
273
+ record.files = updates.files;
274
+ }
275
+ break;
276
+ case "failure":
277
+ if (updates.description !== undefined) {
278
+ record.description = updates.description;
279
+ }
280
+ if (updates.resolution !== undefined) {
281
+ record.resolution = updates.resolution;
282
+ }
283
+ break;
284
+ case "decision":
285
+ if (updates.title !== undefined) {
286
+ record.title = updates.title;
287
+ }
288
+ if (updates.rationale !== undefined) {
289
+ record.rationale = updates.rationale;
290
+ }
291
+ break;
292
+ case "reference":
293
+ if (updates.name !== undefined) {
294
+ record.name = updates.name;
295
+ }
296
+ if (updates.description !== undefined) {
297
+ record.description = updates.description;
298
+ }
299
+ if (updates.files !== undefined) {
300
+ record.files = updates.files;
301
+ }
302
+ break;
303
+ case "guide":
304
+ if (updates.name !== undefined) {
305
+ record.name = updates.name;
306
+ }
307
+ if (updates.description !== undefined) {
308
+ record.description = updates.description;
309
+ }
310
+ break;
311
+ }
312
+
313
+ records[targetIndex] = record;
314
+ await writeExpertiseFile(filePath, records);
315
+
316
+ return record;
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Append an outcome to an existing record by ID in the given domain.
322
+ * Outcomes accumulate (are not replaced) to build a confirmation-frequency
323
+ * history that drives confidence scoring.
324
+ */
325
+ export async function appendOutcome(
326
+ domain: string,
327
+ id: string,
328
+ outcome: Outcome,
329
+ options: OutcomeOptions = {},
330
+ ): Promise<AppendOutcomeResult> {
331
+ const { cwd } = options;
332
+ const config = await readConfig(cwd);
333
+
334
+ if (!(domain in config.domains)) {
335
+ throw new Error(
336
+ `Domain "${domain}" not found in config. Available domains: ${Object.keys(config.domains).join(", ") || "(none)"}`,
337
+ );
338
+ }
339
+
340
+ const filePath = getExpertisePath(domain, cwd);
341
+
342
+ return withFileLock(filePath, async () => {
343
+ const records = await readExpertiseFile(filePath);
344
+ const resolved = resolveRecordId(records, id);
345
+
346
+ if (!resolved.ok) {
347
+ throw new Error(resolved.error);
348
+ }
349
+
350
+ const targetIndex = resolved.index;
351
+ const original = records[targetIndex];
352
+ if (!original) {
353
+ throw new Error(`Record at index ${targetIndex} not found`);
354
+ }
355
+ const record: ExpertiseRecord = { ...original };
356
+
357
+ const o: Outcome = {
358
+ ...outcome,
359
+ recorded_at: outcome.recorded_at ?? new Date().toISOString(),
360
+ };
361
+
362
+ record.outcomes = [...(record.outcomes ?? []), o];
363
+ records[targetIndex] = record;
364
+ await writeExpertiseFile(filePath, records);
365
+
366
+ return { record, outcome: o, total_outcomes: record.outcomes.length };
367
+ });
368
+ }