@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
@@ -0,0 +1,584 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { isInteger, isRecord, nullableString } from "./guards";
4
+ import {
5
+ ACTIVITY_MAX_BYTES,
6
+ ACTIVITY_MAX_ENTRIES,
7
+ ACTIVITY_TRIMMED_MARKER,
8
+ STATE_VERSION,
9
+ runsDirectory,
10
+ } from "./paths";
11
+
12
+ /**
13
+ * The issues worker's published state. It is the whole contract between the
14
+ * detached worker and whatever dashboard is attached: the worker writes it
15
+ * atomically before publishing anything, and a dashboard only ever reads it.
16
+ * A file this module cannot understand is renamed aside, never deleted.
17
+ */
18
+
19
+ export type IssuesLifecycle = "stopped" | "idle" | "running" | "pausing" | "paused" | "failed";
20
+ export type IssuesRunStatus = "running" | "succeeded" | "failed" | "stopped";
21
+
22
+ export interface IssuesIssue {
23
+ number: number;
24
+ title: string;
25
+ url: string;
26
+ createdAt: string;
27
+ labels: string[];
28
+ }
29
+
30
+ export interface QueuedIssue extends IssuesIssue {
31
+ handled: boolean;
32
+ /** The run that last handled this issue, or null before it is handled. */
33
+ runId: string | null;
34
+ /** The run of the agent currently working this issue, or null. */
35
+ agentRunId: string | null;
36
+ }
37
+
38
+ export interface IssuesRunSummary {
39
+ runId: string;
40
+ status: IssuesRunStatus;
41
+ startedAt: string;
42
+ finishedAt: string | null;
43
+ exitCode: number | null;
44
+ issueNumbers: number[];
45
+ activity: number;
46
+ firstStderrLine: string | null;
47
+ }
48
+
49
+ export interface IssuesRunDetail extends IssuesRunSummary {
50
+ repo: string;
51
+ argv: string[];
52
+ cwd: string;
53
+ issues: IssuesIssue[];
54
+ workerPid: number;
55
+ harnessVersion: string | null;
56
+ activityLines: string[];
57
+ }
58
+
59
+ export interface IssuesPollState {
60
+ intervalMs: number;
61
+ lastPollAt: string | null;
62
+ nextPollAt: string | null;
63
+ etag: string | null;
64
+ notModified: boolean;
65
+ rateLimitedUntil: string | null;
66
+ error: string | null;
67
+ }
68
+
69
+ export interface IssuesWorkerRecord {
70
+ pid: number;
71
+ startedAt: string;
72
+ stateDir: string;
73
+ harnessVersion: string | null;
74
+ argv: string[];
75
+ }
76
+
77
+ export interface IssuesAcpChild {
78
+ pid: number;
79
+ argv: string[];
80
+ cwd: string;
81
+ }
82
+
83
+ /**
84
+ * One running issue's own peer. Each agent has a run record, activity, and
85
+ * pending question of its own; `runId` is what routes an answer.
86
+ */
87
+ export interface IssuesAgent {
88
+ runId: string;
89
+ issueNumber: number;
90
+ startedAt: string;
91
+ question: IssuesQuestion | null;
92
+ acp: IssuesAcpChild | null;
93
+ }
94
+
95
+ export interface IssuesQuestion {
96
+ id: number | string;
97
+ message: string;
98
+ options: Array<{ label: string; description?: string; recommended: boolean }>;
99
+ }
100
+
101
+ export interface IssuesState {
102
+ version: 2;
103
+ repo: string;
104
+ root: string;
105
+ lifecycle: IssuesLifecycle;
106
+ enabled: boolean;
107
+ paused: boolean;
108
+ worker: IssuesWorkerRecord | null;
109
+ poll: IssuesPollState;
110
+ queue: QueuedIssue[];
111
+ runs: IssuesRunSummary[];
112
+ /** One entry per concurrently running issue peer. */
113
+ agents: IssuesAgent[];
114
+ /** Per-issue retry gate for a failed agent, keyed by issue number. */
115
+ cooldowns: Record<string, string>;
116
+ lastError: string | null;
117
+ lastExitCode: number | null;
118
+ finishedAt: string | null;
119
+ notice: string | null;
120
+ updatedAt: string;
121
+ }
122
+
123
+
124
+ function isStringArray(value: unknown): value is string[] {
125
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
126
+ }
127
+
128
+ /** Labels are optional in files written before label-aware polling. */
129
+ function isIssue(value: unknown): value is IssuesIssue {
130
+ return (
131
+ isRecord(value) &&
132
+ isInteger(value.number) &&
133
+ typeof value.title === "string" &&
134
+ typeof value.url === "string" &&
135
+ typeof value.createdAt === "string" &&
136
+ (value.labels === undefined || isStringArray(value.labels))
137
+ );
138
+ }
139
+
140
+ export function issueLabels(value: unknown): string[] {
141
+ return isRecord(value) && isStringArray(value.labels) ? [...value.labels] : [];
142
+ }
143
+
144
+ function normalizedIssue(value: IssuesIssue): IssuesIssue {
145
+ return { ...value, labels: issueLabels(value) };
146
+ }
147
+
148
+ /** A queue entry from either document version, with `agentRunId` explicit. */
149
+ function normalizedQueued(value: QueuedIssue): QueuedIssue {
150
+ return {
151
+ ...normalizedIssue(value),
152
+ handled: value.handled,
153
+ runId: value.runId,
154
+ agentRunId: value.agentRunId ?? null,
155
+ };
156
+ }
157
+
158
+ function isQueued(value: unknown): value is QueuedIssue {
159
+ return (
160
+ isRecord(value) &&
161
+ typeof value.handled === "boolean" &&
162
+ nullableString(value.runId) &&
163
+ isIssue(value)
164
+ );
165
+ }
166
+
167
+ function isRunSummary(value: unknown): value is IssuesRunSummary {
168
+ if (!isRecord(value)) return false;
169
+ return (
170
+ typeof value.runId === "string" &&
171
+ (value.status === "running" ||
172
+ value.status === "succeeded" ||
173
+ value.status === "failed" ||
174
+ value.status === "stopped") &&
175
+ typeof value.startedAt === "string" &&
176
+ nullableString(value.finishedAt) &&
177
+ (value.exitCode === null || isInteger(value.exitCode)) &&
178
+ Array.isArray(value.issueNumbers) &&
179
+ value.issueNumbers.every(isInteger) &&
180
+ isInteger(value.activity) &&
181
+ nullableString(value.firstStderrLine)
182
+ );
183
+ }
184
+
185
+ function isQuestion(value: unknown): value is IssuesQuestion {
186
+ if (!isRecord(value)) return false;
187
+ if (typeof value.id !== "string" && !isInteger(value.id)) return false;
188
+ if (typeof value.message !== "string") return false;
189
+ return (
190
+ Array.isArray(value.options) &&
191
+ value.options.every(
192
+ (option) =>
193
+ isRecord(option) &&
194
+ typeof option.label === "string" &&
195
+ (option.description === undefined || typeof option.description === "string") &&
196
+ typeof option.recommended === "boolean",
197
+ )
198
+ );
199
+ }
200
+
201
+ function isWorker(value: unknown): value is IssuesWorkerRecord {
202
+ return (
203
+ isRecord(value) &&
204
+ isInteger(value.pid) &&
205
+ typeof value.startedAt === "string" &&
206
+ typeof value.stateDir === "string" &&
207
+ nullableString(value.harnessVersion) &&
208
+ Array.isArray(value.argv) &&
209
+ value.argv.every((entry) => typeof entry === "string")
210
+ );
211
+ }
212
+
213
+ function isAcp(value: unknown): value is IssuesAcpChild {
214
+ return (
215
+ isRecord(value) &&
216
+ isInteger(value.pid) &&
217
+ Array.isArray(value.argv) &&
218
+ value.argv.every((entry) => typeof entry === "string") &&
219
+ typeof value.cwd === "string"
220
+ );
221
+ }
222
+
223
+ function isAgent(value: unknown): value is IssuesAgent {
224
+ return (
225
+ isRecord(value) &&
226
+ typeof value.runId === "string" &&
227
+ isInteger(value.issueNumber) &&
228
+ typeof value.startedAt === "string" &&
229
+ (value.question === null || isQuestion(value.question)) &&
230
+ (value.acp === null || isAcp(value.acp))
231
+ );
232
+ }
233
+
234
+ function isPoll(value: unknown): value is IssuesPollState {
235
+ return (
236
+ isRecord(value) &&
237
+ isInteger(value.intervalMs) &&
238
+ nullableString(value.lastPollAt) &&
239
+ nullableString(value.nextPollAt) &&
240
+ nullableString(value.etag) &&
241
+ typeof value.notModified === "boolean" &&
242
+ nullableString(value.rateLimitedUntil) &&
243
+ nullableString(value.error)
244
+ );
245
+ }
246
+
247
+ export function emptyPoll(intervalMs: number): IssuesPollState {
248
+ return {
249
+ intervalMs,
250
+ lastPollAt: null,
251
+ nextPollAt: null,
252
+ etag: null,
253
+ notModified: false,
254
+ rateLimitedUntil: null,
255
+ error: null,
256
+ };
257
+ }
258
+
259
+ export function emptyState(options: {
260
+ repo: string;
261
+ root: string;
262
+ lifecycle?: IssuesLifecycle;
263
+ intervalMs?: number;
264
+ now?: Date;
265
+ }): IssuesState {
266
+ return {
267
+ version: STATE_VERSION,
268
+ repo: options.repo,
269
+ root: options.root,
270
+ lifecycle: options.lifecycle ?? "stopped",
271
+ enabled: false,
272
+ paused: false,
273
+ worker: null,
274
+ poll: emptyPoll(options.intervalMs ?? 10_000),
275
+ queue: [],
276
+ runs: [],
277
+ agents: [],
278
+ cooldowns: {},
279
+ lastError: null,
280
+ lastExitCode: null,
281
+ finishedAt: null,
282
+ notice: null,
283
+ updatedAt: (options.now ?? new Date()).toISOString(),
284
+ };
285
+ }
286
+
287
+ /** Parses one document; `undefined` means the bytes are not this version. */
288
+ export function parseState(text: string): IssuesState | undefined {
289
+ let parsed: unknown;
290
+ try {
291
+ parsed = JSON.parse(text);
292
+ } catch {
293
+ return undefined;
294
+ }
295
+ if (!isRecord(parsed)) return undefined;
296
+ const version = parsed.version;
297
+ if (version !== 1 && version !== STATE_VERSION) return undefined;
298
+ if (typeof parsed.repo !== "string" || typeof parsed.root !== "string") return undefined;
299
+ const lifecycle = parsed.lifecycle;
300
+ if (
301
+ lifecycle !== "stopped" &&
302
+ lifecycle !== "idle" &&
303
+ lifecycle !== "running" &&
304
+ lifecycle !== "pausing" &&
305
+ lifecycle !== "paused" &&
306
+ lifecycle !== "failed"
307
+ ) {
308
+ return undefined;
309
+ }
310
+ if (typeof parsed.enabled !== "boolean" || typeof parsed.paused !== "boolean") return undefined;
311
+ if (parsed.worker !== null && !isWorker(parsed.worker)) return undefined;
312
+ if (!isPoll(parsed.poll)) return undefined;
313
+ if (!Array.isArray(parsed.queue) || !parsed.queue.every(isQueued)) return undefined;
314
+ if (!Array.isArray(parsed.runs) || !parsed.runs.every(isRunSummary)) return undefined;
315
+
316
+ // Version 1 carried a single implement-all run and its question. It still
317
+ // loads: the fleet starts empty and the per-issue cooldowns carry over.
318
+ const previous = version === 1;
319
+ if (!previous) {
320
+ if (!Array.isArray(parsed.agents) || !parsed.agents.every(isAgent)) return undefined;
321
+ if (
322
+ !isRecord(parsed.cooldowns) ||
323
+ !Object.values(parsed.cooldowns).every((entry) => typeof entry === "string")
324
+ ) {
325
+ return undefined;
326
+ }
327
+ } else if (
328
+ parsed.goalCooldowns !== undefined &&
329
+ (!isRecord(parsed.goalCooldowns) ||
330
+ !Object.values(parsed.goalCooldowns).every((entry) => typeof entry === "string"))
331
+ ) {
332
+ return undefined;
333
+ }
334
+ if (!nullableString(parsed.lastError)) return undefined;
335
+ if (parsed.lastExitCode !== null && !isInteger(parsed.lastExitCode)) return undefined;
336
+ if (!nullableString(parsed.finishedAt)) return undefined;
337
+ if (!nullableString(parsed.notice)) return undefined;
338
+ if (typeof parsed.updatedAt !== "string") return undefined;
339
+
340
+ const cooldowns = previous
341
+ ? ((parsed.goalCooldowns as Record<string, string> | undefined) ?? {})
342
+ : (parsed.cooldowns as Record<string, string>);
343
+ return {
344
+ version: STATE_VERSION,
345
+ repo: parsed.repo,
346
+ root: parsed.root,
347
+ lifecycle,
348
+ enabled: parsed.enabled,
349
+ paused: parsed.paused,
350
+ worker: parsed.worker as IssuesWorkerRecord | null,
351
+ poll: parsed.poll,
352
+ queue: parsed.queue.map((entry) => normalizedQueued(entry)),
353
+ runs: parsed.runs,
354
+ agents: previous ? [] : (parsed.agents as IssuesAgent[]),
355
+ cooldowns: { ...cooldowns },
356
+ lastError: parsed.lastError,
357
+ lastExitCode: parsed.lastExitCode,
358
+ finishedAt: parsed.finishedAt,
359
+ notice: parsed.notice,
360
+ updatedAt: parsed.updatedAt,
361
+ };
362
+ }
363
+
364
+ /** Reads without side effects; the dashboard uses this. */
365
+ export function readState(path: string): IssuesState | undefined {
366
+ try {
367
+ return parseState(readFileSync(path, "utf8"));
368
+ } catch {
369
+ return undefined;
370
+ }
371
+ }
372
+
373
+ /** Reads and moves an unusable file aside, preserved. */
374
+ export function loadState(path: string): IssuesState | undefined {
375
+ if (!existsSync(path)) return undefined;
376
+ let text: string;
377
+ try {
378
+ text = readFileSync(path, "utf8");
379
+ } catch {
380
+ return undefined;
381
+ }
382
+ const state = parseState(text);
383
+ if (state !== undefined) return state;
384
+ quarantine(path);
385
+ return undefined;
386
+ }
387
+
388
+ export function quarantine(path: string): string | undefined {
389
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
390
+ for (let attempt = 0; attempt < 10; attempt += 1) {
391
+ const target = `${path}.invalid-${stamp}${attempt === 0 ? "" : `-${attempt}`}`;
392
+ if (existsSync(target)) continue;
393
+ try {
394
+ renameSync(path, target);
395
+ return target;
396
+ } catch {
397
+ return undefined;
398
+ }
399
+ }
400
+ return undefined;
401
+ }
402
+
403
+ /** Temporary file plus rename: no reader ever sees a prefix of a state. */
404
+ export function writeState(path: string, state: IssuesState): void {
405
+ mkdirSync(dirname(path), { recursive: true });
406
+ const temporary = `${path}.${process.pid}.tmp`;
407
+ writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`);
408
+ renameSync(temporary, path);
409
+ }
410
+
411
+ /* ------------------------------------------------------------------ */
412
+ /* Run records and their activity */
413
+ /* ------------------------------------------------------------------ */
414
+
415
+ export interface RunFile {
416
+ version: 1;
417
+ runId: string;
418
+ repo: string;
419
+ status: IssuesRunStatus;
420
+ argv: string[];
421
+ cwd: string;
422
+ startedAt: string;
423
+ finishedAt: string | null;
424
+ exitCode: number | null;
425
+ issues: IssuesIssue[];
426
+ firstStderrLine: string | null;
427
+ workerPid: number;
428
+ harnessVersion: string | null;
429
+ }
430
+
431
+ export function parseRunFile(text: string): RunFile | undefined {
432
+ let parsed: unknown;
433
+ try {
434
+ parsed = JSON.parse(text);
435
+ } catch {
436
+ return undefined;
437
+ }
438
+ if (!isRecord(parsed) || parsed.version !== 1) return undefined;
439
+ if (typeof parsed.runId !== "string" || typeof parsed.repo !== "string") return undefined;
440
+ if (
441
+ parsed.status !== "running" &&
442
+ parsed.status !== "succeeded" &&
443
+ parsed.status !== "failed" &&
444
+ parsed.status !== "stopped"
445
+ ) {
446
+ return undefined;
447
+ }
448
+ if (!Array.isArray(parsed.argv) || !parsed.argv.every((entry) => typeof entry === "string")) {
449
+ return undefined;
450
+ }
451
+ if (typeof parsed.cwd !== "string" || typeof parsed.startedAt !== "string") return undefined;
452
+ if (!nullableString(parsed.finishedAt)) return undefined;
453
+ if (parsed.exitCode !== null && !isInteger(parsed.exitCode)) return undefined;
454
+ if (!Array.isArray(parsed.issues) || !parsed.issues.every(isIssue)) return undefined;
455
+ if (!nullableString(parsed.firstStderrLine)) return undefined;
456
+ if (!isInteger(parsed.workerPid)) return undefined;
457
+ if (!nullableString(parsed.harnessVersion)) return undefined;
458
+ return {
459
+ version: 1,
460
+ runId: parsed.runId,
461
+ repo: parsed.repo,
462
+ status: parsed.status,
463
+ argv: parsed.argv,
464
+ cwd: parsed.cwd,
465
+ startedAt: parsed.startedAt,
466
+ finishedAt: parsed.finishedAt,
467
+ exitCode: parsed.exitCode,
468
+ issues: parsed.issues.map(normalizedIssue),
469
+ firstStderrLine: parsed.firstStderrLine,
470
+ workerPid: parsed.workerPid,
471
+ harnessVersion: parsed.harnessVersion,
472
+ };
473
+ }
474
+
475
+ export function runFilePath(directory: string): string {
476
+ return join(directory, "run.json");
477
+ }
478
+
479
+ export function activityPath(directory: string): string {
480
+ return join(directory, "activity.json");
481
+ }
482
+
483
+ export function writeRunFile(directory: string, run: RunFile): void {
484
+ mkdirSync(directory, { recursive: true });
485
+ const path = runFilePath(directory);
486
+ const temporary = `${path}.${process.pid}.tmp`;
487
+ writeFileSync(temporary, `${JSON.stringify(run, null, 2)}\n`);
488
+ renameSync(temporary, path);
489
+ }
490
+
491
+ export function parseActivity(text: string): string[] | undefined {
492
+ let parsed: unknown;
493
+ try {
494
+ parsed = JSON.parse(text);
495
+ } catch {
496
+ return undefined;
497
+ }
498
+ if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) {
499
+ return undefined;
500
+ }
501
+ return parsed;
502
+ }
503
+
504
+ export function readActivity(directory: string): string[] {
505
+ try {
506
+ return parseActivity(readFileSync(activityPath(directory), "utf8")) ?? [];
507
+ } catch {
508
+ return [];
509
+ }
510
+ }
511
+
512
+ /** Atomic, so a dashboard never reads half a line. */
513
+ export function writeActivity(directory: string, lines: string[]): void {
514
+ mkdirSync(directory, { recursive: true });
515
+ const path = activityPath(directory);
516
+ const temporary = `${path}.${process.pid}.tmp`;
517
+ writeFileSync(temporary, `${JSON.stringify(lines)}\n`);
518
+ renameSync(temporary, path);
519
+ }
520
+
521
+ /** Per-run bound: the first of 1,000 entries or 1 MiB, with a marker. */
522
+ export function appendActivityLine(lines: string[], text: string, append: boolean): void {
523
+ if (append && lines.length > 0) lines[lines.length - 1] += text;
524
+ else lines.push(text);
525
+ trimActivityLines(lines);
526
+ }
527
+
528
+ export function trimActivityLines(lines: string[]): void {
529
+ const bytesOf = (line: string): number => Buffer.byteLength(line, "utf8");
530
+ let bytes = 0;
531
+ for (const line of lines) bytes += bytesOf(line);
532
+ if (lines.length <= ACTIVITY_MAX_ENTRIES && bytes <= ACTIVITY_MAX_BYTES) return;
533
+
534
+ const markerBytes = bytesOf(ACTIVITY_TRIMMED_MARKER);
535
+ const kept: string[] = [];
536
+ let keptBytes = markerBytes;
537
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
538
+ const line = lines[index];
539
+ if (line === undefined) continue;
540
+ const size = bytesOf(line);
541
+ const entries = kept.length + 2;
542
+ if (kept.length > 0 && (entries > ACTIVITY_MAX_ENTRIES || keptBytes + size > ACTIVITY_MAX_BYTES)) {
543
+ break;
544
+ }
545
+ kept.unshift(line);
546
+ keptBytes += size;
547
+ }
548
+ lines.splice(0, lines.length, ACTIVITY_TRIMMED_MARKER, ...kept);
549
+ }
550
+
551
+ export function runSummary(run: RunFile, activity: number): IssuesRunSummary {
552
+ return {
553
+ runId: run.runId,
554
+ status: run.status,
555
+ startedAt: run.startedAt,
556
+ finishedAt: run.finishedAt,
557
+ exitCode: run.exitCode,
558
+ issueNumbers: run.issues.map((issue) => issue.number),
559
+ activity,
560
+ firstStderrLine: run.firstStderrLine,
561
+ };
562
+ }
563
+
564
+ export function runDetail(run: RunFile, activityLines: string[]): IssuesRunDetail {
565
+ return {
566
+ ...runSummary(run, activityLines.length),
567
+ repo: run.repo,
568
+ argv: run.argv,
569
+ cwd: run.cwd,
570
+ issues: run.issues,
571
+ workerPid: run.workerPid,
572
+ harnessVersion: run.harnessVersion,
573
+ activityLines,
574
+ };
575
+ }
576
+
577
+ /** Newest-first run directories; the run id is a UTC timestamp name. */
578
+ export function listRunDirectories(stateDir: string, repoDir: string): string[] {
579
+ try {
580
+ return readdirSync(runsDirectory(stateDir, repoDir)).sort().reverse();
581
+ } catch {
582
+ return [];
583
+ }
584
+ }