@dsdenes/devboard 0.1.559 → 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
package/index.html ADDED
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" href="/favicon.ico" sizes="any" />
7
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
8
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
9
+ <title>Devboard</title>
10
+ <script>
11
+ (function () {
12
+ var theme = "dark";
13
+ try {
14
+ var stored = localStorage.getItem("devboard.theme");
15
+ if (stored === "light" || stored === "dark") theme = stored;
16
+ } catch (error) {
17
+ /* A blocked localStorage keeps the dark default. */
18
+ }
19
+ document.documentElement.dataset.theme = theme;
20
+ document.documentElement.style.colorScheme = theme;
21
+ })();
22
+ </script>
23
+ </head>
24
+ <body>
25
+ <div id="app"></div>
26
+ <script type="module" src="/client/main.ts"></script>
27
+ </body>
28
+ </html>
package/install.sh ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env bash
2
+ # Install the harness agent wrappers into the host's .omp directory.
3
+ #
4
+ # Usage: install.sh [--force] [host-path]
5
+ #
6
+ # Copies every file under this directory's omp/ tree to the same relative path
7
+ # under <host>/.omp/. The host path is discovered with
8
+ # `git rev-parse --show-toplevel` when omitted. Files that already match are left
9
+ # untouched, unrelated files in <host>/.omp/ are never removed, and a differing
10
+ # file stops the run unless --force is given.
11
+ set -euo pipefail
12
+
13
+ usage() {
14
+ cat <<'EOF'
15
+ Usage: install.sh [--force] [host-path]
16
+
17
+ Copies every file under omp/ to <host>/.omp/ at the same relative path.
18
+ The host path defaults to the git repository root of the current directory.
19
+
20
+ --force overwrite files that differ from the installed copy
21
+ -h, --help show this help
22
+ EOF
23
+ }
24
+
25
+ force=0
26
+ host_arg=""
27
+ for arg in "$@"; do
28
+ case "$arg" in
29
+ --force) force=1 ;;
30
+ -h | --help)
31
+ usage
32
+ exit 0
33
+ ;;
34
+ -*)
35
+ printf 'install: unknown option: %s\n' "$arg" >&2
36
+ usage >&2
37
+ exit 2
38
+ ;;
39
+ *)
40
+ if [ -n "$host_arg" ]; then
41
+ printf 'install: unexpected extra argument: %s\n' "$arg" >&2
42
+ exit 2
43
+ fi
44
+ host_arg="$arg"
45
+ ;;
46
+ esac
47
+ done
48
+
49
+ source_dir=$(cd -- "$(dirname -- "$0")" && pwd)
50
+ source_omp="$source_dir/omp"
51
+ if [ ! -d "$source_omp" ]; then
52
+ printf 'install: no omp directory beside %s\n' "$0" >&2
53
+ exit 1
54
+ fi
55
+
56
+ if [ -z "$host_arg" ]; then
57
+ if ! host_arg=$(git rev-parse --show-toplevel 2>/dev/null); then
58
+ printf 'install: no host path given and the working directory is not a git work tree\n' >&2
59
+ exit 1
60
+ fi
61
+ fi
62
+
63
+ if ! host=$(cd -- "$host_arg" 2>/dev/null && pwd); then
64
+ printf 'install: host path does not exist: %s\n' "$host_arg" >&2
65
+ exit 1
66
+ fi
67
+
68
+ if [ ! -d "$host/.devboard" ]; then
69
+ printf 'install: no .devboard directory in %s; install the harness there first\n' "$host" >&2
70
+ exit 1
71
+ fi
72
+
73
+ target_omp="$host/.omp"
74
+
75
+ relative_paths=()
76
+ while IFS= read -r -d '' source_file; do
77
+ relative_paths+=("${source_file#"$source_omp"/}")
78
+ done < <(find "$source_omp" -type f -print0)
79
+
80
+ if [ "${#relative_paths[@]}" -eq 0 ]; then
81
+ printf 'install: no files under %s\n' "$source_omp" >&2
82
+ exit 1
83
+ fi
84
+
85
+ conflicts=()
86
+ for relative_path in "${relative_paths[@]}"; do
87
+ destination="$target_omp/$relative_path"
88
+ if [ -e "$destination" ] && ! cmp -s -- "$source_omp/$relative_path" "$destination"; then
89
+ conflicts+=("$relative_path")
90
+ fi
91
+ done
92
+
93
+ if [ "${#conflicts[@]}" -gt 0 ] && [ "$force" -eq 0 ]; then
94
+ printf 'install: refusing to overwrite differing files in %s:\n' "$target_omp" >&2
95
+ printf ' %s\n' "${conflicts[@]}" >&2
96
+ printf 'install: rerun with --force to replace them\n' >&2
97
+ exit 1
98
+ fi
99
+
100
+ installed=0
101
+ for relative_path in ${relative_paths[@]+"${relative_paths[@]}"}; do
102
+ destination="$target_omp/$relative_path"
103
+ if [ -f "$destination" ] && cmp -s -- "$source_omp/$relative_path" "$destination"; then
104
+ continue
105
+ fi
106
+ mkdir -p -- "$(dirname -- "$destination")"
107
+ cp -- "$source_omp/$relative_path" "$destination"
108
+ installed=$((installed + 1))
109
+ done
110
+
111
+ printf 'install: %d file(s) installed into %s\n' "$installed" "$target_omp"
@@ -0,0 +1,91 @@
1
+ import { mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { isInteger, isRecord } from "./guards";
4
+
5
+ /**
6
+ * The dashboard-to-worker channel. Commands are individual files created by
7
+ * atomic rename, so the worker never reads a half-written command, and their
8
+ * names sort in submission order. A worker consumes them from any dashboard
9
+ * attached to the same repository state.
10
+ */
11
+
12
+ export type IssuesCommand =
13
+ | { type: "pause" }
14
+ | { type: "resume" }
15
+ | { type: "stop" }
16
+ | { type: "check" }
17
+ | { type: "answer"; id: number | string; value: string; runId?: string };
18
+
19
+ let sequence = 0;
20
+
21
+ export function writeCommand(directory: string, command: IssuesCommand): string {
22
+ mkdirSync(directory, { recursive: true });
23
+ sequence += 1;
24
+ const name = `${String(Date.now()).padStart(14, "0")}-${String(sequence).padStart(6, "0")}.json`;
25
+ const path = join(directory, name);
26
+ const temporary = `${path}.${process.pid}.tmp`;
27
+ writeFileSync(temporary, `${JSON.stringify(command)}\n`);
28
+ renameSync(temporary, path);
29
+ return path;
30
+ }
31
+
32
+ function parseCommand(text: string): IssuesCommand | undefined {
33
+ let parsed: unknown;
34
+ try {
35
+ parsed = JSON.parse(text);
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ if (!isRecord(parsed)) return undefined;
40
+ switch (parsed.type) {
41
+ case "pause":
42
+ case "resume":
43
+ case "stop":
44
+ case "check":
45
+ return { type: parsed.type };
46
+ case "answer":
47
+ if (
48
+ (typeof parsed.id === "string" || isInteger(parsed.id)) &&
49
+ typeof parsed.value === "string" &&
50
+ (parsed.runId === undefined || typeof parsed.runId === "string")
51
+ ) {
52
+ return {
53
+ type: "answer",
54
+ id: parsed.id,
55
+ value: parsed.value,
56
+ ...(parsed.runId === undefined ? {} : { runId: parsed.runId }),
57
+ };
58
+ }
59
+ return undefined;
60
+ default:
61
+ return undefined;
62
+ }
63
+ }
64
+
65
+ /** Reads and consumes every pending command in name order. */
66
+ export function takeCommands(directory: string): IssuesCommand[] {
67
+ let names: string[];
68
+ try {
69
+ names = readdirSync(directory).filter((name) => name.endsWith(".json")).sort();
70
+ } catch {
71
+ return [];
72
+ }
73
+ const commands: IssuesCommand[] = [];
74
+ for (const name of names) {
75
+ const path = join(directory, name);
76
+ let text: string;
77
+ try {
78
+ text = readFileSync(path, "utf8");
79
+ } catch {
80
+ continue;
81
+ }
82
+ try {
83
+ unlinkSync(path);
84
+ } catch {
85
+ // Another consumer already took it.
86
+ }
87
+ const command = parseCommand(text);
88
+ if (command !== undefined) commands.push(command);
89
+ }
90
+ return commands;
91
+ }
@@ -0,0 +1,12 @@
1
+ /** One shared object guard for every issues boundary parser. */
2
+ export function isRecord(value: unknown): value is Record<string, unknown> {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+
6
+ export function nullableString(value: unknown): value is string | null {
7
+ return value === null || typeof value === "string";
8
+ }
9
+
10
+ export function isInteger(value: unknown): value is number {
11
+ return typeof value === "number" && Number.isInteger(value);
12
+ }
@@ -0,0 +1,122 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { isInteger, isRecord } from "./guards";
4
+ import { LEDGER_VERSION, ledgerPath } from "./paths";
5
+
6
+ /**
7
+ * The ledger records which issue numbers a successful ACP prompt already
8
+ * handled, keyed by the repository's state directory. It controls triggers
9
+ * only; the prompt itself is never scoped. A closed issue that reopens is new
10
+ * again because pruning drops entries that are no longer open.
11
+ */
12
+
13
+ export interface HandledIssue {
14
+ number: number;
15
+ /** The run that handled it, or `legacy` for a migrated entry. */
16
+ runId: string;
17
+ at: string;
18
+ }
19
+
20
+ export interface Ledger {
21
+ version: 1;
22
+ handled: Record<string, HandledIssue>;
23
+ }
24
+
25
+ export function emptyLedger(): Ledger {
26
+ return { version: LEDGER_VERSION, handled: {} };
27
+ }
28
+
29
+ function isEntry(value: unknown): value is HandledIssue {
30
+ return (
31
+ isRecord(value) &&
32
+ isInteger(value.number) &&
33
+ typeof value.runId === "string" &&
34
+ typeof value.at === "string"
35
+ );
36
+ }
37
+
38
+ export function parseLedger(text: string): Ledger | undefined {
39
+ let parsed: unknown;
40
+ try {
41
+ parsed = JSON.parse(text);
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ if (!isRecord(parsed) || parsed.version !== LEDGER_VERSION) return undefined;
46
+ if (!isRecord(parsed.handled)) return undefined;
47
+ const handled: Record<string, HandledIssue> = {};
48
+ for (const [key, value] of Object.entries(parsed.handled)) {
49
+ if (!isEntry(value)) return undefined;
50
+ handled[key] = value;
51
+ }
52
+ return { version: LEDGER_VERSION, handled };
53
+ }
54
+
55
+ /** A missing or unusable ledger reads as empty. */
56
+ export function readLedger(stateDir: string, repoDir: string): Ledger {
57
+ try {
58
+ return parseLedger(readFileSync(ledgerPath(stateDir, repoDir), "utf8")) ?? emptyLedger();
59
+ } catch {
60
+ return emptyLedger();
61
+ }
62
+ }
63
+
64
+ /** A damaged ledger is renamed aside, preserved, and read as empty. */
65
+ export function loadLedger(stateDir: string, repoDir: string): Ledger {
66
+ const path = ledgerPath(stateDir, repoDir);
67
+ if (!existsSync(path)) return emptyLedger();
68
+ let text: string;
69
+ try {
70
+ text = readFileSync(path, "utf8");
71
+ } catch {
72
+ return emptyLedger();
73
+ }
74
+ const ledger = parseLedger(text);
75
+ if (ledger !== undefined) return ledger;
76
+ try {
77
+ renameSync(path, `${path}.invalid-${new Date().toISOString().replace(/[:.]/g, "-")}`);
78
+ } catch {
79
+ // The next successful run writes a fresh ledger anyway.
80
+ }
81
+ return emptyLedger();
82
+ }
83
+
84
+ export function writeLedger(stateDir: string, repoDir: string, ledger: Ledger): void {
85
+ const path = ledgerPath(stateDir, repoDir);
86
+ mkdirSync(dirname(path), { recursive: true });
87
+ const temporary = `${path}.${process.pid}.tmp`;
88
+ writeFileSync(temporary, `${JSON.stringify(ledger, null, 2)}\n`);
89
+ renameSync(temporary, path);
90
+ }
91
+
92
+ export function handledNumbers(ledger: Ledger): Set<number> {
93
+ return new Set(Object.values(ledger.handled).map((entry) => entry.number));
94
+ }
95
+
96
+ export function recordHandled(
97
+ ledger: Ledger,
98
+ issues: Array<{ number: number }>,
99
+ runId: string,
100
+ now: Date,
101
+ ): Ledger {
102
+ const handled = { ...ledger.handled };
103
+ const at = now.toISOString();
104
+ for (const issue of issues) {
105
+ handled[String(issue.number)] = { number: issue.number, runId, at };
106
+ }
107
+ return { version: LEDGER_VERSION, handled };
108
+ }
109
+
110
+ /** Drops entries whose issue is no longer open. */
111
+ export function pruneLedger(ledger: Ledger, open: Set<number>): Ledger {
112
+ const handled: Record<string, HandledIssue> = {};
113
+ let changed = false;
114
+ for (const [key, entry] of Object.entries(ledger.handled)) {
115
+ if (open.has(entry.number)) {
116
+ handled[key] = entry;
117
+ continue;
118
+ }
119
+ changed = true;
120
+ }
121
+ return changed ? { version: LEDGER_VERSION, handled } : ledger;
122
+ }
@@ -0,0 +1,68 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { isRecord } from "./guards";
4
+ import { parseLedger, type HandledIssue } from "./ledger";
5
+ import { pidAlive, readLock, type LockOwner } from "./lock";
6
+ import { legacyLedgerPath, legacyLockPath, legacyStatePath, ledgerPath } from "./paths";
7
+
8
+ /**
9
+ * The removed systemd orchestrator left three facts worth reading exactly
10
+ * once: its handled issue numbers, its state's root, and a live lock. Nothing
11
+ * here starts, stops, or edits that process; the operator owns its cleanup.
12
+ */
13
+
14
+ export function legacyLiveLock(stateDir: string): LockOwner | undefined {
15
+ const path = legacyLockPath(stateDir);
16
+ const owner = readLock(path);
17
+ if (owner === undefined || !pidAlive(owner.pid)) return undefined;
18
+ return owner;
19
+ }
20
+
21
+ export function legacyLockRefusal(owner: LockOwner, stateDir: string): string {
22
+ return (
23
+ `the legacy devboard-orchestrator still holds ${legacyLockPath(stateDir)} (pid ${owner.pid}). ` +
24
+ "Stop it with 'systemctl --user stop devboard-orchestrator.service', disable it with " +
25
+ "'systemctl --user disable devboard-orchestrator.service', remove " +
26
+ "'$HOME/.config/systemd/user/devboard-orchestrator.service', then run " +
27
+ "'systemctl --user daemon-reload'."
28
+ );
29
+ }
30
+
31
+ function legacyRootMatches(stateDir: string, repoDir: string): boolean {
32
+ try {
33
+ const parsed: unknown = JSON.parse(readFileSync(legacyStatePath(stateDir), "utf8"));
34
+ if (!isRecord(parsed) || typeof parsed.root !== "string") return false;
35
+ return resolve(parsed.root) === resolve(repoDir);
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Copies the legacy handled numbers into the repository-keyed ledger, but only
43
+ * when that ledger does not exist yet, the legacy state parses, and its root
44
+ * names this checkout. Missing, damaged, or mismatched legacy files are
45
+ * ignored; the legacy bytes are never rewritten.
46
+ */
47
+ export function migrateLegacyLedger(options: {
48
+ stateDir: string;
49
+ repoDir: string;
50
+ now?: Date;
51
+ }): HandledIssue[] | undefined {
52
+ if (existsSync(ledgerPath(options.stateDir, options.repoDir))) return undefined;
53
+ if (!legacyRootMatches(options.stateDir, options.repoDir)) return undefined;
54
+ let text: string;
55
+ try {
56
+ text = readFileSync(legacyLedgerPath(options.stateDir), "utf8");
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ const ledger = parseLedger(text);
61
+ if (ledger === undefined) return undefined;
62
+ const at = (options.now ?? new Date()).toISOString();
63
+ return Object.values(ledger.handled).map((entry) => ({
64
+ number: entry.number,
65
+ runId: entry.runId,
66
+ at: entry.at.length > 0 ? entry.at : at,
67
+ }));
68
+ }