@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,204 @@
1
+ #!/usr/bin/env bun
2
+ import chalk from "chalk";
3
+ import { Command, Help } from "commander";
4
+ import { brand, muted, setQuiet } from "./output.ts";
5
+ import { VERSION } from "./version.ts";
6
+
7
+ export { VERSION };
8
+
9
+ // Apply quiet mode early so it affects all output during command execution
10
+ const rawArgs = process.argv.slice(2);
11
+ if (rawArgs.includes("--quiet") || rawArgs.includes("-q")) {
12
+ setQuiet(true);
13
+ }
14
+
15
+ const program = new Command();
16
+
17
+ program
18
+ .name("sr")
19
+ .description("sprout — git-native issue tracker")
20
+ .version(VERSION, "-v, --version", "Print version")
21
+ .option("-q, --quiet", "Suppress non-error output")
22
+ .option("--verbose", "Extra diagnostic output")
23
+ .option("--timing", "Show command execution time")
24
+ .addHelpCommand(false)
25
+ .configureHelp({
26
+ formatHelp(cmd: Command, helper: Help): string {
27
+ if (cmd.parent) {
28
+ return Help.prototype.formatHelp.call(helper, cmd, helper);
29
+ }
30
+ const header = `${brand(chalk.bold("sprout"))} ${muted(`v${VERSION}`)} — Git-native issue tracking\n\nUsage: sr <command> [options]`;
31
+
32
+ const cmdLines: string[] = ["\nCommands:"];
33
+ for (const sub of cmd.commands) {
34
+ const name = sub.name();
35
+ const argStr = sub.registeredArguments
36
+ .map((a) => (a.required ? `<${a.name()}>` : `[${a.name()}]`))
37
+ .join(" ");
38
+ const rawEntry = argStr ? `${name} ${argStr}` : name;
39
+ const colored = argStr ? `${chalk.green(name)} ${chalk.dim(argStr)}` : chalk.green(name);
40
+ const pad = " ".repeat(Math.max(18 - rawEntry.length, 2));
41
+ cmdLines.push(` ${colored}${pad}${sub.description()}`);
42
+ }
43
+
44
+ const opts: [string, string][] = [
45
+ ["-h, --help", "Show help"],
46
+ ["-v, --version", "Print version"],
47
+ ["--format <mode>", "Output format (markdown|compact|plain|ids|json)"],
48
+ ["--json", "Alias for --format json"],
49
+ ["-q, --quiet", "Suppress non-error output"],
50
+ ["--verbose", "Extra diagnostic output"],
51
+ ["--timing", "Show command execution time"],
52
+ ];
53
+ const optLines: string[] = ["\nOptions:"];
54
+ for (const [flag, desc] of opts) {
55
+ const pad = " ".repeat(Math.max(18 - flag.length, 2));
56
+ optLines.push(` ${chalk.dim(flag)}${pad}${desc}`);
57
+ }
58
+
59
+ const footer = `\nRun '${chalk.dim("sr")} <command> --help' for command-specific help.`;
60
+
61
+ return `${[header, ...cmdLines, ...optLines, footer].join("\n")}\n`;
62
+ },
63
+ });
64
+
65
+ // --timing: measure command execution time
66
+ let timingStart = 0;
67
+ program.hook("preAction", () => {
68
+ if (program.opts().timing) {
69
+ timingStart = performance.now();
70
+ }
71
+ });
72
+ program.hook("postAction", () => {
73
+ if (program.opts().timing) {
74
+ const elapsed = performance.now() - timingStart;
75
+ const formatted =
76
+ elapsed < 1000 ? `${Math.round(elapsed)}ms` : `${(elapsed / 1000).toFixed(2)}s`;
77
+ process.stderr.write(`${muted(`⏱ ${formatted}`)}\n`);
78
+ }
79
+ });
80
+
81
+ // Lazy-load and register all commands
82
+ async function registerAll(): Promise<void> {
83
+ const mods = await Promise.all([
84
+ import("./commands/init.ts"),
85
+ import("./commands/create.ts"),
86
+ import("./commands/show.ts"),
87
+ import("./commands/list.ts"),
88
+ import("./commands/ready.ts"),
89
+ import("./commands/search.ts"),
90
+ import("./commands/update.ts"),
91
+ import("./commands/close.ts"),
92
+ import("./commands/dep.ts"),
93
+ import("./commands/label.ts"),
94
+ import("./commands/blocked.ts"),
95
+ import("./commands/stats.ts"),
96
+ import("./commands/sync.ts"),
97
+ import("./commands/doctor.ts"),
98
+ import("./commands/tpl.ts"),
99
+ import("./commands/migrate.ts"),
100
+ import("./commands/prime.ts"),
101
+ import("./commands/onboard.ts"),
102
+ import("./commands/upgrade.ts"),
103
+ import("./commands/completions.ts"),
104
+ import("./commands/block.ts"),
105
+ import("./commands/unblock.ts"),
106
+ import("./commands/plan.ts"),
107
+ import("./commands/config.ts"),
108
+ ]);
109
+
110
+ for (const mod of mods) {
111
+ mod.register(program);
112
+ }
113
+ }
114
+
115
+ function levenshtein(a: string, b: string): number {
116
+ const m = a.length;
117
+ const n = b.length;
118
+ if (m === 0) return n;
119
+ if (n === 0) return m;
120
+ let prev: number[] = Array.from({ length: n + 1 }, (_, j) => j);
121
+ for (let i = 1; i <= m; i++) {
122
+ const curr: number[] = [i];
123
+ for (let j = 1; j <= n; j++) {
124
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
125
+ const left = curr[j - 1] ?? 0;
126
+ const up = prev[j] ?? 0;
127
+ const diag = prev[j - 1] ?? 0;
128
+ curr.push(Math.min(left + 1, up + 1, diag + cost));
129
+ }
130
+ prev = curr;
131
+ }
132
+ return prev[n] ?? 0;
133
+ }
134
+
135
+ async function main(): Promise<void> {
136
+ // Handle --version --json before Commander processes the flag
137
+ if ((rawArgs.includes("-v") || rawArgs.includes("--version")) && rawArgs.includes("--json")) {
138
+ const platform = `${process.platform}-${process.arch}`;
139
+ console.log(
140
+ JSON.stringify({ name: "@ag-eco/sprout-cli", version: VERSION, runtime: "bun", platform }),
141
+ );
142
+ process.exitCode = 0;
143
+ return;
144
+ }
145
+
146
+ await registerAll();
147
+
148
+ // Check for unknown commands before parsing
149
+ const firstArg = process.argv[2];
150
+ if (firstArg && !firstArg.startsWith("-")) {
151
+ const knownNames = program.commands.map((c) => c.name());
152
+ if (!knownNames.includes(firstArg)) {
153
+ let best = "";
154
+ let bestDist = Number.POSITIVE_INFINITY;
155
+ for (const name of knownNames) {
156
+ const d = levenshtein(firstArg, name);
157
+ if (d < bestDist) {
158
+ bestDist = d;
159
+ best = name;
160
+ }
161
+ }
162
+ const suggestion = bestDist <= 2 ? best : "";
163
+ const errMsg = suggestion
164
+ ? `Unknown command: ${firstArg}. Did you mean ${suggestion}?`
165
+ : `Unknown command: ${firstArg}`;
166
+ if (jsonMode) {
167
+ const payload: Record<string, unknown> = {
168
+ success: false,
169
+ command: firstArg,
170
+ error: errMsg,
171
+ };
172
+ if (suggestion) payload.suggestion = suggestion;
173
+ await Bun.write(Bun.stdout, `${JSON.stringify(payload)}\n`);
174
+ } else {
175
+ process.stderr.write(`${errMsg}\n`);
176
+ }
177
+ process.exitCode = 1;
178
+ return;
179
+ }
180
+ }
181
+
182
+ await program.parseAsync(process.argv);
183
+ }
184
+
185
+ const jsonMode = process.argv.includes("--json");
186
+
187
+ main().catch(async (err: unknown) => {
188
+ const msg = err instanceof Error ? err.message : String(err);
189
+ const cmd = process.argv[2];
190
+ // Route the structured error (with stack) through pino for observability.
191
+ // Lazy-imported so the happy path never constructs a logger; silent at the
192
+ // default `info` level and surfaces under SPROUT_DEBUG=1.
193
+ const { log } = await import("./log.ts");
194
+ log.debug({ err, cmd }, "command failed");
195
+ if (jsonMode) {
196
+ await Bun.write(
197
+ Bun.stdout,
198
+ `${JSON.stringify({ success: false, command: cmd, error: msg })}\n`,
199
+ );
200
+ } else {
201
+ console.error(chalk.red(`Error: ${msg}`));
202
+ }
203
+ process.exitCode = 1;
204
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Structured logging for the sprout CLI (pino).
3
+ *
4
+ * This is the observability channel — diagnostic/debug output that survives as
5
+ * machine-parseable NDJSON in CI and non-TTY contexts, and renders prettily in
6
+ * an interactive shell. It is NOT the user-output channel: deliberate,
7
+ * chalk-formatted CLI prints live in `src/output.ts` and must stay there.
8
+ *
9
+ * Level resolution lives inside `createLog()` (never read at module top) so
10
+ * tests can override `process.env` and re-create a logger deterministically:
11
+ * - explicit `SPROUT_LOG_LEVEL` wins,
12
+ * - `SPROUT_DEBUG === "1"` flips the default to `debug`,
13
+ * - otherwise `info`.
14
+ *
15
+ * Sensitive values are scrubbed via pino `redact` with `remove: true` (the key
16
+ * is dropped, not masked) before any line reaches stdout.
17
+ */
18
+
19
+ import pino from "pino";
20
+
21
+ export type Logger = pino.Logger;
22
+
23
+ export interface CreateLogOptions {
24
+ level?: pino.Level;
25
+ /** Force pretty (true) or NDJSON (false). Defaults to TTY auto-detection. */
26
+ pretty?: boolean;
27
+ /** Custom sink — bypasses the pretty transport (used by tests). */
28
+ destination?: pino.DestinationStream;
29
+ }
30
+
31
+ // Paths whose values never belong in logs. Bare keys cover top-level fields;
32
+ // `*.<key>` wildcards cover one level of nesting (e.g. `req.token`). Mirrors
33
+ // burrow's redact policy. Documented in AGENTS.md (redact policy).
34
+ export const REDACT_PATHS: readonly string[] = [
35
+ "password",
36
+ "token",
37
+ "apiKey",
38
+ "secret",
39
+ "authorization",
40
+ "*.password",
41
+ "*.token",
42
+ "*.apiKey",
43
+ "*.secret",
44
+ "*.authorization",
45
+ "headers.cookie",
46
+ "headers.authorization",
47
+ ];
48
+
49
+ function resolveLevel(): pino.Level {
50
+ const explicit = process.env.SPROUT_LOG_LEVEL as pino.Level | undefined;
51
+ if (explicit) return explicit;
52
+ if (process.env.SPROUT_DEBUG === "1") return "debug";
53
+ return "info";
54
+ }
55
+
56
+ export function createLog(options: CreateLogOptions = {}): Logger {
57
+ const level = options.level ?? resolveLevel();
58
+ const pretty = options.pretty ?? process.stdout.isTTY === true;
59
+
60
+ const base: pino.LoggerOptions = {
61
+ level,
62
+ redact: { paths: [...REDACT_PATHS], remove: true },
63
+ };
64
+
65
+ if (pretty && !options.destination) {
66
+ base.transport = {
67
+ target: "pino-pretty",
68
+ options: { colorize: true, translateTime: "SYS:HH:MM:ss.l" },
69
+ };
70
+ return pino(base);
71
+ }
72
+
73
+ return options.destination ? pino(base, options.destination) : pino(base);
74
+ }
75
+
76
+ export const log: Logger = createLog();
@@ -0,0 +1,22 @@
1
+ export const START_MARKER = "<!-- sprout:start -->";
2
+ export const END_MARKER = "<!-- sprout:end -->";
3
+
4
+ export function hasMarkerSection(content: string): boolean {
5
+ return content.includes(START_MARKER) && content.includes(END_MARKER);
6
+ }
7
+
8
+ export function replaceMarkerSection(content: string, newSection: string): string | null {
9
+ const startIdx = content.indexOf(START_MARKER);
10
+ const endIdx = content.indexOf(END_MARKER);
11
+ if (startIdx === -1 || endIdx === -1) return null;
12
+ // Markers present but out of order (END before START) means the file was
13
+ // hand-edited into a broken state. Refuse rather than emit garbage.
14
+ if (endIdx < startIdx) return null;
15
+ const before = content.slice(0, startIdx);
16
+ const after = content.slice(endIdx + END_MARKER.length);
17
+ return before + wrapInMarkers(newSection) + after;
18
+ }
19
+
20
+ export function wrapInMarkers(section: string): string {
21
+ return `${START_MARKER}\n${section}\n${END_MARKER}`;
22
+ }
@@ -0,0 +1,121 @@
1
+ import chalk from "chalk";
2
+ import { stripAnsi } from "./format.ts";
3
+ import type { Issue } from "./types.ts";
4
+ import { PRIORITY_LABELS } from "./types.ts";
5
+
6
+ // Forest palette
7
+ export const brand = chalk.rgb(124, 179, 66);
8
+ export const accent = chalk.rgb(255, 183, 77);
9
+ export const muted = chalk.rgb(120, 120, 110);
10
+
11
+ let _quiet = false;
12
+
13
+ export function setQuiet(v: boolean): void {
14
+ _quiet = v;
15
+ }
16
+
17
+ export async function outputJson(data: unknown): Promise<void> {
18
+ await Bun.write(Bun.stdout, `${JSON.stringify(data, null, 2)}\n`);
19
+ }
20
+
21
+ export function printSuccess(msg: string): void {
22
+ if (_quiet) return;
23
+ console.log(`${brand("✓")} ${brand(msg)}`);
24
+ }
25
+
26
+ export function printError(msg: string): void {
27
+ console.error(`${chalk.red("✗")} ${msg}`);
28
+ }
29
+
30
+ export function printWarning(msg: string): void {
31
+ if (_quiet) return;
32
+ console.error(`${chalk.yellow("!")} ${msg}`);
33
+ }
34
+
35
+ // An issue is *effectively* blocked when at least one entry in `blockedBy`
36
+ // references a still-open issue. When `closedBlockerIds` is omitted, fall back
37
+ // to the legacy length-based heuristic for callers that haven't threaded the
38
+ // set through yet.
39
+ function isEffectivelyBlocked(issue: Issue, closedBlockerIds?: Set<string>): boolean {
40
+ const blockers = issue.blockedBy ?? [];
41
+ if (blockers.length === 0) return false;
42
+ if (!closedBlockerIds) return true;
43
+ return blockers.some((bid) => !closedBlockerIds.has(bid));
44
+ }
45
+
46
+ export function formatIssueOneLine(issue: Issue, closedBlockerIds?: Set<string>): string {
47
+ const isBlocked = isEffectivelyBlocked(issue, closedBlockerIds);
48
+ const statusIcon =
49
+ issue.status === "closed"
50
+ ? muted("x")
51
+ : issue.status === "in_progress"
52
+ ? chalk.cyan(">")
53
+ : isBlocked
54
+ ? chalk.yellow("!")
55
+ : brand("-");
56
+ const priorityLabel = PRIORITY_LABELS[issue.priority] ?? String(issue.priority);
57
+ const assignee = issue.assignee ? ` · ${muted(`@${issue.assignee}`)}` : "";
58
+ const blocked = isBlocked ? ` ${chalk.yellow("[blocked]")}` : "";
59
+ const labelStr = issue.labels?.length ? ` ${muted(`{${issue.labels.join(", ")}}`)}` : "";
60
+ return `${statusIcon} ${accent.bold(issue.id)} · ${issue.title} ${muted(`[${priorityLabel} · ${issue.type}]`)}${assignee}${blocked}${labelStr}`;
61
+ }
62
+
63
+ export function formatIssueOneLineCompact(issue: Issue, closedBlockerIds?: Set<string>): string {
64
+ const priorityLabel = PRIORITY_LABELS[issue.priority] ?? String(issue.priority);
65
+ const isBlocked = isEffectivelyBlocked(issue, closedBlockerIds);
66
+ const status = isBlocked ? "blocked" : issue.status;
67
+ return `${issue.id} ${priorityLabel} ${status} ${issue.title}`;
68
+ }
69
+
70
+ export function printIssueOneLine(issue: Issue, closedBlockerIds?: Set<string>): void {
71
+ if (_quiet) return;
72
+ console.log(formatIssueOneLine(issue, closedBlockerIds));
73
+ }
74
+
75
+ // Render Issue.extensions as a single "Extensions: key=value ..." line.
76
+ // Each value is JSON-encoded so the rendering round-trips unambiguously
77
+ // (strings stay quoted; objects/arrays/null print as JSON literals).
78
+ // Returns null when extensions is missing or has no own keys.
79
+ export function formatExtensionsLine(ext: Record<string, unknown> | undefined): string | null {
80
+ if (!ext) return null;
81
+ const keys = Object.keys(ext);
82
+ if (keys.length === 0) return null;
83
+ const pairs = keys.map((k) => `${accent(k)}=${muted(JSON.stringify(ext[k]))}`);
84
+ return `Extensions: ${pairs.join(" ")}`;
85
+ }
86
+
87
+ export function formatIssueFull(issue: Issue): string {
88
+ const statusColor =
89
+ issue.status === "closed" ? muted : issue.status === "in_progress" ? chalk.cyan : brand;
90
+ const priorityLabel = PRIORITY_LABELS[issue.priority] ?? String(issue.priority);
91
+
92
+ const lines: string[] = [];
93
+ lines.push(`${accent.bold(issue.id)} ${statusColor(issue.status)}`);
94
+ lines.push(`Title: ${issue.title}`);
95
+ lines.push(`Type: ${muted(issue.type)} Priority: ${muted(priorityLabel)}`);
96
+ if (issue.assignee) lines.push(`Assignee: ${issue.assignee}`);
97
+ if (issue.labels?.length)
98
+ lines.push(`Labels: ${issue.labels.map((l) => accent(l)).join(", ")}`);
99
+ const extLine = formatExtensionsLine(issue.extensions);
100
+ if (extLine) lines.push(extLine);
101
+ if (issue.description) lines.push(`\n${issue.description}`);
102
+ if (issue.blockedBy?.length)
103
+ lines.push(`Blocked by: ${issue.blockedBy.map((id) => accent(id)).join(", ")}`);
104
+ if (issue.blocks?.length)
105
+ lines.push(`Blocks: ${issue.blocks.map((id) => accent(id)).join(", ")}`);
106
+ if (issue.convoy) lines.push(`Convoy: ${muted(issue.convoy)}`);
107
+ if (issue.closeReason) lines.push(`Reason: ${issue.closeReason}`);
108
+ lines.push(`Created: ${muted(issue.createdAt)}`);
109
+ lines.push(`Updated: ${muted(issue.updatedAt)}`);
110
+ if (issue.closedAt) lines.push(`Closed: ${muted(issue.closedAt)}`);
111
+ return lines.join("\n");
112
+ }
113
+
114
+ export function printIssueFull(issue: Issue): void {
115
+ if (_quiet) return;
116
+ console.log(formatIssueFull(issue));
117
+ }
118
+
119
+ export function plain(s: string): string {
120
+ return stripAnsi(s);
121
+ }
@@ -0,0 +1,93 @@
1
+ // sprout-76af: child sprout spawned by `sr plan submit` carry a backref block in
2
+ // description so an agent picking one up cold has the framing the plan author
3
+ // already wrote down — without having to navigate plan_id → parent → plan.
4
+
5
+ export const BACKREF_START = "<!-- sprout:plan-backref:start -->";
6
+ export const BACKREF_END = "<!-- sprout:plan-backref:end -->";
7
+
8
+ const APPROACH_EXCERPT_MAX = 240;
9
+
10
+ export interface BackrefArgs {
11
+ // 0-based step index in plan.sections.steps. Omit for loose adoptions
12
+ // (sr plan adopt without --step) where the seed has no step anchor.
13
+ stepIndex?: number;
14
+ planId: string;
15
+ parentSeedId: string;
16
+ parentSeedTitle: string;
17
+ templateName: string;
18
+ approach: unknown;
19
+ }
20
+
21
+ export function buildPlanBackref(args: BackrefArgs): string {
22
+ const lines: string[] = [];
23
+ if (args.stepIndex !== undefined) {
24
+ lines.push(`Step ${args.stepIndex + 1} of plan ${args.planId}.`);
25
+ } else {
26
+ lines.push(`Adopted into plan ${args.planId}.`);
27
+ }
28
+ lines.push("");
29
+ lines.push(`Parent seed: ${args.parentSeedId} — ${args.parentSeedTitle}`);
30
+ lines.push(`Plan template: ${args.templateName}`);
31
+ const excerpt = approachExcerpt(args.approach);
32
+ if (excerpt) lines.push(`Plan approach: ${excerpt}`);
33
+ lines.push("");
34
+ lines.push(
35
+ `Run \`sr plan show ${args.planId}\` for the full plan (context, alternatives, sibling steps, acceptance criteria).`,
36
+ );
37
+ const body = lines.join("\n");
38
+ return `${BACKREF_START}\n${body}\n${BACKREF_END}`;
39
+ }
40
+
41
+ // Replace the marker section if present; otherwise, prepend a fresh section in
42
+ // front of the existing description so manual notes survive plan overwrite.
43
+ export function applyPlanBackref(existing: string | undefined, args: BackrefArgs): string {
44
+ const block = buildPlanBackref(args);
45
+ const prior = existing ?? "";
46
+ if (hasBackrefMarkers(prior)) {
47
+ return replaceBackrefSection(prior, block);
48
+ }
49
+ if (prior.trim().length === 0) return block;
50
+ return `${block}\n\n${prior}`;
51
+ }
52
+
53
+ // Inverse of applyPlanBackref: strip the marker-delimited block so a released
54
+ // seed loses its plan framing. Manual notes wrapping the block are preserved;
55
+ // whitespace at the new boundary is collapsed. Returns undefined when nothing
56
+ // but the block remained, so the caller can drop the field entirely.
57
+ export function stripPlanBackref(existing: string | undefined): string | undefined {
58
+ if (existing === undefined) return undefined;
59
+ if (!hasBackrefMarkers(existing)) return existing;
60
+ const startIdx = existing.indexOf(BACKREF_START);
61
+ const endIdx = existing.indexOf(BACKREF_END);
62
+ if (startIdx === -1 || endIdx === -1) return existing;
63
+ const before = existing.slice(0, startIdx).replace(/\s*$/, "");
64
+ const after = existing.slice(endIdx + BACKREF_END.length).replace(/^\s*/, "");
65
+ if (before.length === 0 && after.length === 0) return undefined;
66
+ if (before.length === 0) return after;
67
+ if (after.length === 0) return before;
68
+ return `${before}\n\n${after}`;
69
+ }
70
+
71
+ function hasBackrefMarkers(s: string): boolean {
72
+ return s.includes(BACKREF_START) && s.includes(BACKREF_END);
73
+ }
74
+
75
+ function replaceBackrefSection(existing: string, block: string): string {
76
+ const startIdx = existing.indexOf(BACKREF_START);
77
+ const endIdx = existing.indexOf(BACKREF_END);
78
+ if (startIdx === -1 || endIdx === -1) return block;
79
+ const before = existing.slice(0, startIdx);
80
+ const after = existing.slice(endIdx + BACKREF_END.length);
81
+ return `${before}${block}${after}`;
82
+ }
83
+
84
+ function approachExcerpt(value: unknown): string {
85
+ if (typeof value !== "string") return "";
86
+ const collapsed = value.replace(/\s+/g, " ").trim();
87
+ if (collapsed.length === 0) return "";
88
+ if (collapsed.length <= APPROACH_EXCERPT_MAX) return collapsed;
89
+ const slice = collapsed.slice(0, APPROACH_EXCERPT_MAX);
90
+ const lastSpace = slice.lastIndexOf(" ");
91
+ const cut = lastSpace > APPROACH_EXCERPT_MAX / 2 ? slice.slice(0, lastSpace) : slice;
92
+ return `${cut.trimEnd()}…`;
93
+ }
@@ -0,0 +1,81 @@
1
+ // Shared plan-awareness helpers for ready/show/list integration
2
+ // (PLAN_SPEC.md:154-156). Each consumer reads plans + issues once and uses
3
+ // these helpers to look up plan state and render hints/summaries.
4
+
5
+ import { accent, muted } from "./output.ts";
6
+ import { readPlans } from "./store.ts";
7
+ import type { Issue, Plan } from "./types.ts";
8
+
9
+ export interface PlanContext {
10
+ plansById: Map<string, Plan>;
11
+ plansBySeed: Map<string, Plan>;
12
+ }
13
+
14
+ export async function loadPlanContext(sproutDir: string): Promise<PlanContext> {
15
+ const plans = await readPlans(sproutDir);
16
+ const plansById = new Map(plans.map((p) => [p.id, p]));
17
+ const plansBySeed = new Map<string, Plan>();
18
+ for (const p of plans) {
19
+ // If multiple plans exist for the same seed (would only happen via prior
20
+ // drafts), prefer the most-recently-updated one for surfacing.
21
+ const existing = plansBySeed.get(p.seed);
22
+ if (!existing || existing.updatedAt < p.updatedAt) plansBySeed.set(p.seed, p);
23
+ }
24
+ return { plansById, plansBySeed };
25
+ }
26
+
27
+ export function planForIssue(ctx: PlanContext, issue: Issue): Plan | undefined {
28
+ if (!issue.plan_id) return undefined;
29
+ return ctx.plansById.get(issue.plan_id);
30
+ }
31
+
32
+ // Human-readable suffix appended to one-line issue formatting in list/ready.
33
+ export function planLineSuffix(plan: Plan | undefined): string {
34
+ if (!plan) return "";
35
+ if (plan.status === "draft") {
36
+ return ` ${accent("[plan in draft — run sr plan submit]")}`;
37
+ }
38
+ return ` ${muted(`[plan ${plan.status}]`)}`;
39
+ }
40
+
41
+ // Returns true if `sr ready` should surface this seed regardless of normal
42
+ // blocker checks. Planning takes precedence over implementation when a seed
43
+ // has a draft plan attached.
44
+ export function isPlanDraftBlocking(plan: Plan | undefined): boolean {
45
+ return plan?.status === "draft";
46
+ }
47
+
48
+ export interface ChildSummary {
49
+ id: string;
50
+ title: string;
51
+ status: string;
52
+ adopted: boolean;
53
+ }
54
+
55
+ // Shared --json shape used by list/ready/search/blocked when surfacing an
56
+ // issue that has a plan attached. Returns the issue unchanged when there is
57
+ // no plan, so the JSON payload remains a strict superset (no key churn for
58
+ // plan-less issues).
59
+ export function issueJsonWithPlan(
60
+ issue: Issue,
61
+ plan: Plan | undefined,
62
+ ): Issue & {
63
+ plan_status?: string;
64
+ plan_children?: string[];
65
+ } {
66
+ if (!plan) return issue;
67
+ return { ...issue, plan_status: plan.status, plan_children: plan.children };
68
+ }
69
+
70
+ export function summarisePlanChildren(plan: Plan, issues: Issue[]): ChildSummary[] {
71
+ const adoptedSet = new Set(plan.adoptedChildren ?? []);
72
+ return plan.children.map((id) => {
73
+ const issue = issues.find((i) => i.id === id);
74
+ return {
75
+ id,
76
+ title: issue?.title ?? "(missing)",
77
+ status: issue?.status ?? "missing",
78
+ adopted: adoptedSet.has(id),
79
+ };
80
+ });
81
+ }