@dsdenes/devboard 0.1.560 → 0.1.562

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 +709 -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,348 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { mkdir, mkdtemp, rm, unlink } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import type { AcpActivity, AcpQuestion } from "../server/acp";
6
+ import {
7
+ GoalDraftError,
8
+ GoalDraftManager,
9
+ goalDraftHeading,
10
+ listGoalDrafts,
11
+ readGoalDraft,
12
+ resolveGoalDraft,
13
+ type GoalDraftPeer,
14
+ type GoalDraftPeerOptions,
15
+ } from "../server/goal-drafts";
16
+
17
+ /**
18
+ * Draft reads and the one elaboration peer. Every fixture lives under
19
+ * `.devboard/.tmp`; the peer is a controllable fake and no agent, GitHub
20
+ * object, or file outside the fixture is touched.
21
+ */
22
+
23
+ const harnessRoot = resolve(import.meta.dir, "..");
24
+ const testRoot = join(harnessRoot, ".tmp");
25
+ const temporaryDirectories: string[] = [];
26
+ const managers: GoalDraftManager[] = [];
27
+
28
+ afterAll(async () => {
29
+ for (const manager of managers) manager.dispose();
30
+ await Promise.all(
31
+ temporaryDirectories.map((directory) => rm(directory, { recursive: true, force: true })),
32
+ );
33
+ });
34
+
35
+ async function fixture(): Promise<{ root: string; drafts: string }> {
36
+ await mkdir(testRoot, { recursive: true });
37
+ const root = await mkdtemp(join(testRoot, "goal-drafts-"));
38
+ temporaryDirectories.push(root);
39
+ const drafts = join(root, ".devboard", "goal-proposals");
40
+ await mkdir(drafts, { recursive: true });
41
+ return { root, drafts };
42
+ }
43
+
44
+ function draftText(heading: string, body: string): string {
45
+ return `# ${heading}\n\n> Unprocessed draft.\n\n${body}\n`;
46
+ }
47
+
48
+ class FakePeer implements GoalDraftPeer {
49
+ readonly pid = 4242;
50
+ readonly argv = ["stub-acp"];
51
+ readonly prompts: string[] = [];
52
+ readonly options: GoalDraftPeerOptions;
53
+ closed = false;
54
+ closes = 0;
55
+ private settle: { resolve: (value: unknown) => void; reject: (error: Error) => void } | undefined;
56
+
57
+ constructor(options: GoalDraftPeerOptions) {
58
+ this.options = options;
59
+ }
60
+
61
+ run(prompt: string): Promise<unknown> {
62
+ this.prompts.push(prompt);
63
+ const deferred = Promise.withResolvers<unknown>();
64
+ this.settle = { resolve: deferred.resolve, reject: deferred.reject };
65
+ return deferred.promise;
66
+ }
67
+
68
+ answer(id: number | string, value: string): boolean {
69
+ if (this.lastQuestion === undefined || String(this.lastQuestion.id) !== String(id)) return false;
70
+ this.options.onActivity({ text: `answered ${value}`, append: false });
71
+ return true;
72
+ }
73
+
74
+ private lastQuestion: AcpQuestion | undefined;
75
+
76
+ close(): void {
77
+ this.closes += 1;
78
+ this.closed = true;
79
+ this.settle?.reject(new Error("Agent process exited before answering."));
80
+ }
81
+
82
+ emit(text: string, append = false): void {
83
+ this.options.onActivity({ text, append } satisfies AcpActivity);
84
+ }
85
+
86
+ ask(question: AcpQuestion): void {
87
+ this.lastQuestion = question;
88
+ this.options.onQuestion(question);
89
+ }
90
+
91
+ succeed(): void {
92
+ this.settle?.resolve({});
93
+ this.settle = undefined;
94
+ }
95
+
96
+ fail(message: string): void {
97
+ this.options.onStderr(`${message}\n`);
98
+ this.settle?.reject(new Error(message));
99
+ this.settle = undefined;
100
+ }
101
+ }
102
+
103
+ function managerFor(root: string, peers: FakePeer[]): { manager: GoalDraftManager; events: unknown[] } {
104
+ const events: unknown[] = [];
105
+ const manager = new GoalDraftManager({
106
+ repoDir: root,
107
+ command: "omp acp",
108
+ send: (event) => events.push(event),
109
+ connect: (options) => {
110
+ const peer = new FakePeer(options);
111
+ peers.push(peer);
112
+ return peer;
113
+ },
114
+ });
115
+ managers.push(manager);
116
+ return { manager, events };
117
+ }
118
+
119
+ function isEventType(event: unknown, type: string): boolean {
120
+ return typeof event === "object" && event !== null && "type" in event && event.type === type;
121
+ }
122
+
123
+ async function waitFor<T>(read: () => T | undefined, description: string, timeoutMs = 3000): Promise<T> {
124
+ const deadline = Date.now() + timeoutMs;
125
+ for (;;) {
126
+ const value = read();
127
+ if (value !== undefined && value !== null) return value;
128
+ if (Date.now() > deadline) throw new Error(`timed out waiting for ${description}`);
129
+ await Bun.sleep(10);
130
+ }
131
+ }
132
+
133
+ describe("draft listing and reading", () => {
134
+ test("lists regular direct .md files sorted by name with their first heading", async () => {
135
+ const { root, drafts } = await fixture();
136
+ writeFileSync(join(drafts, "beta.md"), draftText("Beta heading", "Beta body"));
137
+ writeFileSync(join(drafts, "alpha.md"), draftText("Alpha heading", "Alpha body"));
138
+ writeFileSync(join(drafts, "notes.txt"), "not a draft\n");
139
+ await mkdir(join(drafts, "nested.md"), { recursive: true });
140
+
141
+ const listed = listGoalDrafts(root);
142
+ expect(listed.map((draft) => draft.name)).toEqual(["alpha.md", "beta.md"]);
143
+ expect(listed[0]).toEqual({
144
+ path: ".devboard/goal-proposals/alpha.md",
145
+ name: "alpha.md",
146
+ heading: "Alpha heading",
147
+ });
148
+ expect(goalDraftHeading("no heading\n## Second\n")).toBe("Second");
149
+
150
+ const read = readGoalDraft(root, ".devboard/goal-proposals/alpha.md");
151
+ expect(read.text).toBe(draftText("Alpha heading", "Alpha body"));
152
+ expect(read.heading).toBe("Alpha heading");
153
+ });
154
+
155
+ test("refuses traversal, absolute paths, directories, symlink escapes, and missing files", async () => {
156
+ const { root, drafts } = await fixture();
157
+ writeFileSync(join(drafts, "ok.md"), draftText("Ok", "body"));
158
+ writeFileSync(join(root, ".devboard", "secret.md"), "secret\n");
159
+ await mkdir(join(drafts, "folder.md"), { recursive: true });
160
+ symlinkSync(join(root, ".devboard", "secret.md"), join(drafts, "escape.md"));
161
+
162
+ for (const path of [
163
+ "../secret.md",
164
+ ".devboard/goal-proposals/../../secret.md",
165
+ "/etc/passwd",
166
+ ".devboard/goal-proposals",
167
+ ".devboard/goal-proposals/folder.md",
168
+ ".devboard/goal-proposals/escape.md",
169
+ ".devboard/goal-proposals/missing.md",
170
+ "goal-proposals/ok.md",
171
+ ]) {
172
+ let error: unknown;
173
+ try {
174
+ readGoalDraft(root, path);
175
+ } catch (caught) {
176
+ error = caught;
177
+ }
178
+ expect(error).toBeInstanceOf(GoalDraftError);
179
+ expect((error as GoalDraftError).message.length).toBeGreaterThan(0);
180
+ }
181
+ expect(readGoalDraft(root, ".devboard/goal-proposals/ok.md").text).toContain("Ok");
182
+ expect(() => resolveGoalDraft(root, 42)).toThrow(GoalDraftError);
183
+ });
184
+ });
185
+
186
+ describe("elaboration", () => {
187
+ test("sends exactly one /goal-draft prompt and streams activity and questions", async () => {
188
+ const { root, drafts } = await fixture();
189
+ writeFileSync(join(drafts, "elaborate-me.md"), draftText("Elaborate me", "body"));
190
+ const peers: FakePeer[] = [];
191
+ const { manager, events } = managerFor(root, peers);
192
+
193
+ const started = manager.start(".devboard/goal-proposals/elaborate-me.md");
194
+ expect(started.ok).toBe(true);
195
+ const peer = await waitFor(() => peers[0], "the peer");
196
+ expect(peer.prompts).toEqual(["/goal-draft .devboard/goal-proposals/elaborate-me.md"]);
197
+ expect(peer.options.command).toBe("omp acp");
198
+ expect(peer.options.cwd).toBe(root);
199
+ expect(manager.snapshot().status).toBe("running");
200
+ expect(manager.snapshot().path).toBe(".devboard/goal-proposals/elaborate-me.md");
201
+
202
+ peer.emit("Reading the goal proposals");
203
+ expect(manager.snapshot().activity).toEqual(["Reading the goal proposals"]);
204
+ peer.ask({
205
+ id: 3,
206
+ message: "Which scope?",
207
+ options: [{ label: "One", description: "First", recommended: true }],
208
+ properties: {},
209
+ required: undefined,
210
+ });
211
+ const asked = await waitFor(
212
+ () => (manager.snapshot().question === null ? undefined : manager.snapshot()),
213
+ "the question",
214
+ );
215
+ expect(asked.question?.message).toBe("Which scope?");
216
+ expect(asked.question?.options[0]?.recommended).toBe(true);
217
+ expect(manager.answer(3, "One")).toBe(true);
218
+ expect(manager.snapshot().question).toBeNull();
219
+ expect(manager.snapshot().activity).toContain("answered One");
220
+
221
+ expect(events.some((event) => isEventType(event, "issuesDraftState"))).toBe(true);
222
+ });
223
+
224
+ test("runs one elaboration at a time and stop keeps the draft", async () => {
225
+ const { root, drafts } = await fixture();
226
+ writeFileSync(join(drafts, "one.md"), draftText("One", "body"));
227
+ writeFileSync(join(drafts, "two.md"), draftText("Two", "body"));
228
+ const peers: FakePeer[] = [];
229
+ const { manager } = managerFor(root, peers);
230
+
231
+ expect(manager.start(".devboard/goal-proposals/one.md").ok).toBe(true);
232
+ const first = await waitFor(() => peers[0], "the first peer");
233
+ const refused = manager.start(".devboard/goal-proposals/two.md");
234
+ expect(refused.ok).toBe(false);
235
+ if (!refused.ok) {
236
+ expect(refused.message).toContain("An elaboration is already running for");
237
+ expect(refused.message).toContain(".devboard/goal-proposals/one.md");
238
+ }
239
+
240
+ expect(manager.stop()).toBe(true);
241
+ expect(first.closed).toBe(true);
242
+ expect(first.closes).toBe(1);
243
+ expect(manager.snapshot().status).toBe("stopped");
244
+ expect(existsSync(join(drafts, "one.md"))).toBe(true);
245
+
246
+ expect(manager.start(".devboard/goal-proposals/two.md").ok).toBe(true);
247
+ const second = await waitFor(() => peers[1], "the retried peer");
248
+ expect(second.prompts).toEqual(["/goal-draft .devboard/goal-proposals/two.md"]);
249
+ });
250
+
251
+ test("a failed peer keeps the draft and reports the error; retry starts a new peer", async () => {
252
+ const { root, drafts } = await fixture();
253
+ writeFileSync(join(drafts, "fail.md"), draftText("Fail", "body"));
254
+ const peers: FakePeer[] = [];
255
+ const { manager } = managerFor(root, peers);
256
+
257
+ manager.start(".devboard/goal-proposals/fail.md");
258
+ const peer = await waitFor(() => peers[0], "the peer");
259
+ peer.fail("the model is unavailable");
260
+ const failed = await waitFor(
261
+ () => (manager.snapshot().status === "failed" ? manager.snapshot() : undefined),
262
+ "the failure",
263
+ );
264
+ expect(failed.error).toBe("the model is unavailable");
265
+ expect(existsSync(join(drafts, "fail.md"))).toBe(true);
266
+
267
+ manager.start(".devboard/goal-proposals/fail.md");
268
+ const retry = await waitFor(() => peers[1], "the retry peer");
269
+ expect(retry.prompts).toEqual(["/goal-draft .devboard/goal-proposals/fail.md"]);
270
+ });
271
+
272
+ test("a successful stub agent removes the draft only after it verifies the issue label", async () => {
273
+ const { root, drafts } = await fixture();
274
+ writeFileSync(join(drafts, "verified.md"), draftText("Verified", "body"));
275
+ writeFileSync(join(drafts, "unverified.md"), draftText("Unverified", "body"));
276
+ const peers: FakePeer[] = [];
277
+ const { manager } = managerFor(root, peers);
278
+
279
+ manager.start(".devboard/goal-proposals/verified.md");
280
+ const verified = await waitFor(() => peers[0], "the verifying peer");
281
+ // The stub stands in for the agent: file the spec, read the issue back,
282
+ // and delete only when that read carries the goal label and same bytes.
283
+ const spec = join(root, ".devboard", "goals", "verified.md");
284
+ await mkdir(join(root, ".devboard", "goals"), { recursive: true });
285
+ writeFileSync(spec, draftText("Verified", "finalized"));
286
+ const verifiedLabel = true;
287
+ if (verifiedLabel) await unlink(join(drafts, "verified.md"));
288
+ verified.succeed();
289
+ await waitFor(
290
+ () => (manager.snapshot().status === "succeeded" ? true : undefined),
291
+ "the verified success",
292
+ );
293
+ expect(existsSync(join(drafts, "verified.md"))).toBe(false);
294
+ expect(existsSync(spec)).toBe(true);
295
+ expect(verified.closed).toBe(true);
296
+ expect(verified.closes).toBe(1);
297
+ expect(manager.child()).toBeUndefined();
298
+ expect(listGoalDrafts(root).map((draft) => draft.name)).toEqual(["unverified.md"]);
299
+
300
+ manager.start(".devboard/goal-proposals/unverified.md");
301
+ const unverified = await waitFor(() => peers[1], "the failing verification peer");
302
+ expect(manager.child()).toEqual({ pid: unverified.pid, argv: ["stub-acp"], cwd: root });
303
+ const verifiedLabelOnCreate = false;
304
+ if (verifiedLabelOnCreate) await unlink(join(drafts, "unverified.md"));
305
+ unverified.fail("goal label missing from the created issue");
306
+ const failed = await waitFor(
307
+ () => (manager.snapshot().status === "failed" ? manager.snapshot() : undefined),
308
+ "the failed verification",
309
+ );
310
+ expect(existsSync(join(drafts, "unverified.md"))).toBe(true);
311
+ expect(failed.error).toContain("goal label missing");
312
+ expect(unverified.closed).toBe(true);
313
+ expect(unverified.closes).toBe(1);
314
+ expect(manager.child()).toBeUndefined();
315
+ expect(peers.filter((peer) => !peer.closed)).toHaveLength(0);
316
+ });
317
+ });
318
+
319
+ describe("goal-draft prompt contract", () => {
320
+ const prompt = readFileSync(join(harnessRoot, "prompts", "goal-draft.md"), "utf8");
321
+
322
+ test("declares the five sections in order", () => {
323
+ const headings = ["## Objective", "## Success criteria", "## Verification", "## Boundaries", "## Stop conditions"];
324
+ const positions = headings.map((heading) => prompt.indexOf(heading));
325
+ expect(positions.every((position) => position >= 0)).toBe(true);
326
+ expect([...positions].sort((left, right) => left - right)).toEqual(positions);
327
+ });
328
+
329
+ test("requires research, a three-option interview, and forbids effort questions", () => {
330
+ expect(prompt).toContain("`ask`");
331
+ expect(prompt).toContain("exactly one question");
332
+ expect(prompt).toContain("three mutually exclusive");
333
+ expect(prompt).toContain("exactly one recommended");
334
+ expect(prompt.toLowerCase()).toContain("effort");
335
+ expect(prompt.toLowerCase()).toContain("token budget");
336
+ });
337
+
338
+ test("files the spec with the goal label and deletes the draft only after verification", () => {
339
+ expect(prompt).toContain(".devboard/goals/<slug>.md");
340
+ expect(prompt).toContain("--label goal");
341
+ expect(prompt).toContain("--body-file");
342
+ expect(prompt).toContain(`gh issue view <number> --json number,url,labels,body`);
343
+ const verification = prompt.indexOf("gh issue view");
344
+ const deletion = prompt.indexOf("delete the draft");
345
+ expect(deletion).toBeGreaterThan(verification);
346
+ expect(prompt).toContain("forge-auth.ts");
347
+ });
348
+ });