@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,412 @@
1
+ import { connection } from "./connection.svelte";
2
+ import { dependencyName } from "../../lib/dependencies.ts";
3
+ import { notifications } from "./notifications.svelte";
4
+ import {
5
+ commandAlias,
6
+ conceptSignificance,
7
+ conceptUsage,
8
+ filterContexts,
9
+ glossaryHasSignificance,
10
+ scalarText,
11
+ sectionLeaf,
12
+ SIGNIFICANCE_FLOOR,
13
+ } from "./discover-format";
14
+ import type {
15
+ ConceptSignificance,
16
+ ConceptUsage,
17
+ DiscoverSkill,
18
+ DiscoverStatePayload,
19
+ DiscoverTarget,
20
+ Freshness,
21
+ Glossary,
22
+ GlossarySignificance,
23
+ GlossaryUsage,
24
+ RetainedRemoteProject,
25
+ RunQuestion,
26
+ RunQuestionOption,
27
+ } from "./types";
28
+
29
+ type Message = Record<string, unknown>;
30
+
31
+ function isObject(value: unknown): value is Record<string, unknown> {
32
+ return typeof value === "object" && value !== null;
33
+ }
34
+
35
+ function appendLine(lines: string[], text: string, append: boolean): string[] {
36
+ if (append && lines.length > 0) {
37
+ return [...lines.slice(0, -1), (lines[lines.length - 1] ?? "") + text];
38
+ }
39
+ return [...lines, text];
40
+ }
41
+
42
+ export class DiscoverStore {
43
+ state = $state<DiscoverStatePayload | null>(null);
44
+ active = $state(false);
45
+ runId = $state<string | null>(null);
46
+ startedAt = $state<string | null>(null);
47
+ activity = $state<string[]>([]);
48
+ question = $state<RunQuestion | null>(null);
49
+ error = $state<string | null>(null);
50
+ warnings = $state<string[]>([]);
51
+ interrupted = $state(false);
52
+ transition = $state<string | null>(null);
53
+ answerDraft = $state("");
54
+ glossarySearch = $state("");
55
+ /** The URL field of the source-checkout-only remote target strip. */
56
+ remoteUrl = $state("");
57
+ /** Significance floor a listed term needs to stay listed. */
58
+ glossaryThreshold = $state(SIGNIFICANCE_FLOOR);
59
+ /** Hide scored terms below the floor; on by default. */
60
+ glossarySignificantOnly = $state(true);
61
+
62
+ drafts = $state<Record<string, string>>({});
63
+ dirty = $state<Record<string, boolean>>({});
64
+
65
+ get remoteAvailable(): boolean {
66
+ return this.state?.remoteAvailable === true;
67
+ }
68
+
69
+ get target(): DiscoverTarget {
70
+ return this.state?.target ?? { kind: "host" };
71
+ }
72
+
73
+ /** The repository identity the head shows: `owner/name` when it is known. */
74
+ get repository(): string | null {
75
+ if (typeof this.state?.repository === "string" && this.state.repository.length > 0) {
76
+ return this.state.repository;
77
+ }
78
+ const target = this.target;
79
+ return target.kind === "remote" ? `${target.owner}/${target.repo}` : null;
80
+ }
81
+
82
+ get remoteTarget(): Extract<DiscoverTarget, { kind: "remote" }> | null {
83
+ const target = this.target;
84
+ return target.kind === "remote" ? target : null;
85
+ }
86
+
87
+ /** Every retained remote project, newest completed discovery first. */
88
+ get retained(): RetainedRemoteProject[] {
89
+ return Array.isArray(this.state?.retained) ? this.state.retained : [];
90
+ }
91
+
92
+ /** True while the remote target cannot be changed without a refusal. */
93
+ get remoteChangeDisabled(): boolean {
94
+ return this.remoteChangeTitle !== undefined;
95
+ }
96
+
97
+ get remoteChangeTitle(): string | undefined {
98
+ if (!connection.open) return "Not connected";
99
+ if (this.cloning !== null) return "A repository clone is running";
100
+ if (this.active) return "A Discover run is active";
101
+ return this.busy ?? undefined;
102
+ }
103
+
104
+ get cloning(): { url: string } | null {
105
+ return this.state?.cloning ?? null;
106
+ }
107
+
108
+ /** The remote start control's disabled state; its title names the reason. */
109
+ get remoteStartDisabled(): boolean {
110
+ return this.remoteStartTitle !== undefined;
111
+ }
112
+
113
+ get remoteStartTitle(): string | undefined {
114
+ if (!connection.open) return "Not connected";
115
+ if (this.cloning !== null) return "A repository clone is running";
116
+ if (this.active) return "A Discover run is active";
117
+ if (this.busy !== null) return this.busy;
118
+ if (this.remoteUrl.trim().length === 0) return "Enter a GitHub repository URL";
119
+ return undefined;
120
+ }
121
+
122
+ get busy(): string | null {
123
+ return this.state?.busy ?? null;
124
+ }
125
+
126
+ get stale(): boolean {
127
+ return this.state?.stale === true;
128
+ }
129
+
130
+ get freshness(): Freshness | null {
131
+ return this.state?.freshness ?? null;
132
+ }
133
+
134
+ get sheet(): Record<string, unknown> | null {
135
+ return this.state?.sheet ?? null;
136
+ }
137
+
138
+ get repositories(): Record<string, string> {
139
+ const value = this.state?.dependencyRepositories;
140
+ return typeof value === "object" && value !== null ? value : {};
141
+ }
142
+
143
+ /** The repository URL an installed package behind this entry declares. */
144
+ repositoryLink(line: string): string | null {
145
+ const name = dependencyName(line);
146
+ if (name.length === 0) return null;
147
+ return this.repositories[name] ?? this.repositories[name.toLowerCase()] ?? null;
148
+ }
149
+
150
+ get hasArtifacts(): boolean {
151
+ return this.sheet !== null;
152
+ }
153
+
154
+ get displayError(): string | null {
155
+ return this.error ?? this.state?.error ?? null;
156
+ }
157
+
158
+ get displayedWarnings(): string[] {
159
+ return this.warnings.length > 0 ? this.warnings : (this.state?.warnings ?? []);
160
+ }
161
+
162
+ get contexts(): Glossary[] {
163
+ return Array.isArray(this.state?.glossaries) ? this.state.glossaries : [];
164
+ }
165
+
166
+ get glossaryUsage(): GlossaryUsage {
167
+ const value = this.state?.glossaryUsage;
168
+ return typeof value === "object" && value !== null ? value : {};
169
+ }
170
+
171
+ get glossarySignificance(): GlossarySignificance {
172
+ const value = this.state?.glossarySignificance;
173
+ return typeof value === "object" && value !== null ? value : {};
174
+ }
175
+
176
+ /** True when the published glossary carries at least one crawl score. */
177
+ get glossaryHasScores(): boolean {
178
+ return glossaryHasSignificance(this.contexts, this.glossarySignificance);
179
+ }
180
+
181
+ usageFor(context: string, id: string): ConceptUsage {
182
+ return conceptUsage(this.glossaryUsage, context, id);
183
+ }
184
+
185
+ significanceFor(context: string, id: string): ConceptSignificance | null {
186
+ return conceptSignificance(this.glossarySignificance, context, id) ?? null;
187
+ }
188
+
189
+ get filteredContexts(): Glossary[] {
190
+ return filterContexts(this.contexts, {
191
+ search: this.glossarySearch,
192
+ significantOnly: this.glossarySignificantOnly,
193
+ floor: this.glossaryThreshold,
194
+ significance: this.glossarySignificance,
195
+ });
196
+ }
197
+
198
+ /** The invocation that runs the whole suite, preferring the manifest's own alias. */
199
+ get testCommand(): string {
200
+ const leaf = sectionLeaf(this.sheet?.commands, "test");
201
+ if (leaf === undefined || leaf.confidence === "unknown") return "";
202
+ return this.aliasFor(scalarText(leaf.value));
203
+ }
204
+
205
+ /** The command to show for a recorded command: its declared alias when one exists. */
206
+ aliasFor(command: string): string {
207
+ return commandAlias(this.state?.commandAliases, command);
208
+ }
209
+
210
+ get startDisabled(): boolean {
211
+ return !connection.open || this.active || this.busy !== null;
212
+ }
213
+
214
+ get startTitle(): string | undefined {
215
+ if (!connection.open) return "Not connected";
216
+ if (this.active) return "A Discover run is active";
217
+ return this.busy ?? undefined;
218
+ }
219
+
220
+ get skills(): DiscoverSkill[] {
221
+ return Array.isArray(this.state?.skills) ? this.state.skills : [];
222
+ }
223
+
224
+ skill(name: string): DiscoverSkill | null {
225
+ return this.skills.find((skill) => skill.name === name) ?? null;
226
+ }
227
+
228
+ skillDraft(name: string): string {
229
+ const stored = this.drafts[name];
230
+ if (stored !== undefined) return stored;
231
+ const entry = this.skill(name);
232
+ return entry?.live ?? entry?.baseline ?? "";
233
+ }
234
+
235
+ skillDirty(name: string): boolean {
236
+ return this.dirty[name] === true;
237
+ }
238
+
239
+ start(): void {
240
+ connection.send({ type: "discoverProject" });
241
+ }
242
+
243
+ /** Sends exactly one `discoverRemote` for the trimmed URL. */
244
+ startRemote(): void {
245
+ const url = this.remoteUrl.trim();
246
+ if (url.length === 0 || this.remoteStartTitle !== undefined) return;
247
+ this.remoteUrl = "";
248
+ connection.send({ type: "discoverRemote", url });
249
+ }
250
+
251
+ /** Returns to this checkout's own artifacts; retained projects are kept. */
252
+ clearRemote(): void {
253
+ this.remoteUrl = "";
254
+ connection.send({ type: "clearDiscoverRemote" });
255
+ }
256
+
257
+ /** Switches to a retained project; never clones or crawls. */
258
+ switchRemote(url: string): void {
259
+ if (url.length === 0 || url === this.remoteTarget?.url) return;
260
+ connection.send({ type: "switchDiscoverRemote", url });
261
+ }
262
+
263
+ /** Deletes one retained project; the host target returns when it was active. */
264
+ removeRemote(url: string): void {
265
+ if (url.length === 0) return;
266
+ connection.send({ type: "removeDiscoverRemote", url });
267
+ }
268
+
269
+ stop(): void {
270
+ connection.send({ type: "stopDiscover" });
271
+ }
272
+
273
+ answer(value: string): void {
274
+ if (this.question === null) return;
275
+ const trimmed = value.trim();
276
+ if (trimmed.length === 0) return;
277
+ const id = this.question.id;
278
+ this.question = null;
279
+ this.answerDraft = "";
280
+ connection.send({ type: "answerDiscover", id, value: trimmed });
281
+ }
282
+
283
+ updateDraft(name: string, text: string): void {
284
+ this.drafts = { ...this.drafts, [name]: text };
285
+ const live = this.skill(name)?.live ?? this.skill(name)?.baseline ?? "";
286
+ this.dirty = { ...this.dirty, [name]: text !== live };
287
+ }
288
+
289
+ saveSkill(name: string): void {
290
+ connection.send({ type: "saveDiscoverSkill", name, text: this.skillDraft(name) });
291
+ }
292
+
293
+ restoreSkill(name: string): void {
294
+ const baseline = this.skill(name)?.baseline ?? null;
295
+ if (baseline !== null) {
296
+ this.drafts = { ...this.drafts, [name]: baseline };
297
+ this.dirty = { ...this.dirty, [name]: false };
298
+ }
299
+ connection.send({ type: "restoreDiscoverSkill", name });
300
+ }
301
+
302
+ discardDraft(name: string): void {
303
+ const entry = this.skill(name);
304
+ const live = entry?.live ?? entry?.baseline ?? "";
305
+ this.drafts = { ...this.drafts, [name]: live };
306
+ this.dirty = { ...this.dirty, [name]: false };
307
+ }
308
+
309
+ setGlossarySearch(value: string): void {
310
+ this.glossarySearch = value;
311
+ }
312
+
313
+ setGlossaryThreshold(value: number): void {
314
+ if (!Number.isFinite(value)) return;
315
+ this.glossaryThreshold = Math.min(100, Math.max(0, Math.round(value)));
316
+ }
317
+
318
+ setGlossarySignificantOnly(value: boolean): void {
319
+ this.glossarySignificantOnly = value;
320
+ }
321
+
322
+ /** Restores the default view: no search, the floor, and the filter on. */
323
+ resetGlossaryFilters(): void {
324
+ this.glossarySearch = "";
325
+ this.glossaryThreshold = SIGNIFICANCE_FLOOR;
326
+ this.glossarySignificantOnly = true;
327
+ }
328
+
329
+ private syncDrafts(payload: DiscoverStatePayload): void {
330
+ const skills = Array.isArray(payload.skills) ? payload.skills : [];
331
+ for (const skill of skills) {
332
+ if (typeof skill?.name !== "string") continue;
333
+ const live = skill.live ?? skill.baseline ?? "";
334
+ if (this.dirty[skill.name] !== true) {
335
+ this.drafts = { ...this.drafts, [skill.name]: live };
336
+ } else if (this.drafts[skill.name] === live) {
337
+ this.dirty = { ...this.dirty, [skill.name]: false };
338
+ }
339
+ }
340
+ }
341
+
342
+ private applySnapshot(payload: DiscoverStatePayload): void {
343
+ this.state = payload;
344
+ this.glossaryThreshold = Math.min(100, Math.max(0, Math.round(this.glossaryThreshold)));
345
+ this.active = payload.active === true;
346
+ this.runId = typeof payload.runId === "string" ? payload.runId : null;
347
+ this.startedAt = typeof payload.startedAt === "string" ? payload.startedAt : null;
348
+ this.warnings = Array.isArray(payload.warnings)
349
+ ? payload.warnings.filter((warning): warning is string => typeof warning === "string")
350
+ : [];
351
+ this.interrupted = payload.interrupted === true;
352
+ this.syncDrafts(payload);
353
+ }
354
+
355
+ handleEvent(message: Message): void {
356
+ const type = message.type;
357
+
358
+ if (type === "discoverState" && isObject(message.state)) {
359
+ this.applySnapshot(message.state as unknown as DiscoverStatePayload);
360
+ return;
361
+ }
362
+ if (type === "discoverStarted") {
363
+ this.active = true;
364
+ if (typeof message.runId === "string") this.runId = message.runId;
365
+ this.startedAt = typeof message.startedAt === "string" ? message.startedAt : null;
366
+ this.activity = [];
367
+ this.question = null;
368
+ this.answerDraft = "";
369
+ this.error = null;
370
+ this.transition = null;
371
+ return;
372
+ }
373
+ if (type === "discoverActivity" && typeof message.text === "string") {
374
+ this.activity = appendLine(this.activity, message.text, message.append === true);
375
+ return;
376
+ }
377
+ if (
378
+ type === "discoverQuestion" &&
379
+ (typeof message.id === "string" || typeof message.id === "number") &&
380
+ typeof message.message === "string" &&
381
+ Array.isArray(message.options)
382
+ ) {
383
+ this.question = {
384
+ id: message.id,
385
+ message: message.message,
386
+ options: message.options as RunQuestionOption[],
387
+ };
388
+ this.answerDraft = "";
389
+ return;
390
+ }
391
+ if (type === "discoverStopped") {
392
+ this.active = false;
393
+ this.question = null;
394
+ this.answerDraft = "";
395
+ if (message.completed === true) {
396
+ this.transition = null;
397
+ } else if (typeof message.reason === "string" && message.reason.length > 0) {
398
+ this.transition = `Discover run stopped: ${message.reason}`;
399
+ } else {
400
+ this.transition = "Discover run stopped.";
401
+ }
402
+ return;
403
+ }
404
+ if (type === "discoverError" && typeof message.message === "string") {
405
+ this.error = message.message;
406
+ notifications.push("error", message.message);
407
+ }
408
+ }
409
+ }
410
+
411
+ export const discover = new DiscoverStore();
412
+
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { draftProposal } from "./draft-format";
3
+
4
+ const DRAFT = `# Proposed goal: persist remote discoveries
5
+
6
+ Persist every remote project and offer a switcher.
7
+
8
+ ## Captured UI context
9
+
10
+ - Route: \`/\`
11
+ - URL: \`http://127.0.0.1:8081/\`
12
+ - Selected element: \`section.panel:nth-of-type(2)\`
13
+ - Viewport: \`2101x1017 @1.21875x\`
14
+ - Captured at: \`2026-10-28T10:30:03.635Z\`
15
+
16
+ ### Visible text
17
+
18
+ The remote clone is missing.
19
+
20
+ ### DOM excerpt
21
+
22
+ \`\`\`html
23
+ <section class="panel"></section>
24
+ \`\`\`
25
+ `;
26
+
27
+ describe("draft proposal", () => {
28
+ test("drops the captured context and the heading the panel shows", () => {
29
+ const proposal = draftProposal(DRAFT);
30
+ expect(proposal).toBe("Persist every remote project and offer a switcher.");
31
+ expect(proposal).not.toContain("Proposed goal:");
32
+ expect(proposal).not.toContain("Captured UI context");
33
+ expect(proposal).not.toContain("Route:");
34
+ expect(proposal).not.toContain("Unprocessed draft");
35
+ });
36
+
37
+ test("keeps a draft without the marker whole", () => {
38
+ expect(draftProposal("# Question draft\n\nAsk about the scope.\n")).toBe("Ask about the scope.");
39
+ });
40
+
41
+ test("returns nothing for a draft that is only captured context", () => {
42
+ expect(draftProposal("# Draft\n\n## Captured UI context\n\nNo page context was captured.\n")).toBe("");
43
+ });
44
+
45
+ test("returns nothing for an empty draft", () => {
46
+ expect(draftProposal("")).toBe("");
47
+ });
48
+ });
@@ -0,0 +1,13 @@
1
+ const CONTEXT_MARKER = "## Captured UI context";
2
+
3
+ /**
4
+ * Reads the proposal out of a draft. Both writers in `server/feedback.ts` and
5
+ * `server/goal-proposal.ts` put the captured evidence after this one marker;
6
+ * the proposal keeps the writer's own text and loses the heading the panel
7
+ * already shows.
8
+ */
9
+ export function draftProposal(text: string): string {
10
+ const index = text.indexOf(CONTEXT_MARKER);
11
+ const before = index === -1 ? text : text.slice(0, index);
12
+ return before.trim().replace(/^#\s+[^\n]*/, "").trim();
13
+ }
@@ -0,0 +1,148 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { ConnectionStore, connection } from "./connection.svelte";
3
+ import { FlowStore } from "./flow.svelte";
4
+ import type { FlowSnapshot } from "./types";
5
+
6
+ interface FakeEnvelope {
7
+ data: string;
8
+ }
9
+
10
+ /** Stands in for the browser socket so closed and open sends are explicit. */
11
+ class FakeSocket {
12
+ static CONNECTING = 0;
13
+ static OPEN = 1;
14
+ static CLOSING = 2;
15
+ static CLOSED = 3;
16
+ static instances: FakeSocket[] = [];
17
+
18
+ readyState = FakeSocket.CONNECTING;
19
+ onopen: (() => void) | null = null;
20
+ onmessage: ((event: FakeEnvelope) => void) | null = null;
21
+ onerror: (() => void) | null = null;
22
+ onclose: (() => void) | null = null;
23
+ sent: string[] = [];
24
+
25
+ constructor(readonly url: string) {
26
+ FakeSocket.instances.push(this);
27
+ }
28
+
29
+ send(data: string): void {
30
+ this.sent.push(data);
31
+ }
32
+
33
+ close(): void {
34
+ this.readyState = FakeSocket.CLOSED;
35
+ this.onclose?.();
36
+ }
37
+
38
+ open(): void {
39
+ this.readyState = FakeSocket.OPEN;
40
+ this.onopen?.();
41
+ }
42
+ }
43
+
44
+ interface InstalledGlobals {
45
+ WebSocket?: unknown;
46
+ window: unknown;
47
+ location?: unknown;
48
+ }
49
+
50
+ const originalGlobals: InstalledGlobals = {
51
+ WebSocket: (globalThis as unknown as Record<string, unknown>).WebSocket,
52
+ window: (globalThis as unknown as Record<string, unknown>).window,
53
+ location: (globalThis as unknown as Record<string, unknown>).location,
54
+ };
55
+
56
+ function installGlobals(values: InstalledGlobals): void {
57
+ const target = globalThis as unknown as Record<string, unknown>;
58
+ if (values.WebSocket !== undefined) target.WebSocket = values.WebSocket;
59
+ target.window = values.window;
60
+ if (values.location !== undefined) target.location = values.location;
61
+ }
62
+
63
+ function restoreGlobals(): void {
64
+ const target = globalThis as unknown as Record<string, unknown>;
65
+ for (const [key, value] of Object.entries(originalGlobals)) {
66
+ if (value === undefined) delete target[key];
67
+ else target[key] = value;
68
+ }
69
+ }
70
+
71
+ function fakeWindow(): Record<string, unknown> {
72
+ return {
73
+ setTimeout: () => 0,
74
+ clearTimeout: () => undefined,
75
+ setInterval: () => 0,
76
+ clearInterval: () => undefined,
77
+ };
78
+ }
79
+
80
+ function snapshot(goal: string, decisions: string): FlowSnapshot {
81
+ return { goal, decisions } as unknown as FlowSnapshot;
82
+ }
83
+
84
+ afterEach(() => {
85
+ FakeSocket.instances = [];
86
+ restoreGlobals();
87
+ });
88
+
89
+ test("a save on a closed connection keeps the drafts dirty", () => {
90
+ const sockets = new ConnectionStore();
91
+ expect(sockets.open).toBe(false);
92
+ const store = new FlowStore();
93
+ store.applySnapshot(snapshot("old goal", "old decisions"));
94
+
95
+ store.setGoalDraft("edited goal");
96
+ store.saveGoal();
97
+ expect(store.goalDirty).toBe(true);
98
+ expect(store.goalDraft).toBe("edited goal");
99
+
100
+ store.setDecisionsDraft("edited decisions");
101
+ store.saveDecisions();
102
+ expect(store.decisionsDirty).toBe(true);
103
+ expect(store.decisionsDraft).toBe("edited decisions");
104
+
105
+ // The reconnect snapshot must not replace edits that were never sent.
106
+ store.applySnapshot(snapshot("server goal", "server decisions"));
107
+ expect(store.goalDraft).toBe("edited goal");
108
+ expect(store.decisionsDraft).toBe("edited decisions");
109
+ });
110
+
111
+ test("a save on an open connection clears the matching dirty flag", () => {
112
+ FakeSocket.instances = [];
113
+ installGlobals({
114
+ WebSocket: FakeSocket as unknown as typeof WebSocket,
115
+ window: fakeWindow(),
116
+ location: { protocol: "http:", host: "test.invalid" },
117
+ });
118
+ const store = new FlowStore();
119
+ store.applySnapshot(snapshot("old goal", "old decisions"));
120
+
121
+ connection.connect();
122
+ const socket = FakeSocket.instances[0];
123
+ if (socket === undefined) throw new Error("no socket created");
124
+ socket.open();
125
+
126
+ store.setGoalDraft("edited goal");
127
+ store.saveGoal();
128
+ expect(store.goalDirty).toBe(false);
129
+ expect(socket.sent).toContain(
130
+ JSON.stringify({ type: "saveFlowGoal", text: "edited goal" }),
131
+ );
132
+
133
+ store.setDecisionsDraft("edited decisions");
134
+ store.saveDecisions();
135
+ expect(store.decisionsDirty).toBe(false);
136
+ expect(socket.sent).toContain(
137
+ JSON.stringify({ type: "saveFlowDecisions", text: "edited decisions" }),
138
+ );
139
+
140
+ // A later disconnect must not let the next save look successful.
141
+ socket.close();
142
+ store.setGoalDraft("second edit");
143
+ store.saveGoal();
144
+ expect(store.goalDirty).toBe(true);
145
+ expect(store.goalDraft).toBe("second edit");
146
+
147
+ connection.dispose();
148
+ });