@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,166 @@
1
+ import { join } from "node:path";
2
+ import type { Command } from "commander";
3
+ import { c, errorOut, fmt, humanOut, jsonOut } from "../output.ts";
4
+ import { dedupById, dedupByIdLast, readJsonl } from "../store.ts";
5
+ import type { Prompt, Schema } from "../types.ts";
6
+ import { ExitError } from "../types.ts";
7
+ import { validatePrompt } from "../validate.ts";
8
+
9
+ export default async function validate(args: string[], json: boolean): Promise<void> {
10
+ const cwd = process.cwd();
11
+ const promptsPath = join(cwd, ".trellis", "prompts.jsonl");
12
+ const schemasPath = join(cwd, ".trellis", "schemas.jsonl");
13
+
14
+ const allMode = args.includes("--all");
15
+
16
+ const allPromptRecords = await readJsonl<Prompt>(promptsPath);
17
+ const allSchemaRecords = await readJsonl<Schema>(schemasPath);
18
+ const currentPrompts = dedupById(allPromptRecords);
19
+ const currentSchemas = dedupByIdLast(allSchemaRecords);
20
+
21
+ if (args.includes("--help") || args.includes("-h")) {
22
+ humanOut(`Usage: tl validate <name> [options]
23
+ tl validate --all
24
+
25
+ Options:
26
+ --all Validate all prompts with schemas
27
+ --json Output as JSON`);
28
+ return;
29
+ }
30
+
31
+ if (allMode) {
32
+ const promptsWithSchema = currentPrompts.filter((p) => p.schema && p.status !== "archived");
33
+ const results = [];
34
+ let allValid = true;
35
+
36
+ for (const prompt of promptsWithSchema) {
37
+ const schemaRecord = currentSchemas.find((s) => s.name === prompt.schema);
38
+ if (!schemaRecord) {
39
+ results.push({
40
+ name: prompt.name,
41
+ valid: false,
42
+ errors: [{ section: "", rule: "", message: `Schema '${prompt.schema}' not found` }],
43
+ warnings: [],
44
+ });
45
+ allValid = false;
46
+ continue;
47
+ }
48
+
49
+ const result = validatePrompt(prompt, schemaRecord, currentPrompts);
50
+ results.push({ name: prompt.name, ...result });
51
+ if (!result.valid) allValid = false;
52
+ }
53
+
54
+ if (json) {
55
+ jsonOut({ success: allValid, command: "validate", results, count: results.length });
56
+ } else {
57
+ for (const r of results) {
58
+ const icon = r.valid ? c.green("✓") : c.red("✗");
59
+ humanOut(`${icon} ${r.name}`);
60
+ for (const err of r.errors) {
61
+ humanOut(` ${c.red("error")}: [${err.section}] ${err.message}`);
62
+ }
63
+ for (const w of r.warnings) {
64
+ humanOut(` ${c.yellow("warn")}: ${w}`);
65
+ }
66
+ }
67
+ if (results.length === 0) {
68
+ humanOut(c.dim("No prompts with schemas to validate."));
69
+ }
70
+ }
71
+
72
+ if (!allValid) throw new ExitError(1);
73
+ return;
74
+ }
75
+
76
+ // Single prompt validation
77
+ const name = args.filter((a) => !a.startsWith("--"))[0];
78
+ if (!name) {
79
+ if (json) {
80
+ jsonOut({ success: false, command: "validate", error: "Prompt name or --all required" });
81
+ } else {
82
+ errorOut("Usage: tl validate <name> or tl validate --all");
83
+ }
84
+ throw new ExitError(1);
85
+ }
86
+
87
+ const prompt = currentPrompts.find((p) => p.name === name);
88
+ if (!prompt) {
89
+ if (json) {
90
+ jsonOut({ success: false, command: "validate", error: `Prompt '${name}' not found` });
91
+ } else {
92
+ errorOut(`Prompt '${name}' not found`);
93
+ }
94
+ throw new ExitError(1);
95
+ }
96
+
97
+ if (!prompt.schema) {
98
+ if (json) {
99
+ jsonOut({
100
+ success: false,
101
+ command: "validate",
102
+ error: `Prompt '${name}' has no schema assigned`,
103
+ });
104
+ } else {
105
+ errorOut(`Prompt '${name}' has no schema assigned`);
106
+ }
107
+ throw new ExitError(1);
108
+ }
109
+
110
+ const schemaRecord = currentSchemas.find((s) => s.name === prompt.schema);
111
+ if (!schemaRecord) {
112
+ if (json) {
113
+ jsonOut({
114
+ success: false,
115
+ command: "validate",
116
+ error: `Schema '${prompt.schema}' not found`,
117
+ });
118
+ } else {
119
+ errorOut(`Schema '${prompt.schema}' not found`);
120
+ }
121
+ throw new ExitError(1);
122
+ }
123
+
124
+ const result = validatePrompt(prompt, schemaRecord, currentPrompts);
125
+
126
+ if (json) {
127
+ jsonOut({
128
+ success: result.valid,
129
+ command: "validate",
130
+ name,
131
+ valid: result.valid,
132
+ errors: result.errors,
133
+ warnings: result.warnings,
134
+ });
135
+ } else {
136
+ if (result.valid) {
137
+ humanOut(fmt.success(`${name} is valid`));
138
+ if (result.warnings.length > 0) {
139
+ for (const w of result.warnings) {
140
+ humanOut(fmt.warning(w));
141
+ }
142
+ }
143
+ } else {
144
+ humanOut(fmt.error(`${name} is invalid`));
145
+ for (const err of result.errors) {
146
+ humanOut(` ${c.red("error")}: [${err.section}] ${err.message}`);
147
+ }
148
+ }
149
+ }
150
+
151
+ if (!result.valid) throw new ExitError(1);
152
+ }
153
+
154
+ export function registerValidateCommand(program: Command): void {
155
+ program
156
+ .command("validate [name]")
157
+ .description("Validate a prompt against its schema")
158
+ .option("--all", "Validate all prompts with schemas")
159
+ .action(async (name: string | undefined, opts: Record<string, unknown>) => {
160
+ const json: boolean = program.opts().json ?? false;
161
+ const args: string[] = [];
162
+ if (name) args.push(name);
163
+ if (opts.all) args.push("--all");
164
+ await validate(args, json);
165
+ });
166
+ }
@@ -0,0 +1,81 @@
1
+ // JSON Schema for `.trellis/config.yaml`. Wire-format contract for warren V2's
2
+ // schema-driven config UI (warren ROADMAP R-10): warren reads this schema,
3
+ // renders a form, and writes back via `tl config set <path> <value>`. Locked
4
+ // with a golden test (config-schema.test.ts) so any shape change is intentional.
5
+ //
6
+ // Describes the **canonical** shape produced by loadConfig in src/config.ts:
7
+ // `project`, `version`, and a `targets` map of named EmitTargets. The loader
8
+ // transparently migrates legacy `emitDir`/`emitDirByTag` configs to this shape
9
+ // on read; the first `tl config set` rewrites the file in the canonical form.
10
+ // Legacy fields are intentionally absent so warren's UI does not surface
11
+ // deprecated knobs.
12
+
13
+ type JSONSchema = Record<string, unknown>;
14
+
15
+ export function configSchema(): JSONSchema {
16
+ return {
17
+ $schema: "https://json-schema.org/draft/2020-12/schema",
18
+ $id: "https://github.com/jayminwest/trellis/config.schema.json",
19
+ title: "Trellis project config",
20
+ description:
21
+ "Schema for .trellis/config.yaml. Consumed by warren's schema-driven UI; emit via `tl config schema`.",
22
+ type: "object",
23
+ required: ["project", "version"],
24
+ additionalProperties: false,
25
+ properties: {
26
+ project: {
27
+ type: "string",
28
+ minLength: 1,
29
+ title: "Project name",
30
+ description: "Human-readable project identifier surfaced in `tl list` and warren.",
31
+ },
32
+ version: {
33
+ type: "string",
34
+ title: "Config schema version",
35
+ description: "Internal version tag for the config layout. Bumped when the schema breaks.",
36
+ default: "1",
37
+ },
38
+ targets: {
39
+ type: "object",
40
+ title: "Emit targets",
41
+ description:
42
+ "Map of target name → emit target. `tl emit` resolves a prompt to a target by matching tags, falling back to the target marked `default: true`.",
43
+ additionalProperties: { $ref: "#/$defs/EmitTarget" },
44
+ examples: [
45
+ {
46
+ default: { dir: "agents", default: true },
47
+ docs: { dir: "docs/prompts", tags: ["doc"] },
48
+ },
49
+ ],
50
+ },
51
+ },
52
+ $defs: {
53
+ EmitTarget: {
54
+ type: "object",
55
+ required: ["dir"],
56
+ additionalProperties: false,
57
+ properties: {
58
+ dir: {
59
+ type: "string",
60
+ minLength: 1,
61
+ title: "Directory",
62
+ description:
63
+ "Output directory (relative to the repo root) where prompts emitted to this target are written.",
64
+ },
65
+ default: {
66
+ type: "boolean",
67
+ title: "Default target",
68
+ description:
69
+ "When true, prompts with no matching tag-based target are emitted here. At most one target should set this.",
70
+ },
71
+ tags: {
72
+ type: "array",
73
+ items: { type: "string", minLength: 1 },
74
+ title: "Tags",
75
+ description: "Prompts whose `tags` include any of these are routed to this target.",
76
+ },
77
+ },
78
+ },
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,108 @@
1
+ import { join } from "node:path";
2
+ import type { Config, EmitTarget } from "./types.ts";
3
+ import { parseYaml, serializeYaml, type YamlMap } from "./yaml.ts";
4
+
5
+ export async function loadConfig(dir: string): Promise<Config> {
6
+ const configPath = join(dir, ".trellis", "config.yaml");
7
+
8
+ try {
9
+ const text = await Bun.file(configPath).text();
10
+ const parsed = parseYaml(text);
11
+
12
+ const project = typeof parsed.project === "string" ? parsed.project : "trellis";
13
+ const version = typeof parsed.version === "string" ? parsed.version : "1";
14
+
15
+ const config: Config = { project, version };
16
+
17
+ // New format: targets
18
+ if (typeof parsed.targets === "object" && !Array.isArray(parsed.targets)) {
19
+ config.targets = {};
20
+ for (const [name, targetValue] of Object.entries(parsed.targets as Record<string, unknown>)) {
21
+ if (
22
+ typeof targetValue === "object" &&
23
+ targetValue !== null &&
24
+ !Array.isArray(targetValue)
25
+ ) {
26
+ const tv = targetValue as Record<string, unknown>;
27
+ const target: EmitTarget = { dir: typeof tv.dir === "string" ? tv.dir : name };
28
+ if (tv.default === "true" || tv.default === true) target.default = true;
29
+ if (Array.isArray(tv.tags)) {
30
+ target.tags = tv.tags.filter((t): t is string => typeof t === "string");
31
+ }
32
+ config.targets[name] = target;
33
+ }
34
+ }
35
+ }
36
+ // Legacy format: emitDir / emitDirByTag → convert to targets
37
+ else {
38
+ const emitDir = typeof parsed.emitDir === "string" ? parsed.emitDir || undefined : undefined;
39
+ const rawByTag = parsed.emitDirByTag;
40
+
41
+ if (
42
+ emitDir ||
43
+ (typeof rawByTag === "object" && rawByTag !== null && !Array.isArray(rawByTag))
44
+ ) {
45
+ config.targets = {};
46
+ const defaultDir = emitDir || "agents";
47
+ config.targets.default = { dir: defaultDir, default: true };
48
+
49
+ if (typeof rawByTag === "object" && rawByTag !== null && !Array.isArray(rawByTag)) {
50
+ // Invert tag→dir map to dir→tags, creating named targets
51
+ const dirToTags: Record<string, string[]> = {};
52
+ for (const [tag, tdir] of Object.entries(rawByTag as Record<string, string>)) {
53
+ const existing = dirToTags[tdir];
54
+ if (existing) {
55
+ existing.push(tag);
56
+ } else {
57
+ dirToTags[tdir] = [tag];
58
+ }
59
+ }
60
+ for (const [tdir, tags] of Object.entries(dirToTags)) {
61
+ if (tdir === defaultDir) {
62
+ // Merge tags into the default target
63
+ config.targets.default.tags = tags;
64
+ } else {
65
+ // Generate a name from the directory path
66
+ const name =
67
+ tdir
68
+ .replace(/[^a-zA-Z0-9]/g, "-")
69
+ .replace(/^-+|-+$/g, "")
70
+ .replace(/-+/g, "-") || "target";
71
+ config.targets[name] = { dir: tdir, tags };
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ return config;
79
+ } catch {
80
+ // Return defaults if config doesn't exist
81
+ return {
82
+ project: "trellis",
83
+ version: "1",
84
+ };
85
+ }
86
+ }
87
+
88
+ export async function saveConfig(dir: string, config: Config): Promise<void> {
89
+ const configPath = join(dir, ".trellis", "config.yaml");
90
+
91
+ const obj: YamlMap = {
92
+ project: config.project,
93
+ version: config.version,
94
+ };
95
+
96
+ if (config.targets && Object.keys(config.targets).length > 0) {
97
+ const targetsMap: YamlMap = {};
98
+ for (const [name, target] of Object.entries(config.targets)) {
99
+ const targetMap: YamlMap = { dir: target.dir };
100
+ if (target.default) targetMap.default = "true";
101
+ if (target.tags && target.tags.length > 0) targetMap.tags = target.tags;
102
+ targetsMap[name] = targetMap;
103
+ }
104
+ obj.targets = targetsMap;
105
+ }
106
+
107
+ await Bun.write(configPath, serializeYaml(obj));
108
+ }
@@ -0,0 +1,348 @@
1
+ /**
2
+ * YAML frontmatter parser and serializer.
3
+ * Handles arrays, nested objects, booleans, numbers, and quoted strings.
4
+ * Does NOT handle flow syntax ([a,b], {a:b}), multi-line scalars, anchors, or tags.
5
+ */
6
+
7
+ type FmValue = string | number | boolean | FmValue[] | FmObj;
8
+ type FmObj = { [key: string]: FmValue };
9
+
10
+ // --- Helpers ---
11
+
12
+ function getIndent(line: string): number {
13
+ let i = 0;
14
+ while (i < line.length && line[i] === " ") i++;
15
+ return i;
16
+ }
17
+
18
+ function unquoteStr(s: string): string {
19
+ if (s.startsWith('"') && s.endsWith('"') && s.length >= 2) {
20
+ return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\").replace(/\\n/g, "\n");
21
+ }
22
+ if (s.startsWith("'") && s.endsWith("'") && s.length >= 2) {
23
+ return s.slice(1, -1).replace(/''/g, "'");
24
+ }
25
+ return s;
26
+ }
27
+
28
+ function stripInlineComment(s: string): string {
29
+ if (s.startsWith('"') || s.startsWith("'")) return s;
30
+ const idx = s.indexOf(" #");
31
+ return idx !== -1 ? s.slice(0, idx).trim() : s;
32
+ }
33
+
34
+ function parseScalar(raw: string): string | number | boolean {
35
+ raw = stripInlineComment(raw).trim();
36
+ if (
37
+ (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) ||
38
+ (raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2)
39
+ ) {
40
+ return unquoteStr(raw);
41
+ }
42
+ if (raw === "true") return true;
43
+ if (raw === "false") return false;
44
+ if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
45
+ return raw;
46
+ }
47
+
48
+ // --- Parser ---
49
+
50
+ function parseMappingBlock(
51
+ lines: string[],
52
+ start: number,
53
+ indent: number,
54
+ ): { value: FmObj; next: number } {
55
+ const result: FmObj = {};
56
+ let i = start;
57
+
58
+ while (i < lines.length) {
59
+ const rawLine = lines[i] ?? "";
60
+ const trimmed = rawLine.trim();
61
+
62
+ if (!trimmed || trimmed.startsWith("#")) {
63
+ i++;
64
+ continue;
65
+ }
66
+
67
+ const lineIndent = getIndent(rawLine);
68
+ if (lineIndent < indent) break;
69
+ if (lineIndent > indent) {
70
+ i++;
71
+ continue;
72
+ }
73
+
74
+ // Sequence item at this indent — we're leaving mapping territory
75
+ if (trimmed.startsWith("- ") || trimmed === "-") break;
76
+
77
+ const colonIdx = trimmed.indexOf(":");
78
+ if (colonIdx === -1) {
79
+ i++;
80
+ continue;
81
+ }
82
+
83
+ const key = trimmed.slice(0, colonIdx).trim();
84
+ const rest = trimmed.slice(colonIdx + 1).trim();
85
+ i++;
86
+
87
+ if (rest && !rest.startsWith("#")) {
88
+ result[key] = parseScalar(rest);
89
+ } else {
90
+ // Empty value — peek ahead to determine child type
91
+ let peek = i;
92
+ while (peek < lines.length && !lines[peek]?.trim()) peek++;
93
+
94
+ if (peek >= lines.length) {
95
+ result[key] = "";
96
+ continue;
97
+ }
98
+
99
+ const nextLine = lines[peek] ?? "";
100
+ const nextIndent = getIndent(nextLine);
101
+ const nextTrimmed = nextLine.trim();
102
+
103
+ if (nextIndent <= indent) {
104
+ result[key] = "";
105
+ } else if (nextTrimmed.startsWith("- ") || nextTrimmed === "-") {
106
+ const { value, next } = parseSequenceBlock(lines, i, nextIndent);
107
+ result[key] = value;
108
+ i = next;
109
+ } else {
110
+ const { value, next } = parseMappingBlock(lines, i, nextIndent);
111
+ result[key] = value;
112
+ i = next;
113
+ }
114
+ }
115
+ }
116
+
117
+ return { value: result, next: i };
118
+ }
119
+
120
+ function parseSequenceBlock(
121
+ lines: string[],
122
+ start: number,
123
+ indent: number,
124
+ ): { value: FmValue[]; next: number } {
125
+ const items: FmValue[] = [];
126
+ let i = start;
127
+
128
+ while (i < lines.length) {
129
+ const rawLine = lines[i] ?? "";
130
+ const trimmed = rawLine.trim();
131
+
132
+ if (!trimmed || trimmed.startsWith("#")) {
133
+ i++;
134
+ continue;
135
+ }
136
+
137
+ const lineIndent = getIndent(rawLine);
138
+ if (lineIndent < indent) break;
139
+ if (lineIndent > indent) {
140
+ i++;
141
+ continue;
142
+ }
143
+
144
+ if (!trimmed.startsWith("- ") && trimmed !== "-") break;
145
+
146
+ const itemContent = trimmed.startsWith("- ") ? trimmed.slice(2).trim() : "";
147
+ i++;
148
+
149
+ if (!itemContent) {
150
+ // Empty item — check for indented mapping children
151
+ let peek = i;
152
+ while (peek < lines.length && !lines[peek]?.trim()) peek++;
153
+ if (peek < lines.length && getIndent(lines[peek] ?? "") > indent) {
154
+ const childIndent = getIndent(lines[peek] ?? "");
155
+ const { value, next } = parseMappingBlock(lines, i, childIndent);
156
+ items.push(value);
157
+ i = next;
158
+ } else {
159
+ items.push("");
160
+ }
161
+ continue;
162
+ }
163
+
164
+ const colonIdx = itemContent.indexOf(":");
165
+ if (colonIdx !== -1 && !itemContent.startsWith('"') && !itemContent.startsWith("'")) {
166
+ // Mapping item: "- key: value" with optional continuation fields
167
+ const key = itemContent.slice(0, colonIdx).trim();
168
+ const valStr = itemContent.slice(colonIdx + 1).trim();
169
+ const obj: FmObj = { [key]: valStr ? parseScalar(valStr) : "" };
170
+
171
+ // Continuation fields at indent + 2 (same as the content after "- ")
172
+ const contIndent = indent + 2;
173
+ while (i < lines.length) {
174
+ const nextRaw = lines[i] ?? "";
175
+ const nextTrimmed = nextRaw.trim();
176
+
177
+ if (!nextTrimmed || nextTrimmed.startsWith("#")) {
178
+ i++;
179
+ continue;
180
+ }
181
+
182
+ const nextIndent = getIndent(nextRaw);
183
+ if (nextIndent !== contIndent) break;
184
+ if (nextTrimmed.startsWith("- ")) break;
185
+
186
+ const fieldColon = nextTrimmed.indexOf(":");
187
+ if (fieldColon === -1) break;
188
+
189
+ const fieldKey = nextTrimmed.slice(0, fieldColon).trim();
190
+ const fieldRest = nextTrimmed.slice(fieldColon + 1).trim();
191
+ obj[fieldKey] = fieldRest ? parseScalar(fieldRest) : "";
192
+ i++;
193
+ }
194
+
195
+ items.push(obj);
196
+ } else {
197
+ items.push(parseScalar(itemContent));
198
+ }
199
+ }
200
+
201
+ return { value: items, next: i };
202
+ }
203
+
204
+ // --- Serializer ---
205
+
206
+ function needsQuoting(s: string): boolean {
207
+ // Ambiguous scalars that would be misinterpreted without quotes
208
+ if (s === "true" || s === "false" || s === "null" || s === "~") return true;
209
+ if (/^-?\d+(\.\d+)?$/.test(s)) return true;
210
+ return (
211
+ s.includes(":") ||
212
+ s.includes("#") ||
213
+ s.includes('"') ||
214
+ s.includes("'") ||
215
+ s.includes("\n") ||
216
+ s.startsWith(" ") ||
217
+ s.endsWith(" ")
218
+ );
219
+ }
220
+
221
+ function maybeQuote(s: string): string {
222
+ if (needsQuoting(s)) {
223
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
224
+ }
225
+ return s;
226
+ }
227
+
228
+ function serializeScalar(val: unknown): string {
229
+ if (typeof val === "boolean" || typeof val === "number") return String(val);
230
+ if (typeof val === "string") return maybeQuote(val);
231
+ return String(val ?? "");
232
+ }
233
+
234
+ function serializeValue(key: string, val: unknown, indent: number): string[] {
235
+ const pad = " ".repeat(indent);
236
+
237
+ if (val === null || val === undefined) {
238
+ return [`${pad}${key}:`];
239
+ }
240
+
241
+ if (typeof val === "boolean" || typeof val === "number") {
242
+ return [`${pad}${key}: ${val}`];
243
+ }
244
+
245
+ if (typeof val === "string") {
246
+ return [`${pad}${key}: ${maybeQuote(val)}`];
247
+ }
248
+
249
+ if (Array.isArray(val)) {
250
+ const lines: string[] = [`${pad}${key}:`];
251
+ for (const item of val) {
252
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
253
+ const entries = Object.entries(item as Record<string, unknown>);
254
+ if (entries.length === 0) {
255
+ lines.push(`${pad} -`);
256
+ } else {
257
+ for (let j = 0; j < entries.length; j++) {
258
+ const entry = entries[j];
259
+ if (!entry) continue;
260
+ const [k, v] = entry;
261
+ if (j === 0) {
262
+ lines.push(`${pad} - ${k}: ${serializeScalar(v)}`);
263
+ } else {
264
+ lines.push(`${pad} ${k}: ${serializeScalar(v)}`);
265
+ }
266
+ }
267
+ }
268
+ } else {
269
+ lines.push(`${pad} - ${serializeScalar(item)}`);
270
+ }
271
+ }
272
+ return lines;
273
+ }
274
+
275
+ if (typeof val === "object") {
276
+ const lines: string[] = [`${pad}${key}:`];
277
+ for (const [childKey, childVal] of Object.entries(val as Record<string, unknown>)) {
278
+ lines.push(...serializeValue(childKey, childVal, indent + 2));
279
+ }
280
+ return lines;
281
+ }
282
+
283
+ return [`${pad}${key}: ${String(val)}`];
284
+ }
285
+
286
+ // --- Public API ---
287
+
288
+ /**
289
+ * Extract YAML frontmatter from markdown content.
290
+ * Returns parsed metadata + remaining body.
291
+ */
292
+ export function extractFrontmatter(content: string): {
293
+ metadata: Record<string, unknown>;
294
+ body: string;
295
+ } {
296
+ if (!content.startsWith("---")) {
297
+ return { metadata: {}, body: content };
298
+ }
299
+
300
+ // The opening --- must be the entire first line
301
+ const firstNewline = content.indexOf("\n");
302
+ if (firstNewline === -1) {
303
+ return { metadata: {}, body: content };
304
+ }
305
+ if (content.slice(0, firstNewline).trim() !== "---") {
306
+ return { metadata: {}, body: content };
307
+ }
308
+
309
+ const afterFirst = content.slice(firstNewline + 1);
310
+ const lines = afterFirst.split("\n");
311
+
312
+ // Find the closing ---
313
+ let closingIdx = -1;
314
+ for (let i = 0; i < lines.length; i++) {
315
+ if ((lines[i] ?? "").trim() === "---") {
316
+ closingIdx = i;
317
+ break;
318
+ }
319
+ }
320
+
321
+ if (closingIdx === -1) {
322
+ // No closing ---, treat as no frontmatter
323
+ return { metadata: {}, body: content };
324
+ }
325
+
326
+ const yamlLines = lines.slice(0, closingIdx);
327
+ const bodyLines = lines.slice(closingIdx + 1);
328
+ const body = bodyLines.join("\n");
329
+
330
+ const { value: metadata } = parseMappingBlock(yamlLines, 0, 0);
331
+ return { metadata, body };
332
+ }
333
+
334
+ /**
335
+ * Serialize a metadata object to a YAML frontmatter block (with --- delimiters).
336
+ * Returns empty string if metadata is empty.
337
+ */
338
+ export function renderFrontmatter(metadata: Record<string, unknown>): string {
339
+ if (Object.keys(metadata).length === 0) return "";
340
+
341
+ const lines: string[] = ["---"];
342
+ for (const [key, val] of Object.entries(metadata)) {
343
+ lines.push(...serializeValue(key, val, 0));
344
+ }
345
+ lines.push("---");
346
+
347
+ return `${lines.join("\n")}\n`;
348
+ }