@dsdenes/devboard 0.1.560 → 0.1.563

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 (323) hide show
  1. package/AGENTS.md +59 -0
  2. package/README.md +2497 -26
  3. package/bin/devboard.mjs +352 -118
  4. package/client/App.svelte +523 -0
  5. package/client/FeedbackOverlay.svelte +1125 -0
  6. package/client/FlowMode.svelte +1015 -0
  7. package/client/app.css +3922 -0
  8. package/client/env.d.ts +23 -0
  9. package/client/lib/commands.ts +836 -0
  10. package/client/lib/components/ActivityBar.svelte +295 -0
  11. package/client/lib/components/AdrTable.svelte +241 -0
  12. package/client/lib/components/CommandPalette.svelte +198 -0
  13. package/client/lib/components/ConceptKind.svelte +66 -0
  14. package/client/lib/components/ConfirmDialog.svelte +62 -0
  15. package/client/lib/components/DataTable.svelte +243 -0
  16. package/client/lib/components/DetailSplit.svelte +68 -0
  17. package/client/lib/components/DiscoverOverview.svelte +898 -0
  18. package/client/lib/components/DiscoverWorkspace.svelte +437 -0
  19. package/client/lib/components/DraftsPage.svelte +186 -0
  20. package/client/lib/components/GoalWorkspace.svelte +1042 -0
  21. package/client/lib/components/IssuesAgentFocus.svelte +117 -0
  22. package/client/lib/components/IssuesAgentLog.svelte +50 -0
  23. package/client/lib/components/IssuesQuestion.svelte +95 -0
  24. package/client/lib/components/IssuesWorkspace.svelte +577 -0
  25. package/client/lib/components/MermaidDiagram.svelte +89 -0
  26. package/client/lib/components/MonacoEditor.svelte +190 -0
  27. package/client/lib/components/NotificationRegion.svelte +195 -0
  28. package/client/lib/components/PaneHandle.svelte +25 -0
  29. package/client/lib/components/ScoutIssues.svelte +215 -0
  30. package/client/lib/components/ScoutWorkspace.svelte +180 -0
  31. package/client/lib/components/Sidebar.svelte +622 -0
  32. package/client/lib/components/StatusBar.svelte +118 -0
  33. package/client/lib/components/Terminal.svelte +193 -0
  34. package/client/lib/components/WatchdogWorkspace.svelte +537 -0
  35. package/client/lib/components/ui/button.svelte +56 -0
  36. package/client/lib/components/ui/icon-button.svelte +56 -0
  37. package/client/lib/components/ui/input.svelte +19 -0
  38. package/client/lib/components/ui/page-header.svelte +120 -0
  39. package/client/lib/components/ui/stage-track.svelte +141 -0
  40. package/client/lib/components/watchdog-indicator.svelte +189 -0
  41. package/client/lib/connection.svelte.ts +210 -0
  42. package/client/lib/dependency-diagram.test.ts +90 -0
  43. package/client/lib/dependency-diagram.ts +75 -0
  44. package/client/lib/discover-format.test.ts +265 -0
  45. package/client/lib/discover-format.ts +394 -0
  46. package/client/lib/discover.svelte.ts +412 -0
  47. package/client/lib/draft-format.test.ts +48 -0
  48. package/client/lib/draft-format.ts +13 -0
  49. package/client/lib/flow-save.test.ts +148 -0
  50. package/client/lib/flow.svelte.ts +445 -0
  51. package/client/lib/goal-reconnect.test.ts +282 -0
  52. package/client/lib/goal.svelte.ts +652 -0
  53. package/client/lib/issues.svelte.ts +439 -0
  54. package/client/lib/layout.svelte.ts +234 -0
  55. package/client/lib/notification-issue.test.ts +53 -0
  56. package/client/lib/notification-issue.ts +27 -0
  57. package/client/lib/notifications.svelte.ts +62 -0
  58. package/client/lib/poll-status.test.ts +141 -0
  59. package/client/lib/poll-status.ts +88 -0
  60. package/client/lib/record-status.test.ts +19 -0
  61. package/client/lib/record-status.ts +25 -0
  62. package/client/lib/scout.svelte.ts +164 -0
  63. package/client/lib/skill-document.test.ts +77 -0
  64. package/client/lib/skill-document.ts +57 -0
  65. package/client/lib/status-tone.test.ts +40 -0
  66. package/client/lib/status-tone.ts +33 -0
  67. package/client/lib/types.ts +578 -0
  68. package/client/lib/utils.ts +6 -0
  69. package/client/lib/viewport.svelte.ts +20 -0
  70. package/client/lib/watchdog.svelte.ts +69 -0
  71. package/client/main.ts +7 -0
  72. package/dist/apple-touch-icon.png +0 -0
  73. package/dist/assets/abnfDiagram-VCTEODGH-CmBSMArZ.js +1 -0
  74. package/dist/assets/arc-BZgCSX2H.js +1 -0
  75. package/dist/assets/architecture-7GRP2DOG-CF9MHlfo.js +1 -0
  76. package/dist/assets/architectureDiagram-5GKGNRK7-DzWB6xiQ.js +36 -0
  77. package/dist/assets/array-BifhSqXX.js +1 -0
  78. package/dist/assets/blockDiagram-I7D4REHJ-BtmGJ0hn.js +129 -0
  79. package/dist/assets/c4Diagram-7LVT6UL2-D5CHrrRX.js +38 -0
  80. package/dist/assets/channel-CgM4lcUW.js +1 -0
  81. package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  82. package/dist/assets/chunk-4HAMMTFA-DeIufjFl.js +62 -0
  83. package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  84. package/dist/assets/chunk-75Z2AOVW-DbtzyKUg.js +2 -0
  85. package/dist/assets/chunk-DU6HZSFF-C0fdbx3K.js +127 -0
  86. package/dist/assets/chunk-F27PBJKO-HObL2IvF.js +1 -0
  87. package/dist/assets/chunk-FOHPRMQF-DD4dKubD.js +161 -0
  88. package/dist/assets/chunk-GMAD6QVW-fg8wvtPY.js +72 -0
  89. package/dist/assets/chunk-GVQU2GXP-C0TaHOrD.js +1 -0
  90. package/dist/assets/chunk-IMKFNOWR-BVoIdx20.js +231 -0
  91. package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  92. package/dist/assets/chunk-L3NEJ4N5-B_kmqfk5.js +1 -0
  93. package/dist/assets/chunk-OSK3NFVY-DO0vhxHH.js +10 -0
  94. package/dist/assets/chunk-P2QGCYS3-CjljLvFj.js +1 -0
  95. package/dist/assets/chunk-POPQ4Y6H-BU0C7tw6.js +1 -0
  96. package/dist/assets/chunk-PWAF6VOD-Dn0OjBt0.js +1 -0
  97. package/dist/assets/chunk-SHT3W25Y-CMCZE2xx.js +168 -0
  98. package/dist/assets/chunk-SVP7TREG-BIRmRy_W.js +88 -0
  99. package/dist/assets/chunk-TICWLB2K-C1bc2CDE.js +206 -0
  100. package/dist/assets/chunk-XXDRQBXY-Cc38QUmR.js +1 -0
  101. package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  102. package/dist/assets/classDiagram-ZZMXUADV-BDh5w6Nc.js +1 -0
  103. package/dist/assets/classDiagram-v2-VYDZK3BY-BDh5w6Nc.js +1 -0
  104. package/dist/assets/cose-bilkent-JH36ORCC-B7pU7qhM.js +1 -0
  105. package/dist/assets/cynefin-OW5HDTMX-76dswSqS.js +1 -0
  106. package/dist/assets/cynefinDiagram-5FMLGOSQ-2kS4q01d.js +62 -0
  107. package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  108. package/dist/assets/dagre-D7JM2Yhv.js +1 -0
  109. package/dist/assets/dagre-GXQ25YYZ-B5Y21TQM.js +4 -0
  110. package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  111. package/dist/assets/diagram-S7CK7UJ4-CY6td7Et.js +30 -0
  112. package/dist/assets/diagram-UQ7AKVKN-DmDqnGZf.js +41 -0
  113. package/dist/assets/diagram-VSXAHHWV-KGLWOOcE.js +3 -0
  114. package/dist/assets/diagram-VX7I27RA-D5syUraJ.js +24 -0
  115. package/dist/assets/diagram-Z3DM3KII-Dtrbs1Om.js +24 -0
  116. package/dist/assets/dist-Dce-2YbA.js +1 -0
  117. package/dist/assets/ebnfDiagram-PWID7BFC-CQpJLtU3.js +1 -0
  118. package/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
  119. package/dist/assets/erDiagram-RLTQ6QDP-cMTzKLu_.js +99 -0
  120. package/dist/assets/eventmodeling-NTZA5JFV-CMzLdNaL.js +1 -0
  121. package/dist/assets/flowDiagram-HODETNUW-BMUajB-G.js +1 -0
  122. package/dist/assets/ganttDiagram-EL5Y4UJY-DseBS5Fs.js +292 -0
  123. package/dist/assets/gitGraph-4MIJSDKK-CUQZRpxJ.js +1 -0
  124. package/dist/assets/gitGraphDiagram-WWUBYQGX-CfMfsJ3b.js +106 -0
  125. package/dist/assets/graphlib-DS17s2tU.js +1 -0
  126. package/dist/assets/index-ByQ-XX38.css +1 -0
  127. package/dist/assets/index-CIf5TpfY.js +214 -0
  128. package/dist/assets/info-A6RAGUB7-D3nAKZ8M.js +1 -0
  129. package/dist/assets/infoDiagram-27XIBGKW-zu1O0WOy.js +2 -0
  130. package/dist/assets/init-C-OQMol4.js +1 -0
  131. package/dist/assets/ishikawaDiagram-5VMMS53U-DKC7BO9p.js +70 -0
  132. package/dist/assets/journeyDiagram-3NMN7TZE-Ld5mXse7.js +139 -0
  133. package/dist/assets/kanban-definition-UXKFOSKX-CH7_QlHL.js +89 -0
  134. package/dist/assets/katex-DJhrJSbw.js +257 -0
  135. package/dist/assets/line-N9XiCREw.js +1 -0
  136. package/dist/assets/linear-C9hnqmJv.js +1 -0
  137. package/dist/assets/mermaid-parser.core-DgPwV5Bh.js +7 -0
  138. package/dist/assets/mermaid.core-BKqynXLR.js +44 -0
  139. package/dist/assets/mindmap-definition-YA3MSWOX-DkiL-wiW.js +96 -0
  140. package/dist/assets/monaco-DjpsE8to.css +1 -0
  141. package/dist/assets/monaco-DqzJaNIe.js +908 -0
  142. package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  143. package/dist/assets/packet-AYTQ26CC-DuMTvpEJ.js +1 -0
  144. package/dist/assets/path-fybaL0A-.js +1 -0
  145. package/dist/assets/pegDiagram-XKGWAZYB-DvMl_xSe.js +1 -0
  146. package/dist/assets/pie-WAS4IAKB-Da0xPaD8.js +1 -0
  147. package/dist/assets/pieDiagram-E7YTZNPT-amANNRJY.js +39 -0
  148. package/dist/assets/quadrantDiagram-AXDQQJYC-BrgvDMnk.js +7 -0
  149. package/dist/assets/radar-RG4KPBEZ-Dz3Ih5-N.js +1 -0
  150. package/dist/assets/railroad-74A4TZTK-CKMW-IV2.js +1 -0
  151. package/dist/assets/railroad-abnf-HS5TGJTU-ChqmSWSF.js +1 -0
  152. package/dist/assets/railroad-ebnf-LZEXJU2U-V3R4fFdb.js +1 -0
  153. package/dist/assets/railroad-peg-WCYAUIDC-B-RuhUQW.js +1 -0
  154. package/dist/assets/railroadDiagram-O6MQD6OU-FjG-6OLu.js +1 -0
  155. package/dist/assets/requirementDiagram-BXWQKSXE-Cnj8RfE4.js +84 -0
  156. package/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  157. package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  158. package/dist/assets/sankeyDiagram-P5KCCOFB-DMu1fnEh.js +40 -0
  159. package/dist/assets/sequenceDiagram-WJ2MYXX4-CtCtaRpW.js +162 -0
  160. package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  161. package/dist/assets/src-CGp3bfio.js +1 -0
  162. package/dist/assets/stateDiagram-D77RDMKH-xcJbLNhz.js +1 -0
  163. package/dist/assets/stateDiagram-v2-MP3YSRHH-DYHrQmJJ.js +1 -0
  164. package/dist/assets/swimlanes-42K2YHIH-BwLln_ga.js +1 -0
  165. package/dist/assets/swimlanesDiagram-VR7AAH4N-BcDfUQ0A.js +8 -0
  166. package/dist/assets/timeline-definition-24CTP7MA-ucNGsUUz.js +120 -0
  167. package/dist/assets/treeView-Q6P3EWNA-3uOomRue.js +1 -0
  168. package/dist/assets/treemap-WGGIJYW6-iGK7vwID.js +1 -0
  169. package/dist/assets/vennDiagram-4TSXK5OY-B9LgGe16.js +34 -0
  170. package/dist/assets/wardley-WFR3VGLG-FupMOf6k.js +1 -0
  171. package/dist/assets/wardleyDiagram-VM6X3IG4-CGZdEXM2.js +78 -0
  172. package/dist/assets/xterm-BrP-ENHg.css +1 -0
  173. package/dist/assets/xterm-C8zLTNGk.js +16 -0
  174. package/dist/assets/xychartDiagram-S5SC5T6Z-DafZrSBJ.js +7 -0
  175. package/dist/favicon.ico +0 -0
  176. package/dist/favicon.svg +6 -0
  177. package/dist/index.html +34 -0
  178. package/extensions/implement-goal-issue.ts +368 -0
  179. package/extensions/implement-next-epic.ts +835 -0
  180. package/index.html +28 -0
  181. package/install.sh +111 -0
  182. package/issues/control.ts +91 -0
  183. package/issues/guards.ts +12 -0
  184. package/issues/ledger.ts +122 -0
  185. package/issues/legacy.ts +68 -0
  186. package/issues/lock.ts +305 -0
  187. package/issues/paths.ts +76 -0
  188. package/issues/preflight.ts +189 -0
  189. package/issues/repository.ts +203 -0
  190. package/issues/scout/actions.ts +255 -0
  191. package/issues/scout/control.ts +80 -0
  192. package/issues/scout/github.ts +358 -0
  193. package/issues/scout/paths.ts +129 -0
  194. package/issues/scout/preflight.ts +277 -0
  195. package/issues/scout/recommendation.ts +400 -0
  196. package/issues/scout/scan.ts +143 -0
  197. package/issues/scout/state.ts +448 -0
  198. package/issues/scout/worker.ts +1282 -0
  199. package/issues/state.ts +584 -0
  200. package/issues/tracker.ts +271 -0
  201. package/issues/worker.ts +1065 -0
  202. package/lib/dependencies.ts +61 -0
  203. package/lib/forge-auth.ts +953 -0
  204. package/lib/forge.ts +141 -0
  205. package/lib/gitea.ts +729 -0
  206. package/lib/github-release.ts +318 -0
  207. package/lib/inventory.test.ts +79 -0
  208. package/lib/inventory.ts +53 -0
  209. package/lib/release.test.ts +269 -0
  210. package/lib/release.ts +801 -0
  211. package/omp/commands/adr-resolve.md +8 -0
  212. package/omp/commands/discover.md +6 -0
  213. package/omp/commands/epic-next.md +8 -0
  214. package/omp/commands/flow-change.md +8 -0
  215. package/omp/commands/flow-goal.md +8 -0
  216. package/omp/commands/flow-implement.md +8 -0
  217. package/omp/commands/flow-widget.md +8 -0
  218. package/omp/commands/goal-draft.md +8 -0
  219. package/omp/commands/goal-outcome.md +8 -0
  220. package/omp/commands/implement-issue.md +8 -0
  221. package/omp/commands/prepare-skills.md +6 -0
  222. package/omp/commands/scout-recommendations.md +8 -0
  223. package/omp/commands/setup-devboard.md +24 -0
  224. package/omp/config.yml +12 -0
  225. package/omp/extensions/implement-goal-issue.ts +2 -0
  226. package/omp/extensions/implement-next-epic.ts +2 -0
  227. package/omp/mcp.json +9 -0
  228. package/package.json +86 -7
  229. package/prompts/adr-resolve.md +130 -0
  230. package/prompts/discover.md +541 -0
  231. package/prompts/epic-next.md +533 -0
  232. package/prompts/flow-change.md +76 -0
  233. package/prompts/flow-goal.md +126 -0
  234. package/prompts/flow-implement.md +103 -0
  235. package/prompts/flow-widget.md +54 -0
  236. package/prompts/goal-draft.md +147 -0
  237. package/prompts/goal-outcome.md +87 -0
  238. package/prompts/implement-issue.md +66 -0
  239. package/prompts/prepare-skills.md +142 -0
  240. package/prompts/scout-recommendations.md +143 -0
  241. package/public/apple-touch-icon.png +0 -0
  242. package/public/favicon.ico +0 -0
  243. package/public/favicon.svg +6 -0
  244. package/server/acp.ts +540 -0
  245. package/server/activity.test.ts +96 -0
  246. package/server/activity.ts +48 -0
  247. package/server/app.test.ts +882 -0
  248. package/server/app.ts +6179 -0
  249. package/server/ask.test.ts +543 -0
  250. package/server/ask.ts +589 -0
  251. package/server/dependencies.test.ts +118 -0
  252. package/server/discover-remote.test.ts +1602 -0
  253. package/server/discover-sheet.test.ts +176 -0
  254. package/server/discover.test.ts +1510 -0
  255. package/server/discover.ts +3198 -0
  256. package/server/feedback.test.ts +458 -0
  257. package/server/feedback.ts +369 -0
  258. package/server/fixtures/adr-source.ts +71 -0
  259. package/server/fixtures/hang-download.ts +14 -0
  260. package/server/fixtures/stub-agent.ts +1870 -0
  261. package/server/fixtures/stub-forge.ts +141 -0
  262. package/server/fixtures/stub-gh.ts +57 -0
  263. package/server/flow-pr.test.ts +976 -0
  264. package/server/flow.test.ts +1632 -0
  265. package/server/flow.ts +2655 -0
  266. package/server/forge-pr.ts +387 -0
  267. package/server/goal-creation.test.ts +957 -0
  268. package/server/goal-draft.test.ts +348 -0
  269. package/server/goal-drafts.ts +449 -0
  270. package/server/goal-proposal.test.ts +479 -0
  271. package/server/goal-proposal.ts +232 -0
  272. package/server/goal-revision.test.ts +613 -0
  273. package/server/goal-slug.test.ts +56 -0
  274. package/server/goal-slug.ts +96 -0
  275. package/server/goal.ts +20 -0
  276. package/server/index.ts +147 -0
  277. package/server/issue-close.test.ts +100 -0
  278. package/server/issue-close.ts +178 -0
  279. package/server/issues.test.ts +167 -0
  280. package/server/issues.ts +479 -0
  281. package/server/journal.test.ts +350 -0
  282. package/server/journal.ts +329 -0
  283. package/server/lifecycle-pause.test.ts +1795 -0
  284. package/server/lifecycle.test.ts +1879 -0
  285. package/server/parallel-lifecycle.test.ts +1029 -0
  286. package/server/persistence-restart.test.ts +723 -0
  287. package/server/persistence.test.ts +1935 -0
  288. package/server/records.test.ts +180 -0
  289. package/server/records.ts +99 -0
  290. package/server/reset-implementation.test.ts +997 -0
  291. package/server/run.test.ts +611 -0
  292. package/server/scout.test.ts +877 -0
  293. package/server/scout.ts +459 -0
  294. package/server/skills-preparation.test.ts +1073 -0
  295. package/server/state.ts +249 -0
  296. package/server/tracker.ts +369 -0
  297. package/server/updates.test.ts +444 -0
  298. package/server/updates.ts +198 -0
  299. package/server/version.test.ts +140 -0
  300. package/server/version.ts +151 -0
  301. package/server/watchdog.ts +389 -0
  302. package/svelte.config.js +5 -0
  303. package/templates/host-AGENTS.md +235 -0
  304. package/tsconfig.json +21 -0
  305. package/vite.config.ts +21 -0
  306. package/watchdog/cleanup-main.ts +98 -0
  307. package/watchdog/cleanup.ts +301 -0
  308. package/watchdog/collect.ts +311 -0
  309. package/watchdog/coordinator.ts +397 -0
  310. package/watchdog/delivery.ts +107 -0
  311. package/watchdog/discovery.ts +209 -0
  312. package/watchdog/findings.ts +211 -0
  313. package/watchdog/github.ts +189 -0
  314. package/watchdog/history.ts +138 -0
  315. package/watchdog/install.ts +103 -0
  316. package/watchdog/ledger.ts +188 -0
  317. package/watchdog/lock.ts +63 -0
  318. package/watchdog/main.ts +106 -0
  319. package/watchdog/status.ts +71 -0
  320. package/watchdog/systemd.ts +96 -0
  321. package/watchdog/uninstall.ts +30 -0
  322. package/watchdog/units/devboard-watchdog.service +9 -0
  323. package/watchdog/units/devboard-watchdog.timer +10 -0
@@ -0,0 +1,449 @@
1
+ /**
2
+ * Proposed-goal drafts and their one elaboration peer.
3
+ *
4
+ * A draft is unprocessed Markdown under `<REPO_DIR>/.devboard/goal-proposals/`.
5
+ * The Drafts view lists them, shows one verbatim, and can start the harness
6
+ * `/goal-draft <path>` command in one ACP peer at a time. The peer itself owns
7
+ * the interview, the goal spec under `.devboard/goals/`, the GitHub issue, and
8
+ * the draft removal; this module only validates paths, streams the transcript,
9
+ * and refuses a second concurrent elaboration.
10
+ */
11
+
12
+ import { readdirSync, readFileSync, realpathSync, statSync, type Dirent } from "node:fs";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { AcpConnection, type AcpActivity, type AcpQuestion } from "./acp";
15
+ import { GOAL_PROPOSALS_DIRECTORY } from "./goal-proposal";
16
+ import {
17
+ appendActivityLine,
18
+ trimActivityLines,
19
+ } from "../issues/state";
20
+ import type { RecordedChild } from "./state";
21
+
22
+ export type GoalDraftStatus = "idle" | "running" | "succeeded" | "failed" | "stopped";
23
+
24
+ export interface GoalDraftSummary {
25
+ /** Repository-relative path with `/` separators. */
26
+ path: string;
27
+ name: string;
28
+ /** The first Markdown heading text, or the empty string when there is none. */
29
+ heading: string;
30
+ }
31
+
32
+ export interface GoalDraft extends GoalDraftSummary {
33
+ text: string;
34
+ }
35
+
36
+ export interface GoalDraftQuestion {
37
+ id: number | string;
38
+ message: string;
39
+ options: Array<{ label: string; description?: string; recommended: boolean }>;
40
+ }
41
+
42
+ export interface GoalDraftRun {
43
+ status: GoalDraftStatus;
44
+ path: string | null;
45
+ startedAt: string | null;
46
+ finishedAt: string | null;
47
+ error: string | null;
48
+ activity: string[];
49
+ question: GoalDraftQuestion | null;
50
+ }
51
+
52
+ /** A refusal the dashboard shows verbatim; nothing is read or started. */
53
+ export class GoalDraftError extends Error {
54
+ readonly status: number;
55
+
56
+ constructor(status: number, message: string) {
57
+ super(message);
58
+ this.name = "GoalDraftError";
59
+ this.status = status;
60
+ }
61
+ }
62
+
63
+ /** The first ATX heading line's text, with trailing hashes removed. */
64
+ export function goalDraftHeading(text: string): string {
65
+ for (const line of text.split("\n")) {
66
+ const match = /^#{1,6}\s+(.+?)\s*$/.exec(line);
67
+ if (match?.[1] !== undefined) {
68
+ const heading = match[1].replace(/\s+#+\s*$/, "").trim();
69
+ if (heading.length > 0) return heading;
70
+ }
71
+ }
72
+ return "";
73
+ }
74
+
75
+ /**
76
+ * Resolves one repository-relative draft path. Only
77
+ * `<GOAL_PROPOSALS_DIRECTORY>/<name>.md` is accepted, the file must exist as a
78
+ * regular file, and its real path must stay directly inside the real drafts
79
+ * directory, so neither traversal nor a symlink can read elsewhere.
80
+ */
81
+ export function resolveGoalDraft(
82
+ repoDir: string,
83
+ relativePath: unknown,
84
+ ): { path: string; name: string; file: string } {
85
+ if (typeof relativePath !== "string") {
86
+ throw new GoalDraftError(400, "A draft path is required.");
87
+ }
88
+ const normalized = relativePath.replace(/\\/g, "/");
89
+ if (normalized.length === 0 || /^[A-Za-z]:\//.test(normalized) || normalized.startsWith("/")) {
90
+ throw new GoalDraftError(400, "A draft path must be repository-relative.");
91
+ }
92
+ const parts = normalized.split("/");
93
+ if (
94
+ parts.length !== 3 ||
95
+ parts[0] !== ".devboard" ||
96
+ parts[1] !== "goal-proposals"
97
+ ) {
98
+ throw new GoalDraftError(
99
+ 400,
100
+ `A draft path must name a file directly under ${GOAL_PROPOSALS_DIRECTORY}/.`,
101
+ );
102
+ }
103
+ const name = parts[2] ?? "";
104
+ if (name.length === 0 || name === "." || name === ".." || !name.endsWith(".md")) {
105
+ throw new GoalDraftError(400, "A draft path must end in .md.");
106
+ }
107
+
108
+ const directory = resolve(repoDir, GOAL_PROPOSALS_DIRECTORY);
109
+ let realDirectory: string;
110
+ try {
111
+ realDirectory = realpathSync(directory);
112
+ } catch {
113
+ throw new GoalDraftError(404, `No draft at ${normalized}.`);
114
+ }
115
+ const candidate = join(directory, name);
116
+ let realFile: string;
117
+ try {
118
+ realFile = realpathSync(candidate);
119
+ } catch {
120
+ throw new GoalDraftError(404, `No draft at ${normalized}.`);
121
+ }
122
+ if (dirname(realFile) !== realDirectory) {
123
+ throw new GoalDraftError(400, `A draft path must stay directly under ${GOAL_PROPOSALS_DIRECTORY}/.`);
124
+ }
125
+ if (!statSync(realFile).isFile()) {
126
+ throw new GoalDraftError(400, "A draft must be a regular file.");
127
+ }
128
+ return { path: `${GOAL_PROPOSALS_DIRECTORY}/${name}`, name, file: realFile };
129
+ }
130
+
131
+ /** Every regular `.md` file directly under the drafts directory, by name. */
132
+ export function listGoalDrafts(repoDir: string): GoalDraftSummary[] {
133
+ const directory = resolve(repoDir, GOAL_PROPOSALS_DIRECTORY);
134
+ let entries: Dirent[];
135
+ try {
136
+ entries = readdirSync(directory, { withFileTypes: true });
137
+ } catch {
138
+ return [];
139
+ }
140
+ const drafts: GoalDraftSummary[] = [];
141
+ for (const entry of entries) {
142
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
143
+ let text: string;
144
+ try {
145
+ text = readFileSync(join(directory, entry.name), "utf8");
146
+ } catch {
147
+ continue;
148
+ }
149
+ drafts.push({
150
+ path: `${GOAL_PROPOSALS_DIRECTORY}/${entry.name}`,
151
+ name: entry.name,
152
+ heading: goalDraftHeading(text),
153
+ });
154
+ }
155
+ return drafts.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
156
+ }
157
+
158
+ /** Reads one draft's bytes exactly, after the same path validation. */
159
+ export function readGoalDraft(repoDir: string, relativePath: unknown): GoalDraft {
160
+ const resolved = resolveGoalDraft(repoDir, relativePath);
161
+ let text: string;
162
+ try {
163
+ text = readFileSync(resolved.file, "utf8");
164
+ } catch {
165
+ throw new GoalDraftError(404, `No draft at ${resolved.path}.`);
166
+ }
167
+ return {
168
+ path: resolved.path,
169
+ name: resolved.name,
170
+ heading: goalDraftHeading(text),
171
+ text,
172
+ };
173
+ }
174
+
175
+ export interface GoalDraftPeer {
176
+ readonly pid: number | undefined;
177
+ readonly argv: string[];
178
+ run(prompt: string): Promise<unknown>;
179
+ answer(id: number | string, value: string): boolean;
180
+ close(): void;
181
+ }
182
+
183
+ export interface GoalDraftPeerOptions {
184
+ command: string;
185
+ cwd: string;
186
+ onActivity: (activity: AcpActivity) => void;
187
+ onQuestion: (question: AcpQuestion) => void;
188
+ onStderr: (text: string) => void;
189
+ }
190
+
191
+ export interface GoalDraftManagerOptions {
192
+ repoDir: string;
193
+ command: string;
194
+ send: (event: unknown) => void;
195
+ /** Injected for tests; defaults to the real ACP client. */
196
+ connect?: (options: GoalDraftPeerOptions) => GoalDraftPeer;
197
+ now?: () => Date;
198
+ log?: (line: string) => void;
199
+ }
200
+
201
+ interface Elaboration {
202
+ path: string;
203
+ /** Undefined only while the peer is being constructed. */
204
+ peer: GoalDraftPeer | undefined;
205
+ activity: string[];
206
+ firstStderr: string;
207
+ startedAt: string;
208
+ }
209
+
210
+ function firstLine(text: string): string {
211
+ return (
212
+ text
213
+ .split("\n")
214
+ .map((line) => line.trim())
215
+ .find((line) => line.length > 0) ?? ""
216
+ );
217
+ }
218
+
219
+ const IDLE: GoalDraftRun = {
220
+ status: "idle",
221
+ path: null,
222
+ startedAt: null,
223
+ finishedAt: null,
224
+ error: null,
225
+ activity: [],
226
+ question: null,
227
+ };
228
+
229
+ /** One elaboration at a time, scoped to one repository. */
230
+ export class GoalDraftManager {
231
+ private readonly repoDir: string;
232
+ private readonly command: string;
233
+ private readonly send: (event: unknown) => void;
234
+ private readonly connect: (options: GoalDraftPeerOptions) => GoalDraftPeer;
235
+ private readonly clock: () => Date;
236
+ private readonly log: (line: string) => void;
237
+
238
+ private run: GoalDraftRun = { ...IDLE };
239
+ private current: Elaboration | undefined;
240
+
241
+ constructor(options: GoalDraftManagerOptions) {
242
+ this.repoDir = resolve(options.repoDir);
243
+ this.command = options.command;
244
+ this.send = options.send;
245
+ this.connect =
246
+ options.connect ??
247
+ ((peerOptions) => new AcpConnection(peerOptions) as unknown as GoalDraftPeer);
248
+ this.clock = options.now ?? (() => new Date());
249
+ this.log = options.log ?? (() => undefined);
250
+ }
251
+
252
+ snapshot(): GoalDraftRun {
253
+ return {
254
+ ...this.run,
255
+ activity: [...this.run.activity],
256
+ question:
257
+ this.run.question === null
258
+ ? null
259
+ : { ...this.run.question, options: this.run.question.options.map((option) => ({ ...option })) },
260
+ };
261
+ }
262
+
263
+ get running(): boolean {
264
+ return this.run.status === "running";
265
+ }
266
+
267
+ private publishActivity(): void {
268
+ this.send({ type: "issuesDraftState", draft: this.snapshot() });
269
+ }
270
+
271
+ publishDrafts(): void {
272
+ this.send({ type: "issuesDrafts", drafts: listGoalDrafts(this.repoDir) });
273
+ }
274
+
275
+ /**
276
+ * Ends the elaboration: closes its peer exactly once, then clears it. The
277
+ * peer is closed here rather than at each call site so a success, a
278
+ * failure, a stop, and a connect failure all release the agent process.
279
+ */
280
+ private finish(status: GoalDraftStatus, error: string | null): void {
281
+ const elaboration = this.current;
282
+ this.current = undefined;
283
+ if (elaboration?.peer !== undefined) {
284
+ try {
285
+ elaboration.peer.close();
286
+ } catch {
287
+ // The peer already exited.
288
+ }
289
+ }
290
+ this.run = {
291
+ ...this.run,
292
+ status,
293
+ finishedAt: this.clock().toISOString(),
294
+ error,
295
+ question: null,
296
+ };
297
+ this.publishActivity();
298
+ this.publishDrafts();
299
+ }
300
+
301
+ /**
302
+ * The running elaboration peer's identity, recorded in the state so a
303
+ * replacement server can end an orphaned child.
304
+ */
305
+ child(): RecordedChild | undefined {
306
+ const peer = this.current?.peer;
307
+ if (peer === undefined) return undefined;
308
+ const pid = peer.pid;
309
+ if (pid === undefined) return undefined;
310
+ return { pid, argv: peer.argv, cwd: this.repoDir };
311
+ }
312
+
313
+ start(relativePath: unknown): { ok: true; path: string } | { ok: false; message: string } {
314
+ if (this.current !== undefined) {
315
+ const path = this.current.path;
316
+ return {
317
+ ok: false,
318
+ message: `An elaboration is already running for ${path}. Stop it before starting another draft.`,
319
+ };
320
+ }
321
+ let draft: GoalDraft;
322
+ try {
323
+ draft = readGoalDraft(this.repoDir, relativePath);
324
+ } catch (error) {
325
+ return {
326
+ ok: false,
327
+ message: error instanceof GoalDraftError ? error.message : String(error),
328
+ };
329
+ }
330
+
331
+ const activity: string[] = [];
332
+ this.run = {
333
+ status: "running",
334
+ path: draft.path,
335
+ startedAt: this.clock().toISOString(),
336
+ finishedAt: null,
337
+ error: null,
338
+ activity,
339
+ question: null,
340
+ };
341
+
342
+ const elaboration: Elaboration = {
343
+ path: draft.path,
344
+ peer: undefined,
345
+ activity,
346
+ firstStderr: "",
347
+ startedAt: this.run.startedAt ?? this.clock().toISOString(),
348
+ };
349
+ this.current = elaboration;
350
+ let peer: GoalDraftPeer;
351
+ try {
352
+ peer = this.connect({
353
+ command: this.command,
354
+ cwd: this.repoDir,
355
+ onActivity: (entry) => {
356
+ if (this.current !== elaboration) return;
357
+ appendActivityLine(elaboration.activity, entry.text, entry.append);
358
+ trimActivityLines(elaboration.activity);
359
+ this.run = { ...this.run, activity: elaboration.activity };
360
+ this.publishActivity();
361
+ },
362
+ onQuestion: (question) => {
363
+ if (this.current !== elaboration) return;
364
+ this.run = {
365
+ ...this.run,
366
+ question: {
367
+ id: question.id,
368
+ message: question.message,
369
+ options: question.options.map((option) => ({
370
+ label: option.label,
371
+ ...(option.description === undefined ? {} : { description: option.description }),
372
+ recommended: option.recommended,
373
+ })),
374
+ },
375
+ };
376
+ this.publishActivity();
377
+ },
378
+ onStderr: (text) => {
379
+ if (this.current !== elaboration) return;
380
+ const line = firstLine(text);
381
+ if (line.length > 0 && elaboration.firstStderr.length === 0) {
382
+ elaboration.firstStderr = line;
383
+ }
384
+ },
385
+ });
386
+ } catch (error) {
387
+ this.finish("failed", error instanceof Error ? error.message : String(error));
388
+ return { ok: false, message: this.run.error ?? "the elaboration peer could not be started" };
389
+ }
390
+
391
+ elaboration.peer = peer;
392
+ this.publishActivity();
393
+ this.publishDrafts();
394
+
395
+ void peer
396
+ .run(`/goal-draft ${draft.path}`)
397
+ .then(() => {
398
+ if (this.current !== elaboration) return;
399
+ this.finish("succeeded", null);
400
+ })
401
+ .catch((error: unknown) => {
402
+ if (this.current !== elaboration) return;
403
+ const detail =
404
+ error instanceof Error && error.message.length > 0
405
+ ? error.message
406
+ : elaboration.firstStderr.length > 0
407
+ ? elaboration.firstStderr
408
+ : "the elaboration agent ended with an error";
409
+ this.finish("failed", detail);
410
+ });
411
+
412
+ return { ok: true, path: draft.path };
413
+ }
414
+
415
+ stop(): boolean {
416
+ if (this.current === undefined) return false;
417
+ this.finish("stopped", null);
418
+ return true;
419
+ }
420
+
421
+ answer(id: number | string, value: string): boolean {
422
+ const elaboration = this.current;
423
+ if (elaboration === undefined || elaboration.peer === undefined) return false;
424
+ if (this.run.question === null) return false;
425
+ if (String(this.run.question.id) !== String(id)) return false;
426
+ if (!elaboration.peer.answer(id, value)) return false;
427
+ this.run = { ...this.run, question: null };
428
+ this.publishActivity();
429
+ return true;
430
+ }
431
+
432
+ /** Ends the peer without touching the draft; used when the server stops. */
433
+ dispose(): void {
434
+ const elaboration = this.current;
435
+ if (elaboration === undefined) return;
436
+ try {
437
+ elaboration.peer?.close();
438
+ } catch {
439
+ // The peer already exited.
440
+ }
441
+ this.current = undefined;
442
+ this.run = {
443
+ ...this.run,
444
+ status: "stopped",
445
+ finishedAt: this.clock().toISOString(),
446
+ question: null,
447
+ };
448
+ }
449
+ }