@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,139 @@
1
+ import type { Issue } from "./types.ts";
2
+
3
+ // Soft-coupling helper: infers the loam domain to query for a given seed.
4
+ // PLAN_SPEC.md:344-352. Returns null when no signal matches or ml is absent —
5
+ // callers must treat null as "skip loam enrichment", not as an error.
6
+
7
+ export interface InferDomainOptions {
8
+ seed: Issue;
9
+ explicitDomain?: string;
10
+ cwd?: string;
11
+ }
12
+
13
+ export type DomainSource = "explicit" | "labels" | "files" | "none";
14
+
15
+ export interface InferDomainResult {
16
+ domain: string | null;
17
+ source: DomainSource;
18
+ }
19
+
20
+ export function inferDomain(opts: InferDomainOptions): InferDomainResult {
21
+ if (opts.explicitDomain && opts.explicitDomain.length > 0) {
22
+ return { domain: opts.explicitDomain, source: "explicit" };
23
+ }
24
+
25
+ const cwd = opts.cwd ?? process.cwd();
26
+
27
+ // Pass PATH explicitly: Bun.which resolves against a snapshot taken at
28
+ // process start otherwise, which makes the helper untestable when callers
29
+ // mutate process.env.PATH (e.g. tests prepending a fake-ml bin/).
30
+ const ml = Bun.which("ml", { PATH: process.env.PATH });
31
+ if (!ml) return { domain: null, source: "none" };
32
+
33
+ const domains = listLoamDomains(ml, cwd);
34
+ if (!domains || domains.length === 0) return { domain: null, source: "none" };
35
+ const domainSet = new Set(domains);
36
+
37
+ for (const label of opts.seed.labels ?? []) {
38
+ if (domainSet.has(label)) {
39
+ return { domain: label, source: "labels" };
40
+ }
41
+ }
42
+
43
+ const candidates = collectFileCandidates(opts.seed.description ?? "", cwd);
44
+ for (const path of candidates) {
45
+ for (const seg of path.split("/")) {
46
+ if (domainSet.has(seg)) {
47
+ return { domain: seg, source: "files" };
48
+ }
49
+ }
50
+ }
51
+
52
+ return { domain: null, source: "none" };
53
+ }
54
+
55
+ function listLoamDomains(ml: string, cwd: string): string[] | null {
56
+ try {
57
+ const result = Bun.spawnSync([ml, "--json", "status"], {
58
+ cwd,
59
+ stdout: "pipe",
60
+ stderr: "pipe",
61
+ });
62
+ if ((result.exitCode ?? 0) !== 0) return null;
63
+ const stdout = new TextDecoder().decode(result.stdout);
64
+ const parsed = JSON.parse(stdout) as unknown;
65
+ if (!parsed || typeof parsed !== "object") return null;
66
+ const domainsRaw = (parsed as { domains?: unknown }).domains;
67
+ if (!Array.isArray(domainsRaw)) return null;
68
+ const out: string[] = [];
69
+ for (const entry of domainsRaw) {
70
+ if (
71
+ entry &&
72
+ typeof entry === "object" &&
73
+ typeof (entry as { domain?: unknown }).domain === "string"
74
+ ) {
75
+ out.push((entry as { domain: string }).domain);
76
+ }
77
+ }
78
+ return out;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ // Find file paths to map to domains. Combines two sources:
85
+ // 1. Path-like tokens in the seed description (e.g. "src/commands/plan.ts").
86
+ // 2. Files currently changed in the working tree (`git diff --name-only`),
87
+ // intersected with the description references — this prefers files the
88
+ // user is actively working on, which best signal the right domain.
89
+ // Falls back to all description references if no diff intersection exists.
90
+ function collectFileCandidates(description: string, cwd: string): string[] {
91
+ const refs = extractPathRefs(description);
92
+ if (refs.length === 0) return [];
93
+
94
+ const changed = gitChangedFiles(cwd);
95
+ if (changed && changed.length > 0) {
96
+ const changedSet = new Set(changed);
97
+ const intersect = refs.filter((r) => changedSet.has(r));
98
+ if (intersect.length > 0) return intersect;
99
+ }
100
+ return refs;
101
+ }
102
+
103
+ // Match path-shaped tokens: at least one slash, ending in .ext, no spaces.
104
+ // Avoids false positives like "v1.0" or bare filenames without directory anchors.
105
+ const PATH_REGEX = /[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+\.[A-Za-z0-9]+/g;
106
+
107
+ function extractPathRefs(text: string): string[] {
108
+ if (!text) return [];
109
+ const out: string[] = [];
110
+ const seen = new Set<string>();
111
+ const matches = text.match(PATH_REGEX);
112
+ if (!matches) return [];
113
+ for (const m of matches) {
114
+ const cleaned = m.replace(/[.,;:)\]]+$/, "");
115
+ if (!seen.has(cleaned)) {
116
+ seen.add(cleaned);
117
+ out.push(cleaned);
118
+ }
119
+ }
120
+ return out;
121
+ }
122
+
123
+ function gitChangedFiles(cwd: string): string[] | null {
124
+ try {
125
+ const result = Bun.spawnSync(["git", "diff", "--name-only", "HEAD"], {
126
+ cwd,
127
+ stdout: "pipe",
128
+ stderr: "pipe",
129
+ });
130
+ if ((result.exitCode ?? 0) !== 0) return null;
131
+ const stdout = new TextDecoder().decode(result.stdout);
132
+ return stdout
133
+ .split("\n")
134
+ .map((s) => s.trim())
135
+ .filter((s) => s.length > 0);
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
@@ -0,0 +1,65 @@
1
+ // Plan status state machine (PLAN_SPEC.md:109-127). Plan status is derived
2
+ // from its children — `sr plan submit` sets the initial `approved` state,
3
+ // then update.ts/close.ts call `applyPlanTransitions` to recompute on every
4
+ // child status change.
5
+ //
6
+ // Transition rules:
7
+ // draft → draft (manual; never auto-derived)
8
+ // approved → active when any child is in_progress
9
+ // active → done when all children are closed
10
+ // done → active when a closed child re-opens (reopen path)
11
+ // active → active while at least one child is non-closed (no regress)
12
+ // approved → approved when no child has moved yet (idempotent)
13
+ //
14
+ // All transitions happen under the plans-lock held by the caller (update.ts /
15
+ // close.ts wrap their issue writes in `withLock(plansPath, () => withLock(issuesPath, ...))`).
16
+
17
+ import type { Issue, Plan, PlanStatus } from "./types.ts";
18
+
19
+ export function computeNextPlanStatus(plan: Plan, planChildren: Issue[]): PlanStatus {
20
+ if (plan.status === "draft") return "draft";
21
+ if (planChildren.length === 0) return plan.status;
22
+ const allClosed = planChildren.every((c) => c.status === "closed");
23
+ if (allClosed) return "done";
24
+ // Has at least one non-closed child.
25
+ if (plan.status === "active" || plan.status === "done") return "active";
26
+ const anyInProgress = planChildren.some((c) => c.status === "in_progress");
27
+ return anyInProgress ? "active" : "approved";
28
+ }
29
+
30
+ // Recompute and apply transitions for any plan whose children may have changed.
31
+ // Mutates `plans` in place; returns the count of rows that changed.
32
+ export function applyPlanTransitions(
33
+ plans: Plan[],
34
+ allIssues: Issue[],
35
+ affectedPlanIds: Iterable<string>,
36
+ now: string,
37
+ ): number {
38
+ const targets = new Set(affectedPlanIds);
39
+ let changed = 0;
40
+ for (let i = 0; i < plans.length; i++) {
41
+ const p = plans[i];
42
+ if (!p || !targets.has(p.id)) continue;
43
+ const children: Issue[] = [];
44
+ for (const cid of p.children) {
45
+ const c = allIssues.find((iss) => iss.id === cid);
46
+ if (c) children.push(c);
47
+ }
48
+ const next = computeNextPlanStatus(p, children);
49
+ if (next !== p.status) {
50
+ plans[i] = { ...p, status: next, updatedAt: now };
51
+ changed++;
52
+ }
53
+ }
54
+ return changed;
55
+ }
56
+
57
+ // Find every plan whose `children` array contains any of the given issue ids.
58
+ export function affectedPlanIds(plans: Plan[], issueIds: string[]): string[] {
59
+ const ids = new Set(issueIds);
60
+ const out: string[] = [];
61
+ for (const p of plans) {
62
+ if (p.children.some((cid) => ids.has(cid))) out.push(p.id);
63
+ }
64
+ return out;
65
+ }
@@ -0,0 +1,207 @@
1
+ // Per-section prior_art enrichment via `ml query` shell-out.
2
+ // PLAN_SPEC.md:344-352. Soft coupling: any failure (ml absent, query failure,
3
+ // malformed output) produces empty prior_art arrays — never throws, never logs
4
+ // to stderr.
5
+
6
+ export interface PriorArtEntry {
7
+ id: string;
8
+ type: string;
9
+ summary: string;
10
+ relevance: number;
11
+ }
12
+
13
+ export interface SectionRequest {
14
+ name: string;
15
+ loamSource?: string;
16
+ }
17
+
18
+ const PRIOR_ART_LIMIT = 5;
19
+
20
+ // PLAN_SPEC.md:349 — well-known section names map to record types when no
21
+ // `loam_source:` hint is present on the section spec.
22
+ const WELL_KNOWN_SECTION_TYPES: Record<string, string[]> = {
23
+ approach: ["pattern", "decision"],
24
+ risks: ["failure"],
25
+ acceptance: ["guide"],
26
+ };
27
+
28
+ export function typesForSection(name: string, hint?: string): string[] {
29
+ if (hint && hint.length > 0) return [hint];
30
+ return WELL_KNOWN_SECTION_TYPES[name] ?? [];
31
+ }
32
+
33
+ export interface EnrichOptions {
34
+ domain: string | null;
35
+ sections: SectionRequest[];
36
+ cwd?: string;
37
+ }
38
+
39
+ // Returns prior_art entries keyed by section name. Sections that do not opt
40
+ // into loam enrichment still appear in the map with an empty array, so the
41
+ // caller can do a flat lookup without checking presence.
42
+ export function enrichPriorArt(opts: EnrichOptions): Record<string, PriorArtEntry[]> {
43
+ const out: Record<string, PriorArtEntry[]> = {};
44
+ for (const s of opts.sections) out[s.name] = [];
45
+
46
+ if (!opts.domain) return out;
47
+
48
+ const cwd = opts.cwd ?? process.cwd();
49
+ const ml = Bun.which("ml", { PATH: process.env.PATH });
50
+ if (!ml) return out;
51
+
52
+ for (const s of opts.sections) {
53
+ const types = typesForSection(s.name, s.loamSource);
54
+ if (types.length === 0) continue;
55
+ const collected: PriorArtEntry[] = [];
56
+ for (const t of types) {
57
+ const entries = queryLoamRecords({ ml, domain: opts.domain, type: t, cwd });
58
+ collected.push(...entries);
59
+ }
60
+ out[s.name] = collected.slice(0, PRIOR_ART_LIMIT);
61
+ }
62
+ return out;
63
+ }
64
+
65
+ interface QueryArgs {
66
+ ml: string;
67
+ domain: string;
68
+ type: string;
69
+ cwd: string;
70
+ }
71
+
72
+ function queryLoamRecords(args: QueryArgs): PriorArtEntry[] {
73
+ try {
74
+ // The actual loam CLI takes domain positionally and lifts --json to the
75
+ // top-level (`ml --json query <domain>`), not `ml query --domain` as the
76
+ // spec text describes. Parse what loam actually emits.
77
+ const result = Bun.spawnSync([args.ml, "--json", "query", args.domain, "--type", args.type], {
78
+ cwd: args.cwd,
79
+ stdout: "pipe",
80
+ stderr: "pipe",
81
+ });
82
+ if ((result.exitCode ?? 0) !== 0) return [];
83
+ const stdout = new TextDecoder().decode(result.stdout);
84
+ return parseQueryOutput(stdout, args.type);
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ function parseQueryOutput(stdout: string, type: string): PriorArtEntry[] {
91
+ let parsed: unknown;
92
+ try {
93
+ parsed = JSON.parse(stdout);
94
+ } catch {
95
+ return [];
96
+ }
97
+ if (!parsed || typeof parsed !== "object") return [];
98
+ const domains = (parsed as { domains?: unknown }).domains;
99
+ if (!Array.isArray(domains)) return [];
100
+
101
+ const records: unknown[] = [];
102
+ for (const d of domains) {
103
+ if (!d || typeof d !== "object") continue;
104
+ const r = (d as { records?: unknown }).records;
105
+ if (Array.isArray(r)) records.push(...r);
106
+ }
107
+
108
+ const top = records.slice(0, PRIOR_ART_LIMIT);
109
+ const out: PriorArtEntry[] = [];
110
+ for (let i = 0; i < top.length; i++) {
111
+ const rec = top[i];
112
+ if (!rec || typeof rec !== "object") continue;
113
+ const id = stringField(rec, "id");
114
+ if (!id) continue;
115
+ const recType = stringField(rec, "type") ?? type;
116
+ const summary = summarize(rec);
117
+ // Relevance synthesized from rank: ml's CLI surface doesn't expose a
118
+ // score, so we rank by emit order (top hit = highest relevance).
119
+ const relevance = Number(((PRIOR_ART_LIMIT - i) / PRIOR_ART_LIMIT).toFixed(2));
120
+ out.push({ id, type: recType, summary, relevance });
121
+ }
122
+ return out;
123
+ }
124
+
125
+ function stringField(rec: object, key: string): string | undefined {
126
+ const v = (rec as Record<string, unknown>)[key];
127
+ return typeof v === "string" && v.length > 0 ? v : undefined;
128
+ }
129
+
130
+ // PLAN_SPEC.md:354-356 — opt-in outbound write. Best-effort: never throws;
131
+ // the caller renders `reason` to stderr on `ok: false` but does NOT roll back
132
+ // the plan write or change the submit exit code.
133
+
134
+ export interface RecordDecisionOptions {
135
+ domain: string;
136
+ planId: string;
137
+ title: string;
138
+ approach: string;
139
+ cwd?: string;
140
+ }
141
+
142
+ export interface RecordDecisionResult {
143
+ ok: boolean;
144
+ reason?: string;
145
+ loamId?: string;
146
+ }
147
+
148
+ export function recordDecision(opts: RecordDecisionOptions): RecordDecisionResult {
149
+ const cwd = opts.cwd ?? process.cwd();
150
+ const ml = Bun.which("ml", { PATH: process.env.PATH });
151
+ if (!ml) {
152
+ return { ok: false, reason: "ml not found on PATH; skipping --record-decision" };
153
+ }
154
+ try {
155
+ // `ml record decision` requires --title in addition to the spec args; pass
156
+ // the seed title so the recorded decision has a meaningful name. --json
157
+ // is appended (not prepended) so existing fixture parsers that key off
158
+ // argv[0] === "record" keep working.
159
+ const result = Bun.spawnSync(
160
+ [
161
+ ml,
162
+ "record",
163
+ opts.domain,
164
+ "--type",
165
+ "decision",
166
+ "--title",
167
+ opts.title,
168
+ "--rationale",
169
+ opts.approach,
170
+ "--evidence-sprout",
171
+ opts.planId,
172
+ "--json",
173
+ ],
174
+ { cwd, stdout: "pipe", stderr: "pipe" },
175
+ );
176
+ if ((result.exitCode ?? 0) !== 0) {
177
+ const stderr = new TextDecoder().decode(result.stderr).trim();
178
+ const detail = stderr ? `: ${stderr.split("\n")[0]}` : "";
179
+ return { ok: false, reason: `ml record failed${detail}` };
180
+ }
181
+ const stdout = new TextDecoder().decode(result.stdout).trim();
182
+ let loamId: string | undefined;
183
+ if (stdout.length > 0) {
184
+ try {
185
+ const parsed = JSON.parse(stdout) as { record?: { id?: unknown } };
186
+ const id = parsed?.record?.id;
187
+ if (typeof id === "string" && id.length > 0) loamId = id;
188
+ } catch {
189
+ // Older loam versions or non-JSON output: still success, just
190
+ // no id to surface.
191
+ }
192
+ }
193
+ return loamId ? { ok: true, loamId } : { ok: true };
194
+ } catch (e) {
195
+ return { ok: false, reason: `ml record threw: ${(e as Error).message}` };
196
+ }
197
+ }
198
+
199
+ const SUMMARY_MAX = 240;
200
+
201
+ function summarize(rec: object): string {
202
+ const name = stringField(rec, "name");
203
+ const desc = stringField(rec, "description");
204
+ const base = name && desc ? `${name}: ${desc}` : (desc ?? name ?? "");
205
+ if (base.length <= SUMMARY_MAX) return base;
206
+ return `${base.slice(0, SUMMARY_MAX - 1).trimEnd()}…`;
207
+ }
@@ -0,0 +1,209 @@
1
+ // Generates an AJV-compatible JSON Schema from a PlanTemplate (PLAN_SPEC.md:312).
2
+ //
3
+ // The shape of the output mirrors what Phase 1's hand-written `featureSchema`
4
+ // produced (src/plan-templates/feature.ts), so the same `compileSchema` from
5
+ // src/validation.ts consumes it. Custom templates declared in config.yaml
6
+ // compile through the same pipeline.
7
+ //
8
+ // Step.blocks structural checks (self-reference, out-of-range) live outside the
9
+ // schema since they depend on array length — `compilePlanTemplate` runs them
10
+ // as a second pass after AJV.
11
+
12
+ import type { PlanTemplate, SectionSpec } from "./types.ts";
13
+ import type { PartialStateDiff } from "./validation.ts";
14
+ import { compileSchema, type ErrorEntry } from "./validation.ts";
15
+
16
+ type JSONSchema = Record<string, unknown>;
17
+
18
+ // Step.title is enforced by validateStepTitleOrAdopt below rather than the
19
+ // AJV `required` list: adoption-only steps (existing_seed set, no spawn) may
20
+ // omit `title` since the adopted seed's title is preserved verbatim. The
21
+ // "either title or existing_seed" invariant runs as a post-AJV pass so the
22
+ // error message points at the offending step path (sprout-5583 / warren §11.Q).
23
+ const STEP_SCHEMA: JSONSchema = {
24
+ type: "object",
25
+ properties: {
26
+ title: { type: "string", minLength: 1 },
27
+ type: { type: "string", enum: ["task", "bug", "feature", "epic"] },
28
+ priority: { type: "integer", minimum: 0, maximum: 4 },
29
+ blocks: { type: "array", items: { type: "integer" } },
30
+ plan_template: { type: "string" },
31
+ // existing_seed adopts an already-open seed at submit time instead of
32
+ // spawning a fresh child (sprout-3c89 / pl-43ff step 1). The schema accepts
33
+ // any non-empty string; existence, status, and id-shape checks live in
34
+ // runSubmit alongside the rest of the spawn pipeline.
35
+ existing_seed: { type: "string", minLength: 1 },
36
+ // labels: optional per-step labels applied to the spawned/adopted child seed
37
+ // (sprout-7561 / pl-e5a8 step 1). Author-facing strings are normalized
38
+ // (lowercase, trim, dedup) by runSubmit; the schema only enforces the
39
+ // pre-normalization shape: an array of non-empty trimmed strings.
40
+ labels: {
41
+ type: "array",
42
+ items: { type: "string", minLength: 1, pattern: "\\S" },
43
+ },
44
+ },
45
+ };
46
+
47
+ export function generatePlanSchema(template: PlanTemplate): JSONSchema {
48
+ const required: string[] = [];
49
+ const properties: Record<string, JSONSchema> = {};
50
+ for (const [name, spec] of Object.entries(template.sections)) {
51
+ if (spec.required) required.push(name);
52
+ properties[name] = sectionToSchema(spec);
53
+ }
54
+ const sections: JSONSchema = { type: "object", properties };
55
+ if (required.length > 0) sections.required = required;
56
+ return {
57
+ type: "object",
58
+ required: ["template", "sections"],
59
+ properties: {
60
+ template: { type: "string", const: template.name },
61
+ sections,
62
+ },
63
+ };
64
+ }
65
+
66
+ function sectionToSchema(spec: SectionSpec): JSONSchema {
67
+ if (typeof spec.kind === "string") {
68
+ if (spec.kind === "text") {
69
+ return { type: "string", minLength: spec.min_length ?? 1 };
70
+ }
71
+ if (spec.kind === "list") {
72
+ return {
73
+ type: "array",
74
+ minItems: spec.min ?? 0,
75
+ items: itemToSchema(spec.item ?? "text"),
76
+ };
77
+ }
78
+ if (spec.kind === "steps") {
79
+ return { type: "array", minItems: spec.min ?? 0, items: STEP_SCHEMA };
80
+ }
81
+ }
82
+ return objectKindToSchema(spec.kind as Record<string, SectionSpec>);
83
+ }
84
+
85
+ function objectKindToSchema(fields: Record<string, SectionSpec>): JSONSchema {
86
+ const required: string[] = [];
87
+ const properties: Record<string, JSONSchema> = {};
88
+ for (const [k, v] of Object.entries(fields)) {
89
+ if (v.required) required.push(k);
90
+ properties[k] = sectionToSchema(v);
91
+ }
92
+ const out: JSONSchema = { type: "object", properties };
93
+ if (required.length > 0) out.required = required;
94
+ return out;
95
+ }
96
+
97
+ function itemToSchema(item: "text" | Record<string, SectionSpec>): JSONSchema {
98
+ if (item === "text") return { type: "string", minLength: 1 };
99
+ return objectKindToSchema(item);
100
+ }
101
+
102
+ export type PlanValidator = (
103
+ data: unknown,
104
+ ) => { valid: true } | { valid: false; diff: PartialStateDiff };
105
+
106
+ // Compile a PlanTemplate into a runnable validator. AJV runs first; the
107
+ // steps[].blocks structural pass appends extra errors when relevant.
108
+ export function compilePlanTemplate(template: PlanTemplate): PlanValidator {
109
+ const schema = generatePlanSchema(template);
110
+ const ajv = compileSchema(schema);
111
+ const stepsKey = findStepsSectionKey(template);
112
+ return (data: unknown) => {
113
+ const ajvResult = ajv(data);
114
+ const errors: ErrorEntry[] = ajvResult.valid ? [] : [...ajvResult.diff.errors];
115
+ if (stepsKey) {
116
+ errors.push(...validateStepTitleOrAdopt(data, stepsKey));
117
+ errors.push(...validateStepBlocks(data, stepsKey));
118
+ }
119
+ if (errors.length === 0) return { valid: true };
120
+ return { valid: false, diff: { errors, current: data } };
121
+ };
122
+ }
123
+
124
+ function findStepsSectionKey(template: PlanTemplate): string | undefined {
125
+ for (const [k, v] of Object.entries(template.sections)) {
126
+ if (v.kind === "steps") return k;
127
+ }
128
+ return undefined;
129
+ }
130
+
131
+ // Each step must declare either `title` (fresh spawn) or `existing_seed`
132
+ // (adoption). Title is optional only when existing_seed is set, since the
133
+ // adopted seed's title is preserved verbatim. Synthesis-style plans (warren
134
+ // §11.Q) where every child is an adoption can therefore omit titles entirely.
135
+ // (sprout-5583)
136
+ function validateStepTitleOrAdopt(data: unknown, sectionKey: string): ErrorEntry[] {
137
+ const sections = (data as { sections?: unknown })?.sections;
138
+ if (!sections || typeof sections !== "object") return [];
139
+ const steps = (sections as Record<string, unknown>)[sectionKey];
140
+ if (!Array.isArray(steps)) return [];
141
+ const errors: ErrorEntry[] = [];
142
+ for (let i = 0; i < steps.length; i++) {
143
+ const step = steps[i];
144
+ if (!step || typeof step !== "object") continue;
145
+ const title = (step as { title?: unknown }).title;
146
+ const adopt = (step as { existing_seed?: unknown }).existing_seed;
147
+ const hasTitle = typeof title === "string" && title.length > 0;
148
+ const hasAdopt = typeof adopt === "string" && adopt.length > 0;
149
+ if (!hasTitle && !hasAdopt) {
150
+ errors.push({
151
+ path: `sections.${sectionKey}.${i}`,
152
+ code: "missing-title",
153
+ fix: `step ${i + 1} must declare either 'title' (fresh spawn) or 'existing_seed' (adoption)`,
154
+ });
155
+ }
156
+ }
157
+ return errors;
158
+ }
159
+
160
+ // step.blocks values are 1-based: step 1 is the first step, step N is the
161
+ // last (sprout-185f). Internal `plan_step_index` on spawned children stays
162
+ // 0-based — it's a code-level back-link, not author-facing.
163
+ function validateStepBlocks(data: unknown, sectionKey: string): ErrorEntry[] {
164
+ const sections = (data as { sections?: unknown })?.sections;
165
+ if (!sections || typeof sections !== "object") return [];
166
+ const steps = (sections as Record<string, unknown>)[sectionKey];
167
+ if (!Array.isArray(steps)) return [];
168
+ const errors: ErrorEntry[] = [];
169
+ const len = steps.length;
170
+ for (let i = 0; i < len; i++) {
171
+ const step = steps[i];
172
+ if (!step || typeof step !== "object") continue;
173
+ const blocks = (step as { blocks?: unknown }).blocks;
174
+ if (!Array.isArray(blocks)) continue;
175
+ const stepLabel = i + 1;
176
+ for (const b of blocks) {
177
+ if (typeof b !== "number" || !Number.isInteger(b)) continue;
178
+ if (b === stepLabel) {
179
+ errors.push({
180
+ path: `sections.${sectionKey}.${i}.blocks`,
181
+ code: "self-reference",
182
+ fix: `step ${stepLabel} cannot block itself; remove ${b} from blocks`,
183
+ });
184
+ } else if (b < 1 || b > len) {
185
+ errors.push({
186
+ path: `sections.${sectionKey}.${i}.blocks`,
187
+ code: "out-of-range",
188
+ fix: `step index ${b} is out of range (step indices are 1-based; valid range 1..${len})`,
189
+ });
190
+ }
191
+ }
192
+ }
193
+ return errors;
194
+ }
195
+
196
+ // Map seed type -> default template name (PLAN_SPEC.md:271, 429). Hard-coded
197
+ // per the answer to open question 4. `--template` always overrides. `refactor`
198
+ // is intentionally opt-in via `--template` only; see sprout-6730 / the header
199
+ // comment on BUILTIN_REFACTOR_TEMPLATE.
200
+ const TYPE_DEFAULTS: Record<string, string> = {
201
+ task: "feature",
202
+ bug: "bug",
203
+ feature: "feature",
204
+ epic: "feature",
205
+ };
206
+
207
+ export function defaultTemplateForType(type: string): string {
208
+ return TYPE_DEFAULTS[type] ?? "feature";
209
+ }
@@ -0,0 +1,31 @@
1
+ import type { Issue } from "./types.ts";
2
+
3
+ export type SortMode = "priority" | "created" | "updated" | "id";
4
+
5
+ export const VALID_SORT_MODES: readonly SortMode[] = ["priority", "created", "updated", "id"];
6
+
7
+ export function isSortMode(value: string): value is SortMode {
8
+ return (VALID_SORT_MODES as readonly string[]).includes(value);
9
+ }
10
+
11
+ export function sortIssues(issues: Issue[], mode: SortMode = "priority"): Issue[] {
12
+ const sorted = [...issues];
13
+ switch (mode) {
14
+ case "priority":
15
+ sorted.sort((a, b) => {
16
+ if (a.priority !== b.priority) return a.priority - b.priority;
17
+ return b.createdAt.localeCompare(a.createdAt);
18
+ });
19
+ break;
20
+ case "created":
21
+ sorted.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
22
+ break;
23
+ case "updated":
24
+ sorted.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
25
+ break;
26
+ case "id":
27
+ sorted.sort((a, b) => a.id.localeCompare(b.id));
28
+ break;
29
+ }
30
+ return sorted;
31
+ }