@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,979 @@
1
+ import { existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import chalk from "chalk";
4
+ import type { Command } from "commander";
5
+ import { findSproutDir, projectRootFromSproutDir, readConfig } from "../config.ts";
6
+ import { brand, muted, outputJson } from "../output.ts";
7
+ import { readIssues } from "../store.ts";
8
+ import type { Issue, Template } from "../types.ts";
9
+ import {
10
+ ISSUES_FILE,
11
+ LOCK_STALE_MS,
12
+ TEMPLATES_FILE,
13
+ VALID_STATUSES,
14
+ VALID_TYPES,
15
+ } from "../types.ts";
16
+
17
+ interface DoctorCheck {
18
+ name: string;
19
+ status: "pass" | "warn" | "fail";
20
+ message: string;
21
+ details: string[];
22
+ fixable: boolean;
23
+ }
24
+
25
+ interface RawLine {
26
+ lineNumber: number;
27
+ text: string;
28
+ parsed?: unknown;
29
+ error?: string;
30
+ }
31
+
32
+ function readRawLines(filePath: string): RawLine[] {
33
+ if (!existsSync(filePath)) return [];
34
+ const content = readFileSync(filePath, "utf8");
35
+ const lines: RawLine[] = [];
36
+ for (const [i, raw] of content.split("\n").entries()) {
37
+ const text = raw.trim();
38
+ if (!text) continue;
39
+ try {
40
+ lines.push({ lineNumber: i + 1, text, parsed: JSON.parse(text) });
41
+ } catch (err: unknown) {
42
+ const msg = err instanceof Error ? err.message : String(err);
43
+ lines.push({ lineNumber: i + 1, text, error: msg });
44
+ }
45
+ }
46
+ return lines;
47
+ }
48
+
49
+ function checkConfig(sproutDir: string, config: { project: string } | null): DoctorCheck {
50
+ if (!existsSync(sproutDir)) {
51
+ return {
52
+ name: "config",
53
+ status: "fail",
54
+ message: ".sprout/ directory not found",
55
+ details: [],
56
+ fixable: false,
57
+ };
58
+ }
59
+ if (!config) {
60
+ return {
61
+ name: "config",
62
+ status: "fail",
63
+ message: "config.yaml is missing or unparseable",
64
+ details: [],
65
+ fixable: false,
66
+ };
67
+ }
68
+ if (!config.project) {
69
+ return {
70
+ name: "config",
71
+ status: "fail",
72
+ message: "config.yaml missing required 'project' field",
73
+ details: [],
74
+ fixable: false,
75
+ };
76
+ }
77
+ return {
78
+ name: "config",
79
+ status: "pass",
80
+ message: "Config is valid",
81
+ details: [],
82
+ fixable: false,
83
+ };
84
+ }
85
+
86
+ function checkJsonlIntegrity(sproutDir: string): DoctorCheck {
87
+ const details: string[] = [];
88
+ for (const file of [ISSUES_FILE, TEMPLATES_FILE]) {
89
+ const lines = readRawLines(join(sproutDir, file));
90
+ for (const line of lines) {
91
+ if (line.error) {
92
+ details.push(`${file} line ${String(line.lineNumber)}: ${line.error}`);
93
+ }
94
+ }
95
+ }
96
+ if (details.length > 0) {
97
+ return {
98
+ name: "jsonl-integrity",
99
+ status: "fail",
100
+ message: `${String(details.length)} malformed line(s) in JSONL files`,
101
+ details,
102
+ fixable: true,
103
+ };
104
+ }
105
+ return {
106
+ name: "jsonl-integrity",
107
+ status: "pass",
108
+ message: "All JSONL lines parse correctly",
109
+ details: [],
110
+ fixable: false,
111
+ };
112
+ }
113
+
114
+ function checkSchemaValidation(sproutDir: string): DoctorCheck {
115
+ const details: string[] = [];
116
+ const lines = readRawLines(join(sproutDir, ISSUES_FILE));
117
+ for (const line of lines) {
118
+ if (!line.parsed) continue;
119
+ const issue = line.parsed as Record<string, unknown>;
120
+ const id = typeof issue.id === "string" ? issue.id : `line ${String(line.lineNumber)}`;
121
+ if (!issue.id || typeof issue.id !== "string") {
122
+ details.push(`${id}: missing or invalid 'id'`);
123
+ }
124
+ if (!issue.title || typeof issue.title !== "string") {
125
+ details.push(`${id}: missing or invalid 'title'`);
126
+ }
127
+ if (!issue.createdAt || typeof issue.createdAt !== "string") {
128
+ details.push(`${id}: missing or invalid 'createdAt'`);
129
+ }
130
+ if (!issue.updatedAt || typeof issue.updatedAt !== "string") {
131
+ details.push(`${id}: missing or invalid 'updatedAt'`);
132
+ }
133
+ if (
134
+ typeof issue.status === "string" &&
135
+ !(VALID_STATUSES as readonly string[]).includes(issue.status)
136
+ ) {
137
+ details.push(`${id}: invalid status '${issue.status}'`);
138
+ }
139
+ if (
140
+ typeof issue.type === "string" &&
141
+ !(VALID_TYPES as readonly string[]).includes(issue.type)
142
+ ) {
143
+ details.push(`${id}: invalid type '${issue.type}'`);
144
+ }
145
+ if (typeof issue.priority === "number" && (issue.priority < 0 || issue.priority > 4)) {
146
+ details.push(`${id}: invalid priority ${String(issue.priority)} (must be 0-4)`);
147
+ }
148
+ }
149
+ if (details.length > 0) {
150
+ return {
151
+ name: "schema-validation",
152
+ status: "fail",
153
+ message: `${String(details.length)} schema violation(s)`,
154
+ details,
155
+ fixable: false,
156
+ };
157
+ }
158
+ return {
159
+ name: "schema-validation",
160
+ status: "pass",
161
+ message: "All issues have valid schema",
162
+ details: [],
163
+ fixable: false,
164
+ };
165
+ }
166
+
167
+ function checkDuplicateIds(sproutDir: string): DoctorCheck {
168
+ const details: string[] = [];
169
+ for (const file of [ISSUES_FILE, TEMPLATES_FILE]) {
170
+ const lines = readRawLines(join(sproutDir, file));
171
+ const counts = new Map<string, number>();
172
+ for (const line of lines) {
173
+ if (!line.parsed) continue;
174
+ const item = line.parsed as { id?: string };
175
+ if (typeof item.id === "string") {
176
+ counts.set(item.id, (counts.get(item.id) ?? 0) + 1);
177
+ }
178
+ }
179
+ for (const [id, count] of counts) {
180
+ if (count > 1) {
181
+ details.push(`${id} appears ${String(count)} times in ${file}`);
182
+ }
183
+ }
184
+ }
185
+ if (details.length > 0) {
186
+ return {
187
+ name: "duplicate-ids",
188
+ status: "warn",
189
+ message: `${String(details.length)} duplicate ID(s) found`,
190
+ details,
191
+ fixable: true,
192
+ };
193
+ }
194
+ return {
195
+ name: "duplicate-ids",
196
+ status: "pass",
197
+ message: "No duplicate IDs",
198
+ details: [],
199
+ fixable: false,
200
+ };
201
+ }
202
+
203
+ function checkReferentialIntegrity(issues: Issue[]): DoctorCheck {
204
+ const ids = new Set(issues.map((i) => i.id));
205
+ const details: string[] = [];
206
+ for (const issue of issues) {
207
+ for (const ref of issue.blockedBy ?? []) {
208
+ if (!ids.has(ref)) {
209
+ details.push(`${issue.id}.blockedBy → ${ref} (not found)`);
210
+ }
211
+ }
212
+ for (const ref of issue.blocks ?? []) {
213
+ if (!ids.has(ref)) {
214
+ details.push(`${issue.id}.blocks → ${ref} (not found)`);
215
+ }
216
+ }
217
+ }
218
+ if (details.length > 0) {
219
+ return {
220
+ name: "referential-integrity",
221
+ status: "warn",
222
+ message: `${String(details.length)} dangling dependency reference(s)`,
223
+ details,
224
+ fixable: true,
225
+ };
226
+ }
227
+ return {
228
+ name: "referential-integrity",
229
+ status: "pass",
230
+ message: "All dependency references are valid",
231
+ details: [],
232
+ fixable: false,
233
+ };
234
+ }
235
+
236
+ function checkBidirectionalConsistency(issues: Issue[]): DoctorCheck {
237
+ const byId = new Map<string, Issue>();
238
+ for (const issue of issues) {
239
+ byId.set(issue.id, issue);
240
+ }
241
+ const details: string[] = [];
242
+ for (const issue of issues) {
243
+ for (const ref of issue.blockedBy ?? []) {
244
+ const target = byId.get(ref);
245
+ if (target && !(target.blocks ?? []).includes(issue.id)) {
246
+ details.push(`${issue.id}.blockedBy has ${ref}, but ${ref}.blocks missing ${issue.id}`);
247
+ }
248
+ }
249
+ for (const ref of issue.blocks ?? []) {
250
+ const target = byId.get(ref);
251
+ if (target && !(target.blockedBy ?? []).includes(issue.id)) {
252
+ details.push(`${issue.id}.blocks has ${ref}, but ${ref}.blockedBy missing ${issue.id}`);
253
+ }
254
+ }
255
+ }
256
+ if (details.length > 0) {
257
+ return {
258
+ name: "bidirectional-consistency",
259
+ status: "warn",
260
+ message: `${String(details.length)} bidirectional mismatch(es)`,
261
+ details,
262
+ fixable: true,
263
+ };
264
+ }
265
+ return {
266
+ name: "bidirectional-consistency",
267
+ status: "pass",
268
+ message: "All dependency links are bidirectional",
269
+ details: [],
270
+ fixable: false,
271
+ };
272
+ }
273
+
274
+ function checkCircularDependencies(issues: Issue[]): DoctorCheck {
275
+ const graph = new Map<string, string[]>();
276
+ for (const issue of issues) {
277
+ graph.set(issue.id, issue.blockedBy ?? []);
278
+ }
279
+ const visited = new Set<string>();
280
+ const inStack = new Set<string>();
281
+ const cycles: string[][] = [];
282
+
283
+ function dfs(node: string, path: string[]): void {
284
+ if (inStack.has(node)) {
285
+ const cycleStart = path.indexOf(node);
286
+ if (cycleStart >= 0) {
287
+ cycles.push(path.slice(cycleStart).concat(node));
288
+ }
289
+ return;
290
+ }
291
+ if (visited.has(node)) return;
292
+ visited.add(node);
293
+ inStack.add(node);
294
+ for (const dep of graph.get(node) ?? []) {
295
+ dfs(dep, [...path, node]);
296
+ }
297
+ inStack.delete(node);
298
+ }
299
+
300
+ for (const id of graph.keys()) {
301
+ dfs(id, []);
302
+ }
303
+
304
+ if (cycles.length > 0) {
305
+ const details = cycles.map((cycle) => cycle.join(" → "));
306
+ return {
307
+ name: "circular-dependencies",
308
+ status: "warn",
309
+ message: `${String(cycles.length)} circular dependency chain(s) found`,
310
+ details,
311
+ fixable: false,
312
+ };
313
+ }
314
+ return {
315
+ name: "circular-dependencies",
316
+ status: "pass",
317
+ message: "No circular dependencies",
318
+ details: [],
319
+ fixable: false,
320
+ };
321
+ }
322
+
323
+ function checkLabelSchema(sproutDir: string): DoctorCheck {
324
+ const details: string[] = [];
325
+ const lines = readRawLines(join(sproutDir, ISSUES_FILE));
326
+ for (const line of lines) {
327
+ if (!line.parsed) continue;
328
+ const issue = line.parsed as Record<string, unknown>;
329
+ const id = typeof issue.id === "string" ? issue.id : `line ${String(line.lineNumber)}`;
330
+ if (issue.labels !== undefined) {
331
+ if (!Array.isArray(issue.labels)) {
332
+ details.push(`${id}: labels is not an array`);
333
+ } else {
334
+ for (const label of issue.labels) {
335
+ if (typeof label !== "string") {
336
+ details.push(`${id}: label is not a string: ${JSON.stringify(label)}`);
337
+ } else if (label.trim() === "") {
338
+ details.push(`${id}: empty label string`);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+ if (details.length > 0) {
345
+ return {
346
+ name: "label-schema",
347
+ status: "warn",
348
+ message: `${String(details.length)} label schema issue(s)`,
349
+ details,
350
+ fixable: true,
351
+ };
352
+ }
353
+ return {
354
+ name: "label-schema",
355
+ status: "pass",
356
+ message: "All label arrays are valid",
357
+ details: [],
358
+ fixable: false,
359
+ };
360
+ }
361
+
362
+ function checkExtensionsSchema(sproutDir: string): DoctorCheck {
363
+ const details: string[] = [];
364
+ const lines = readRawLines(join(sproutDir, ISSUES_FILE));
365
+ for (const line of lines) {
366
+ if (!line.parsed) continue;
367
+ const issue = line.parsed as Record<string, unknown>;
368
+ const id = typeof issue.id === "string" ? issue.id : `line ${String(line.lineNumber)}`;
369
+ if (issue.extensions === undefined) continue;
370
+ const ext = issue.extensions;
371
+ if (ext === null || typeof ext !== "object" || Array.isArray(ext)) {
372
+ details.push(`${id}: extensions must be a plain object: ${JSON.stringify(ext)}`);
373
+ }
374
+ }
375
+ if (details.length > 0) {
376
+ return {
377
+ name: "extensions-schema",
378
+ status: "warn",
379
+ message: `${String(details.length)} malformed extensions field(s)`,
380
+ details,
381
+ fixable: true,
382
+ };
383
+ }
384
+ return {
385
+ name: "extensions-schema",
386
+ status: "pass",
387
+ message: "All extensions fields are valid",
388
+ details: [],
389
+ fixable: false,
390
+ };
391
+ }
392
+
393
+ function checkClosedFieldsConsistency(sproutDir: string): DoctorCheck {
394
+ const failures: string[] = [];
395
+ const warnings: string[] = [];
396
+ const lines = readRawLines(join(sproutDir, ISSUES_FILE));
397
+ for (const line of lines) {
398
+ if (!line.parsed) continue;
399
+ const issue = line.parsed as Record<string, unknown>;
400
+ const id = typeof issue.id === "string" ? issue.id : `line ${String(line.lineNumber)}`;
401
+ const status = typeof issue.status === "string" ? issue.status : undefined;
402
+ const hasClosedAt = typeof issue.closedAt === "string" && issue.closedAt.length > 0;
403
+ const hasReason = typeof issue.closeReason === "string" && issue.closeReason.length > 0;
404
+ if (status && status !== "closed" && (hasClosedAt || hasReason)) {
405
+ const stale: string[] = [];
406
+ if (hasClosedAt) stale.push("closedAt");
407
+ if (hasReason) stale.push("closeReason");
408
+ failures.push(`${id}: status=${status} but ${stale.join("+")} still set`);
409
+ } else if (status === "closed" && !hasClosedAt) {
410
+ warnings.push(`${id}: status=closed but closedAt missing`);
411
+ }
412
+ }
413
+ const total = failures.length + warnings.length;
414
+ if (failures.length > 0) {
415
+ return {
416
+ name: "closed-fields-consistency",
417
+ status: "fail",
418
+ message: `${String(total)} status/close-metadata mismatch(es)`,
419
+ details: [...failures, ...warnings],
420
+ fixable: true,
421
+ };
422
+ }
423
+ if (warnings.length > 0) {
424
+ return {
425
+ name: "closed-fields-consistency",
426
+ status: "warn",
427
+ message: `${String(warnings.length)} closed issue(s) missing closedAt`,
428
+ details: warnings,
429
+ fixable: true,
430
+ };
431
+ }
432
+ return {
433
+ name: "closed-fields-consistency",
434
+ status: "pass",
435
+ message: "All status/close-metadata pairs are consistent",
436
+ details: [],
437
+ fixable: false,
438
+ };
439
+ }
440
+
441
+ function checkStaleLocks(sproutDir: string): DoctorCheck {
442
+ const details: string[] = [];
443
+ for (const file of [ISSUES_FILE, TEMPLATES_FILE]) {
444
+ const lockPath = join(sproutDir, `${file}.lock`);
445
+ if (existsSync(lockPath)) {
446
+ try {
447
+ const st = statSync(lockPath);
448
+ const age = Date.now() - st.mtimeMs;
449
+ if (age > LOCK_STALE_MS) {
450
+ details.push(`${file}.lock is stale (${String(Math.round(age / 1000))}s old)`);
451
+ } else {
452
+ details.push(
453
+ `${file}.lock exists (${String(Math.round(age / 1000))}s old, may be active)`,
454
+ );
455
+ }
456
+ } catch {
457
+ details.push(`${file}.lock exists but cannot stat`);
458
+ }
459
+ }
460
+ }
461
+ if (details.length > 0) {
462
+ return {
463
+ name: "stale-locks",
464
+ status: "warn",
465
+ message: `${String(details.length)} lock file(s) found`,
466
+ details,
467
+ fixable: true,
468
+ };
469
+ }
470
+ return {
471
+ name: "stale-locks",
472
+ status: "pass",
473
+ message: "No stale lock files",
474
+ details: [],
475
+ fixable: false,
476
+ };
477
+ }
478
+
479
+ function checkGitattributes(sproutDir: string): DoctorCheck {
480
+ const projectRoot = projectRootFromSproutDir(sproutDir);
481
+ const gitattrsPath = join(projectRoot, ".gitattributes");
482
+ const details: string[] = [];
483
+
484
+ if (!existsSync(gitattrsPath)) {
485
+ details.push(".gitattributes file not found");
486
+ } else {
487
+ const content = readFileSync(gitattrsPath, "utf8");
488
+ if (!content.includes(".sprout/issues.jsonl merge=union")) {
489
+ details.push("Missing: .sprout/issues.jsonl merge=union");
490
+ }
491
+ if (!content.includes(".sprout/templates.jsonl merge=union")) {
492
+ details.push("Missing: .sprout/templates.jsonl merge=union");
493
+ }
494
+ }
495
+
496
+ if (details.length > 0) {
497
+ return {
498
+ name: "gitattributes",
499
+ status: "warn",
500
+ message: "Missing merge=union gitattributes entries",
501
+ details,
502
+ fixable: true,
503
+ };
504
+ }
505
+ return {
506
+ name: "gitattributes",
507
+ status: "pass",
508
+ message: "Gitattributes configured correctly",
509
+ details: [],
510
+ fixable: false,
511
+ };
512
+ }
513
+
514
+ function applyFixes(sproutDir: string, checks: DoctorCheck[]): string[] {
515
+ const fixed: string[] = [];
516
+
517
+ for (const check of checks) {
518
+ if (check.status === "pass" || !check.fixable) continue;
519
+
520
+ switch (check.name) {
521
+ case "jsonl-integrity": {
522
+ for (const file of [ISSUES_FILE, TEMPLATES_FILE]) {
523
+ const filePath = join(sproutDir, file);
524
+ if (!existsSync(filePath)) continue;
525
+ const lines = readRawLines(filePath);
526
+ const validLines = lines.filter((l) => !l.error);
527
+ if (validLines.length < lines.length) {
528
+ const content =
529
+ validLines.length > 0 ? `${validLines.map((l) => l.text).join("\n")}\n` : "";
530
+ writeFileSync(filePath, content);
531
+ fixed.push(
532
+ `Removed ${String(lines.length - validLines.length)} malformed line(s) from ${file}`,
533
+ );
534
+ }
535
+ }
536
+ break;
537
+ }
538
+ case "duplicate-ids": {
539
+ // Read, dedup (last wins), and rewrite via store functions
540
+ fixDuplicates(sproutDir, fixed);
541
+ break;
542
+ }
543
+ case "referential-integrity": {
544
+ fixDanglingRefs(sproutDir, fixed);
545
+ break;
546
+ }
547
+ case "bidirectional-consistency": {
548
+ fixBidirectional(sproutDir, fixed);
549
+ break;
550
+ }
551
+ case "stale-locks": {
552
+ for (const file of [ISSUES_FILE, TEMPLATES_FILE]) {
553
+ const lockPath = join(sproutDir, `${file}.lock`);
554
+ if (existsSync(lockPath)) {
555
+ try {
556
+ const st = statSync(lockPath);
557
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
558
+ unlinkSync(lockPath);
559
+ fixed.push(`Removed stale ${file}.lock`);
560
+ }
561
+ } catch {
562
+ // best-effort
563
+ }
564
+ }
565
+ }
566
+ break;
567
+ }
568
+ case "label-schema": {
569
+ fixLabelSchema(sproutDir, fixed);
570
+ break;
571
+ }
572
+ case "extensions-schema": {
573
+ fixExtensionsSchema(sproutDir, fixed);
574
+ break;
575
+ }
576
+ case "closed-fields-consistency": {
577
+ fixClosedFieldsConsistency(sproutDir, fixed);
578
+ break;
579
+ }
580
+ case "gitattributes": {
581
+ fixGitattributes(sproutDir, fixed);
582
+ break;
583
+ }
584
+ }
585
+ }
586
+ return fixed;
587
+ }
588
+
589
+ function fixLabelSchema(sproutDir: string, fixed: string[]): void {
590
+ const lines = readRawLines(join(sproutDir, ISSUES_FILE));
591
+ const idMap = new Map<string, Issue>();
592
+ for (const line of lines) {
593
+ if (!line.parsed) continue;
594
+ const issue = line.parsed as Issue;
595
+ if (typeof issue.id === "string") {
596
+ idMap.set(issue.id, issue);
597
+ }
598
+ }
599
+ const issues = Array.from(idMap.values());
600
+ let changed = false;
601
+ for (const issue of issues) {
602
+ if (issue.labels !== undefined) {
603
+ if (!Array.isArray(issue.labels)) {
604
+ issue.labels = undefined;
605
+ changed = true;
606
+ } else {
607
+ const cleaned = issue.labels.filter(
608
+ (l): l is string => typeof l === "string" && l.trim() !== "",
609
+ );
610
+ if (cleaned.length !== issue.labels.length) {
611
+ issue.labels = cleaned.length > 0 ? cleaned : undefined;
612
+ changed = true;
613
+ }
614
+ }
615
+ }
616
+ }
617
+ if (changed) {
618
+ const content = `${issues.map((i) => JSON.stringify(i)).join("\n")}\n`;
619
+ writeFileSync(join(sproutDir, ISSUES_FILE), content);
620
+ fixed.push("Cleaned up invalid label entries");
621
+ }
622
+ }
623
+
624
+ function fixExtensionsSchema(sproutDir: string, fixed: string[]): void {
625
+ const lines = readRawLines(join(sproutDir, ISSUES_FILE));
626
+ const idMap = new Map<string, Issue>();
627
+ for (const line of lines) {
628
+ if (!line.parsed) continue;
629
+ const issue = line.parsed as Issue;
630
+ if (typeof issue.id === "string") {
631
+ idMap.set(issue.id, issue);
632
+ }
633
+ }
634
+ const issues = Array.from(idMap.values());
635
+ let changed = false;
636
+ for (const issue of issues) {
637
+ const ext = issue.extensions as unknown;
638
+ if (ext === undefined) continue;
639
+ if (ext === null || typeof ext !== "object" || Array.isArray(ext)) {
640
+ issue.extensions = undefined;
641
+ changed = true;
642
+ }
643
+ }
644
+ if (changed) {
645
+ const content = `${issues.map((i) => JSON.stringify(i)).join("\n")}\n`;
646
+ writeFileSync(join(sproutDir, ISSUES_FILE), content);
647
+ fixed.push("Dropped malformed extensions fields");
648
+ }
649
+ }
650
+
651
+ function fixClosedFieldsConsistency(sproutDir: string, fixed: string[]): void {
652
+ const lines = readRawLines(join(sproutDir, ISSUES_FILE));
653
+ const idMap = new Map<string, Issue>();
654
+ for (const line of lines) {
655
+ if (!line.parsed) continue;
656
+ const issue = line.parsed as Issue;
657
+ if (typeof issue.id === "string") {
658
+ idMap.set(issue.id, issue);
659
+ }
660
+ }
661
+ const issues = Array.from(idMap.values());
662
+ let changed = false;
663
+ for (const issue of issues) {
664
+ if (issue.status !== "closed") {
665
+ if (issue.closedAt !== undefined || issue.closeReason !== undefined) {
666
+ issue.closedAt = undefined;
667
+ issue.closeReason = undefined;
668
+ changed = true;
669
+ }
670
+ } else if (!issue.closedAt) {
671
+ issue.closedAt = issue.updatedAt;
672
+ changed = true;
673
+ }
674
+ }
675
+ if (changed) {
676
+ const content = `${issues.map((i) => JSON.stringify(i)).join("\n")}\n`;
677
+ writeFileSync(join(sproutDir, ISSUES_FILE), content);
678
+ fixed.push("Repaired status/close-metadata mismatches");
679
+ }
680
+ }
681
+
682
+ function fixDuplicates(sproutDir: string, fixed: string[]): void {
683
+ // Issues
684
+ const issueLines = readRawLines(join(sproutDir, ISSUES_FILE));
685
+ const issueMap = new Map<string, Issue>();
686
+ for (const line of issueLines) {
687
+ if (!line.parsed) continue;
688
+ const issue = line.parsed as Issue;
689
+ if (typeof issue.id === "string") {
690
+ issueMap.set(issue.id, issue);
691
+ }
692
+ }
693
+ if (issueMap.size < issueLines.filter((l) => l.parsed).length) {
694
+ const content = `${Array.from(issueMap.values())
695
+ .map((i) => JSON.stringify(i))
696
+ .join("\n")}\n`;
697
+ writeFileSync(join(sproutDir, ISSUES_FILE), content);
698
+ fixed.push("Deduplicated issues.jsonl");
699
+ }
700
+
701
+ // Templates
702
+ const tplLines = readRawLines(join(sproutDir, TEMPLATES_FILE));
703
+ const tplMap = new Map<string, Template>();
704
+ for (const line of tplLines) {
705
+ if (!line.parsed) continue;
706
+ const tpl = line.parsed as Template;
707
+ if (typeof tpl.id === "string") {
708
+ tplMap.set(tpl.id, tpl);
709
+ }
710
+ }
711
+ if (tplMap.size < tplLines.filter((l) => l.parsed).length) {
712
+ const content = `${Array.from(tplMap.values())
713
+ .map((t) => JSON.stringify(t))
714
+ .join("\n")}\n`;
715
+ writeFileSync(join(sproutDir, TEMPLATES_FILE), content);
716
+ fixed.push("Deduplicated templates.jsonl");
717
+ }
718
+ }
719
+
720
+ function fixDanglingRefs(sproutDir: string, fixed: string[]): void {
721
+ const lines = readRawLines(join(sproutDir, ISSUES_FILE));
722
+ const issues: Issue[] = [];
723
+ const idMap = new Map<string, Issue>();
724
+ for (const line of lines) {
725
+ if (!line.parsed) continue;
726
+ const issue = line.parsed as Issue;
727
+ if (typeof issue.id === "string") {
728
+ idMap.set(issue.id, issue);
729
+ }
730
+ }
731
+ // Dedup: last wins
732
+ const deduped = Array.from(idMap.values());
733
+ const ids = new Set(deduped.map((i) => i.id));
734
+ let changed = false;
735
+ for (const issue of deduped) {
736
+ const origBlockedBy = issue.blockedBy?.length ?? 0;
737
+ const origBlocks = issue.blocks?.length ?? 0;
738
+ if (issue.blockedBy) {
739
+ issue.blockedBy = issue.blockedBy.filter((ref) => ids.has(ref));
740
+ if (issue.blockedBy.length === 0) issue.blockedBy = undefined;
741
+ }
742
+ if (issue.blocks) {
743
+ issue.blocks = issue.blocks.filter((ref) => ids.has(ref));
744
+ if (issue.blocks.length === 0) issue.blocks = undefined;
745
+ }
746
+ if (
747
+ (issue.blockedBy?.length ?? 0) !== origBlockedBy ||
748
+ (issue.blocks?.length ?? 0) !== origBlocks
749
+ ) {
750
+ changed = true;
751
+ }
752
+ issues.push(issue);
753
+ }
754
+ if (changed) {
755
+ const content = `${issues.map((i) => JSON.stringify(i)).join("\n")}\n`;
756
+ writeFileSync(join(sproutDir, ISSUES_FILE), content);
757
+ fixed.push("Removed dangling dependency references");
758
+ }
759
+ }
760
+
761
+ function fixBidirectional(sproutDir: string, fixed: string[]): void {
762
+ const lines = readRawLines(join(sproutDir, ISSUES_FILE));
763
+ const idMap = new Map<string, Issue>();
764
+ for (const line of lines) {
765
+ if (!line.parsed) continue;
766
+ const issue = line.parsed as Issue;
767
+ if (typeof issue.id === "string") {
768
+ idMap.set(issue.id, issue);
769
+ }
770
+ }
771
+ const issues = Array.from(idMap.values());
772
+ let changed = false;
773
+
774
+ for (const issue of issues) {
775
+ for (const ref of issue.blockedBy ?? []) {
776
+ const target = idMap.get(ref);
777
+ if (target && !(target.blocks ?? []).includes(issue.id)) {
778
+ target.blocks = [...(target.blocks ?? []), issue.id];
779
+ changed = true;
780
+ }
781
+ }
782
+ for (const ref of issue.blocks ?? []) {
783
+ const target = idMap.get(ref);
784
+ if (target && !(target.blockedBy ?? []).includes(issue.id)) {
785
+ target.blockedBy = [...(target.blockedBy ?? []), issue.id];
786
+ changed = true;
787
+ }
788
+ }
789
+ }
790
+
791
+ if (changed) {
792
+ const content = `${issues.map((i) => JSON.stringify(i)).join("\n")}\n`;
793
+ writeFileSync(join(sproutDir, ISSUES_FILE), content);
794
+ fixed.push("Added missing bidirectional dependency links");
795
+ }
796
+ }
797
+
798
+ function fixGitattributes(sproutDir: string, fixed: string[]): void {
799
+ const projectRoot = projectRootFromSproutDir(sproutDir);
800
+ const gitattrsPath = join(projectRoot, ".gitattributes");
801
+ const issueEntry = ".sprout/issues.jsonl merge=union";
802
+ const tplEntry = ".sprout/templates.jsonl merge=union";
803
+
804
+ if (!existsSync(gitattrsPath)) {
805
+ writeFileSync(gitattrsPath, `${issueEntry}\n${tplEntry}\n`);
806
+ fixed.push("Created .gitattributes with merge=union entries");
807
+ return;
808
+ }
809
+
810
+ const content = readFileSync(gitattrsPath, "utf8");
811
+ const missing: string[] = [];
812
+ if (!content.includes(issueEntry)) missing.push(issueEntry);
813
+ if (!content.includes(tplEntry)) missing.push(tplEntry);
814
+
815
+ if (missing.length > 0) {
816
+ const suffix = missing.map((e) => `${e}\n`).join("");
817
+ const separator = content.endsWith("\n") ? "" : "\n";
818
+ writeFileSync(gitattrsPath, `${content}${separator}${suffix}`);
819
+ fixed.push("Added missing merge=union entries to .gitattributes");
820
+ }
821
+ }
822
+
823
+ function printCheck(check: DoctorCheck, verbose: boolean): void {
824
+ if (check.status === "pass" && !verbose) return;
825
+
826
+ const icon =
827
+ check.status === "pass"
828
+ ? brand("✓")
829
+ : check.status === "warn"
830
+ ? chalk.yellow("!")
831
+ : chalk.red("✗");
832
+
833
+ console.log(` ${icon} ${check.message}`);
834
+ for (const detail of check.details) {
835
+ console.log(` ${muted(detail)}`);
836
+ }
837
+ }
838
+
839
+ export async function run(args: string[], sproutDir?: string): Promise<void> {
840
+ const jsonMode = args.includes("--json");
841
+ const fixMode = args.includes("--fix");
842
+ const verbose = args.includes("--verbose");
843
+
844
+ const dir = sproutDir ?? (await findSproutDir());
845
+
846
+ // Load config
847
+ let config: { project: string } | null = null;
848
+ try {
849
+ config = await readConfig(dir);
850
+ } catch {
851
+ config = null;
852
+ }
853
+
854
+ // Run checks
855
+ const checks: DoctorCheck[] = [];
856
+
857
+ const configCheck = checkConfig(dir, config);
858
+ checks.push(configCheck);
859
+
860
+ // If config fails, skip remaining checks
861
+ if (configCheck.status === "fail") {
862
+ return await reportResults(checks, jsonMode, verbose, fixMode, dir);
863
+ }
864
+
865
+ checks.push(checkJsonlIntegrity(dir));
866
+ checks.push(checkSchemaValidation(dir));
867
+ checks.push(checkDuplicateIds(dir));
868
+
869
+ // Load deduped issues for dependency checks
870
+ const issues = await readIssues(dir);
871
+ checks.push(checkReferentialIntegrity(issues));
872
+ checks.push(checkBidirectionalConsistency(issues));
873
+ checks.push(checkCircularDependencies(issues));
874
+ checks.push(checkLabelSchema(dir));
875
+ checks.push(checkExtensionsSchema(dir));
876
+ checks.push(checkClosedFieldsConsistency(dir));
877
+ checks.push(checkStaleLocks(dir));
878
+ checks.push(checkGitattributes(dir));
879
+
880
+ // Apply fixes if requested
881
+ if (fixMode) {
882
+ const fixableFailures = checks.filter((ch) => ch.fixable && ch.status !== "pass");
883
+ if (fixableFailures.length > 0) {
884
+ const fixedItems = applyFixes(dir, checks);
885
+
886
+ // Re-run all checks after fixes
887
+ const reChecks: DoctorCheck[] = [];
888
+ let reConfig: { project: string } | null = null;
889
+ try {
890
+ reConfig = await readConfig(dir);
891
+ } catch {
892
+ reConfig = null;
893
+ }
894
+ reChecks.push(checkConfig(dir, reConfig));
895
+ if (reChecks[0]?.status !== "fail") {
896
+ reChecks.push(checkJsonlIntegrity(dir));
897
+ reChecks.push(checkSchemaValidation(dir));
898
+ reChecks.push(checkDuplicateIds(dir));
899
+ const reIssues = await readIssues(dir);
900
+ reChecks.push(checkReferentialIntegrity(reIssues));
901
+ reChecks.push(checkBidirectionalConsistency(reIssues));
902
+ reChecks.push(checkCircularDependencies(reIssues));
903
+ reChecks.push(checkLabelSchema(dir));
904
+ reChecks.push(checkExtensionsSchema(dir));
905
+ reChecks.push(checkClosedFieldsConsistency(dir));
906
+ reChecks.push(checkStaleLocks(dir));
907
+ reChecks.push(checkGitattributes(dir));
908
+ }
909
+ return await reportResults(reChecks, jsonMode, verbose, fixMode, dir, fixedItems);
910
+ }
911
+ }
912
+
913
+ return await reportResults(checks, jsonMode, verbose, fixMode, dir);
914
+ }
915
+
916
+ async function reportResults(
917
+ checks: DoctorCheck[],
918
+ jsonMode: boolean,
919
+ verbose: boolean,
920
+ _fixMode: boolean,
921
+ _sproutDir: string,
922
+ fixedItems?: string[],
923
+ ): Promise<void> {
924
+ const summary = {
925
+ pass: checks.filter((ch) => ch.status === "pass").length,
926
+ warn: checks.filter((ch) => ch.status === "warn").length,
927
+ fail: checks.filter((ch) => ch.status === "fail").length,
928
+ };
929
+
930
+ if (jsonMode) {
931
+ await outputJson({
932
+ success: summary.fail === 0,
933
+ command: "doctor",
934
+ checks: checks.map((ch) => ({
935
+ name: ch.name,
936
+ status: ch.status,
937
+ message: ch.message,
938
+ details: ch.details,
939
+ fixable: ch.fixable,
940
+ })),
941
+ summary,
942
+ ...(fixedItems && fixedItems.length > 0 ? { fixed: fixedItems } : {}),
943
+ });
944
+ } else {
945
+ console.log(`\n${chalk.bold("Sprout Doctor")}\n`);
946
+ for (const check of checks) {
947
+ printCheck(check, verbose);
948
+ }
949
+ console.log(
950
+ `\n${muted(`${String(summary.pass)} passed, ${String(summary.warn)} warning(s), ${String(summary.fail)} failure(s)`)}`,
951
+ );
952
+ if (fixedItems && fixedItems.length > 0) {
953
+ console.log(`\n${chalk.bold("Fixed:")}`);
954
+ for (const item of fixedItems) {
955
+ console.log(` ${brand("✓")} ${item}`);
956
+ }
957
+ }
958
+ }
959
+
960
+ if (summary.fail > 0) {
961
+ process.exitCode = 1;
962
+ }
963
+ }
964
+
965
+ export function register(program: Command): void {
966
+ program
967
+ .command("doctor")
968
+ .description("Check project health and data integrity")
969
+ .option("--fix", "Auto-fix fixable issues")
970
+ .option("--verbose", "Show all check results including passes")
971
+ .option("--json", "Output as JSON")
972
+ .action(async (opts: { fix?: boolean; verbose?: boolean; json?: boolean }) => {
973
+ const args: string[] = [];
974
+ if (opts.fix) args.push("--fix");
975
+ if (opts.verbose) args.push("--verbose");
976
+ if (opts.json) args.push("--json");
977
+ await run(args);
978
+ });
979
+ }