@dsdenes/devboard 0.1.560 → 0.1.562
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +59 -0
- package/README.md +2497 -26
- package/bin/devboard.mjs +352 -118
- package/client/App.svelte +523 -0
- package/client/FeedbackOverlay.svelte +1125 -0
- package/client/FlowMode.svelte +1015 -0
- package/client/app.css +3922 -0
- package/client/env.d.ts +23 -0
- package/client/lib/commands.ts +836 -0
- package/client/lib/components/ActivityBar.svelte +295 -0
- package/client/lib/components/AdrTable.svelte +241 -0
- package/client/lib/components/CommandPalette.svelte +198 -0
- package/client/lib/components/ConceptKind.svelte +66 -0
- package/client/lib/components/ConfirmDialog.svelte +62 -0
- package/client/lib/components/DataTable.svelte +243 -0
- package/client/lib/components/DetailSplit.svelte +68 -0
- package/client/lib/components/DiscoverOverview.svelte +898 -0
- package/client/lib/components/DiscoverWorkspace.svelte +437 -0
- package/client/lib/components/DraftsPage.svelte +186 -0
- package/client/lib/components/GoalWorkspace.svelte +1042 -0
- package/client/lib/components/IssuesAgentFocus.svelte +117 -0
- package/client/lib/components/IssuesAgentLog.svelte +50 -0
- package/client/lib/components/IssuesQuestion.svelte +95 -0
- package/client/lib/components/IssuesWorkspace.svelte +577 -0
- package/client/lib/components/MermaidDiagram.svelte +89 -0
- package/client/lib/components/MonacoEditor.svelte +190 -0
- package/client/lib/components/NotificationRegion.svelte +195 -0
- package/client/lib/components/PaneHandle.svelte +25 -0
- package/client/lib/components/ScoutIssues.svelte +215 -0
- package/client/lib/components/ScoutWorkspace.svelte +180 -0
- package/client/lib/components/Sidebar.svelte +622 -0
- package/client/lib/components/StatusBar.svelte +118 -0
- package/client/lib/components/Terminal.svelte +193 -0
- package/client/lib/components/WatchdogWorkspace.svelte +537 -0
- package/client/lib/components/ui/button.svelte +56 -0
- package/client/lib/components/ui/icon-button.svelte +56 -0
- package/client/lib/components/ui/input.svelte +19 -0
- package/client/lib/components/ui/page-header.svelte +120 -0
- package/client/lib/components/ui/stage-track.svelte +141 -0
- package/client/lib/components/watchdog-indicator.svelte +189 -0
- package/client/lib/connection.svelte.ts +210 -0
- package/client/lib/dependency-diagram.test.ts +90 -0
- package/client/lib/dependency-diagram.ts +75 -0
- package/client/lib/discover-format.test.ts +265 -0
- package/client/lib/discover-format.ts +394 -0
- package/client/lib/discover.svelte.ts +412 -0
- package/client/lib/draft-format.test.ts +48 -0
- package/client/lib/draft-format.ts +13 -0
- package/client/lib/flow-save.test.ts +148 -0
- package/client/lib/flow.svelte.ts +445 -0
- package/client/lib/goal-reconnect.test.ts +282 -0
- package/client/lib/goal.svelte.ts +652 -0
- package/client/lib/issues.svelte.ts +439 -0
- package/client/lib/layout.svelte.ts +234 -0
- package/client/lib/notification-issue.test.ts +53 -0
- package/client/lib/notification-issue.ts +27 -0
- package/client/lib/notifications.svelte.ts +62 -0
- package/client/lib/poll-status.test.ts +141 -0
- package/client/lib/poll-status.ts +88 -0
- package/client/lib/record-status.test.ts +19 -0
- package/client/lib/record-status.ts +25 -0
- package/client/lib/scout.svelte.ts +164 -0
- package/client/lib/skill-document.test.ts +77 -0
- package/client/lib/skill-document.ts +57 -0
- package/client/lib/status-tone.test.ts +40 -0
- package/client/lib/status-tone.ts +33 -0
- package/client/lib/types.ts +578 -0
- package/client/lib/utils.ts +6 -0
- package/client/lib/viewport.svelte.ts +20 -0
- package/client/lib/watchdog.svelte.ts +69 -0
- package/client/main.ts +7 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/abnfDiagram-VCTEODGH-CmBSMArZ.js +1 -0
- package/dist/assets/arc-BZgCSX2H.js +1 -0
- package/dist/assets/architecture-7GRP2DOG-CF9MHlfo.js +1 -0
- package/dist/assets/architectureDiagram-5GKGNRK7-DzWB6xiQ.js +36 -0
- package/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/assets/blockDiagram-I7D4REHJ-BtmGJ0hn.js +129 -0
- package/dist/assets/c4Diagram-7LVT6UL2-D5CHrrRX.js +38 -0
- package/dist/assets/channel-CgM4lcUW.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/assets/chunk-4HAMMTFA-DeIufjFl.js +62 -0
- package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/assets/chunk-75Z2AOVW-DbtzyKUg.js +2 -0
- package/dist/assets/chunk-DU6HZSFF-C0fdbx3K.js +127 -0
- package/dist/assets/chunk-F27PBJKO-HObL2IvF.js +1 -0
- package/dist/assets/chunk-FOHPRMQF-DD4dKubD.js +161 -0
- package/dist/assets/chunk-GMAD6QVW-fg8wvtPY.js +72 -0
- package/dist/assets/chunk-GVQU2GXP-C0TaHOrD.js +1 -0
- package/dist/assets/chunk-IMKFNOWR-BVoIdx20.js +231 -0
- package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/assets/chunk-L3NEJ4N5-B_kmqfk5.js +1 -0
- package/dist/assets/chunk-OSK3NFVY-DO0vhxHH.js +10 -0
- package/dist/assets/chunk-P2QGCYS3-CjljLvFj.js +1 -0
- package/dist/assets/chunk-POPQ4Y6H-BU0C7tw6.js +1 -0
- package/dist/assets/chunk-PWAF6VOD-Dn0OjBt0.js +1 -0
- package/dist/assets/chunk-SHT3W25Y-CMCZE2xx.js +168 -0
- package/dist/assets/chunk-SVP7TREG-BIRmRy_W.js +88 -0
- package/dist/assets/chunk-TICWLB2K-C1bc2CDE.js +206 -0
- package/dist/assets/chunk-XXDRQBXY-Cc38QUmR.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/assets/classDiagram-ZZMXUADV-BDh5w6Nc.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-BDh5w6Nc.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-B7pU7qhM.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-76dswSqS.js +1 -0
- package/dist/assets/cynefinDiagram-5FMLGOSQ-2kS4q01d.js +62 -0
- package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/assets/dagre-D7JM2Yhv.js +1 -0
- package/dist/assets/dagre-GXQ25YYZ-B5Y21TQM.js +4 -0
- package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/assets/diagram-S7CK7UJ4-CY6td7Et.js +30 -0
- package/dist/assets/diagram-UQ7AKVKN-DmDqnGZf.js +41 -0
- package/dist/assets/diagram-VSXAHHWV-KGLWOOcE.js +3 -0
- package/dist/assets/diagram-VX7I27RA-D5syUraJ.js +24 -0
- package/dist/assets/diagram-Z3DM3KII-Dtrbs1Om.js +24 -0
- package/dist/assets/dist-Dce-2YbA.js +1 -0
- package/dist/assets/ebnfDiagram-PWID7BFC-CQpJLtU3.js +1 -0
- package/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
- package/dist/assets/erDiagram-RLTQ6QDP-cMTzKLu_.js +99 -0
- package/dist/assets/eventmodeling-NTZA5JFV-CMzLdNaL.js +1 -0
- package/dist/assets/flowDiagram-HODETNUW-BMUajB-G.js +1 -0
- package/dist/assets/ganttDiagram-EL5Y4UJY-DseBS5Fs.js +292 -0
- package/dist/assets/gitGraph-4MIJSDKK-CUQZRpxJ.js +1 -0
- package/dist/assets/gitGraphDiagram-WWUBYQGX-CfMfsJ3b.js +106 -0
- package/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/assets/index-ByQ-XX38.css +1 -0
- package/dist/assets/index-CIf5TpfY.js +214 -0
- package/dist/assets/info-A6RAGUB7-D3nAKZ8M.js +1 -0
- package/dist/assets/infoDiagram-27XIBGKW-zu1O0WOy.js +2 -0
- package/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/assets/ishikawaDiagram-5VMMS53U-DKC7BO9p.js +70 -0
- package/dist/assets/journeyDiagram-3NMN7TZE-Ld5mXse7.js +139 -0
- package/dist/assets/kanban-definition-UXKFOSKX-CH7_QlHL.js +89 -0
- package/dist/assets/katex-DJhrJSbw.js +257 -0
- package/dist/assets/line-N9XiCREw.js +1 -0
- package/dist/assets/linear-C9hnqmJv.js +1 -0
- package/dist/assets/mermaid-parser.core-DgPwV5Bh.js +7 -0
- package/dist/assets/mermaid.core-BKqynXLR.js +44 -0
- package/dist/assets/mindmap-definition-YA3MSWOX-DkiL-wiW.js +96 -0
- package/dist/assets/monaco-DjpsE8to.css +1 -0
- package/dist/assets/monaco-DqzJaNIe.js +908 -0
- package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/assets/packet-AYTQ26CC-DuMTvpEJ.js +1 -0
- package/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/assets/pegDiagram-XKGWAZYB-DvMl_xSe.js +1 -0
- package/dist/assets/pie-WAS4IAKB-Da0xPaD8.js +1 -0
- package/dist/assets/pieDiagram-E7YTZNPT-amANNRJY.js +39 -0
- package/dist/assets/quadrantDiagram-AXDQQJYC-BrgvDMnk.js +7 -0
- package/dist/assets/radar-RG4KPBEZ-Dz3Ih5-N.js +1 -0
- package/dist/assets/railroad-74A4TZTK-CKMW-IV2.js +1 -0
- package/dist/assets/railroad-abnf-HS5TGJTU-ChqmSWSF.js +1 -0
- package/dist/assets/railroad-ebnf-LZEXJU2U-V3R4fFdb.js +1 -0
- package/dist/assets/railroad-peg-WCYAUIDC-B-RuhUQW.js +1 -0
- package/dist/assets/railroadDiagram-O6MQD6OU-FjG-6OLu.js +1 -0
- package/dist/assets/requirementDiagram-BXWQKSXE-Cnj8RfE4.js +84 -0
- package/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/assets/sankeyDiagram-P5KCCOFB-DMu1fnEh.js +40 -0
- package/dist/assets/sequenceDiagram-WJ2MYXX4-CtCtaRpW.js +162 -0
- package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/assets/src-CGp3bfio.js +1 -0
- package/dist/assets/stateDiagram-D77RDMKH-xcJbLNhz.js +1 -0
- package/dist/assets/stateDiagram-v2-MP3YSRHH-DYHrQmJJ.js +1 -0
- package/dist/assets/swimlanes-42K2YHIH-BwLln_ga.js +1 -0
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BcDfUQ0A.js +8 -0
- package/dist/assets/timeline-definition-24CTP7MA-ucNGsUUz.js +120 -0
- package/dist/assets/treeView-Q6P3EWNA-3uOomRue.js +1 -0
- package/dist/assets/treemap-WGGIJYW6-iGK7vwID.js +1 -0
- package/dist/assets/vennDiagram-4TSXK5OY-B9LgGe16.js +34 -0
- package/dist/assets/wardley-WFR3VGLG-FupMOf6k.js +1 -0
- package/dist/assets/wardleyDiagram-VM6X3IG4-CGZdEXM2.js +78 -0
- package/dist/assets/xterm-BrP-ENHg.css +1 -0
- package/dist/assets/xterm-C8zLTNGk.js +16 -0
- package/dist/assets/xychartDiagram-S5SC5T6Z-DafZrSBJ.js +7 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +6 -0
- package/dist/index.html +34 -0
- package/extensions/implement-goal-issue.ts +368 -0
- package/extensions/implement-next-epic.ts +835 -0
- package/index.html +28 -0
- package/install.sh +111 -0
- package/issues/control.ts +91 -0
- package/issues/guards.ts +12 -0
- package/issues/ledger.ts +122 -0
- package/issues/legacy.ts +68 -0
- package/issues/lock.ts +305 -0
- package/issues/paths.ts +76 -0
- package/issues/preflight.ts +189 -0
- package/issues/repository.ts +203 -0
- package/issues/scout/actions.ts +255 -0
- package/issues/scout/control.ts +80 -0
- package/issues/scout/github.ts +358 -0
- package/issues/scout/paths.ts +129 -0
- package/issues/scout/preflight.ts +277 -0
- package/issues/scout/recommendation.ts +400 -0
- package/issues/scout/scan.ts +143 -0
- package/issues/scout/state.ts +448 -0
- package/issues/scout/worker.ts +1282 -0
- package/issues/state.ts +584 -0
- package/issues/tracker.ts +271 -0
- package/issues/worker.ts +1065 -0
- package/lib/dependencies.ts +61 -0
- package/lib/forge-auth.ts +953 -0
- package/lib/forge.ts +141 -0
- package/lib/gitea.ts +709 -0
- package/lib/github-release.ts +318 -0
- package/lib/inventory.test.ts +79 -0
- package/lib/inventory.ts +53 -0
- package/lib/release.test.ts +269 -0
- package/lib/release.ts +801 -0
- package/omp/commands/adr-resolve.md +8 -0
- package/omp/commands/discover.md +6 -0
- package/omp/commands/epic-next.md +8 -0
- package/omp/commands/flow-change.md +8 -0
- package/omp/commands/flow-goal.md +8 -0
- package/omp/commands/flow-implement.md +8 -0
- package/omp/commands/flow-widget.md +8 -0
- package/omp/commands/goal-draft.md +8 -0
- package/omp/commands/goal-outcome.md +8 -0
- package/omp/commands/implement-issue.md +8 -0
- package/omp/commands/prepare-skills.md +6 -0
- package/omp/commands/scout-recommendations.md +8 -0
- package/omp/commands/setup-devboard.md +24 -0
- package/omp/config.yml +12 -0
- package/omp/extensions/implement-goal-issue.ts +2 -0
- package/omp/extensions/implement-next-epic.ts +2 -0
- package/omp/mcp.json +9 -0
- package/package.json +86 -7
- package/prompts/adr-resolve.md +130 -0
- package/prompts/discover.md +541 -0
- package/prompts/epic-next.md +533 -0
- package/prompts/flow-change.md +76 -0
- package/prompts/flow-goal.md +126 -0
- package/prompts/flow-implement.md +103 -0
- package/prompts/flow-widget.md +54 -0
- package/prompts/goal-draft.md +147 -0
- package/prompts/goal-outcome.md +87 -0
- package/prompts/implement-issue.md +66 -0
- package/prompts/prepare-skills.md +142 -0
- package/prompts/scout-recommendations.md +143 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +6 -0
- package/server/acp.ts +540 -0
- package/server/activity.test.ts +96 -0
- package/server/activity.ts +48 -0
- package/server/app.test.ts +882 -0
- package/server/app.ts +6179 -0
- package/server/ask.test.ts +543 -0
- package/server/ask.ts +589 -0
- package/server/dependencies.test.ts +118 -0
- package/server/discover-remote.test.ts +1602 -0
- package/server/discover-sheet.test.ts +176 -0
- package/server/discover.test.ts +1510 -0
- package/server/discover.ts +3198 -0
- package/server/feedback.test.ts +458 -0
- package/server/feedback.ts +369 -0
- package/server/fixtures/adr-source.ts +71 -0
- package/server/fixtures/hang-download.ts +14 -0
- package/server/fixtures/stub-agent.ts +1870 -0
- package/server/fixtures/stub-forge.ts +141 -0
- package/server/fixtures/stub-gh.ts +57 -0
- package/server/flow-pr.test.ts +976 -0
- package/server/flow.test.ts +1632 -0
- package/server/flow.ts +2655 -0
- package/server/forge-pr.ts +387 -0
- package/server/goal-creation.test.ts +957 -0
- package/server/goal-draft.test.ts +348 -0
- package/server/goal-drafts.ts +449 -0
- package/server/goal-proposal.test.ts +479 -0
- package/server/goal-proposal.ts +232 -0
- package/server/goal-revision.test.ts +613 -0
- package/server/goal-slug.test.ts +56 -0
- package/server/goal-slug.ts +96 -0
- package/server/goal.ts +20 -0
- package/server/index.ts +147 -0
- package/server/issue-close.test.ts +100 -0
- package/server/issue-close.ts +178 -0
- package/server/issues.test.ts +167 -0
- package/server/issues.ts +479 -0
- package/server/journal.test.ts +350 -0
- package/server/journal.ts +329 -0
- package/server/lifecycle-pause.test.ts +1795 -0
- package/server/lifecycle.test.ts +1879 -0
- package/server/parallel-lifecycle.test.ts +1029 -0
- package/server/persistence-restart.test.ts +723 -0
- package/server/persistence.test.ts +1935 -0
- package/server/records.test.ts +180 -0
- package/server/records.ts +99 -0
- package/server/reset-implementation.test.ts +997 -0
- package/server/run.test.ts +611 -0
- package/server/scout.test.ts +877 -0
- package/server/scout.ts +459 -0
- package/server/skills-preparation.test.ts +1073 -0
- package/server/state.ts +249 -0
- package/server/tracker.ts +369 -0
- package/server/updates.test.ts +444 -0
- package/server/updates.ts +198 -0
- package/server/version.test.ts +140 -0
- package/server/version.ts +151 -0
- package/server/watchdog.ts +389 -0
- package/svelte.config.js +5 -0
- package/templates/host-AGENTS.md +235 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +21 -0
- package/watchdog/cleanup-main.ts +98 -0
- package/watchdog/cleanup.ts +301 -0
- package/watchdog/collect.ts +311 -0
- package/watchdog/coordinator.ts +397 -0
- package/watchdog/delivery.ts +107 -0
- package/watchdog/discovery.ts +209 -0
- package/watchdog/findings.ts +211 -0
- package/watchdog/github.ts +189 -0
- package/watchdog/history.ts +138 -0
- package/watchdog/install.ts +103 -0
- package/watchdog/ledger.ts +188 -0
- package/watchdog/lock.ts +63 -0
- package/watchdog/main.ts +106 -0
- package/watchdog/status.ts +71 -0
- package/watchdog/systemd.ts +96 -0
- package/watchdog/uninstall.ts +30 -0
- package/watchdog/units/devboard-watchdog.service +9 -0
- package/watchdog/units/devboard-watchdog.timer +10 -0
package/server/scout.ts
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFileSync, readlinkSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { liveLockOwner, pidAlive, type LockOwner } from "../issues/lock";
|
|
5
|
+
import {
|
|
6
|
+
controlDirectory,
|
|
7
|
+
lockPath,
|
|
8
|
+
scanDirectory,
|
|
9
|
+
settingsPath,
|
|
10
|
+
statePath,
|
|
11
|
+
} from "../issues/scout/paths";
|
|
12
|
+
import { writeCommand, type ScoutCommand } from "../issues/scout/control";
|
|
13
|
+
import { preflightScout, type ScoutPreflightResult } from "../issues/scout/preflight";
|
|
14
|
+
import { cachedRepository } from "../issues/repository";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_LIMITS,
|
|
17
|
+
isValidLimit,
|
|
18
|
+
readSettings,
|
|
19
|
+
readState,
|
|
20
|
+
writeSettings,
|
|
21
|
+
type ScoutIssueRow,
|
|
22
|
+
type ScoutLimits,
|
|
23
|
+
type ScoutState,
|
|
24
|
+
type ScoutSync,
|
|
25
|
+
} from "../issues/scout/state";
|
|
26
|
+
import { readActivity } from "../issues/state";
|
|
27
|
+
import { activityDelta } from "./activity";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The dashboard half of Auto Scout. It never owns a scan loop, an agent, or a
|
|
31
|
+
* lock: it reads the worker's repo-scoped board, replays its activity, and
|
|
32
|
+
* writes commands to it. A worker started here is detached in its own session,
|
|
33
|
+
* so killing this server leaves it running and a replacement reattaches.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export interface ScoutSnapshot {
|
|
37
|
+
present: boolean;
|
|
38
|
+
repo: string | null;
|
|
39
|
+
root: string;
|
|
40
|
+
lifecycle: ScoutState["lifecycle"];
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
attached: boolean;
|
|
43
|
+
worker: ScoutState["worker"];
|
|
44
|
+
/** The limits the dashboard persisted, so a save shows without a worker. */
|
|
45
|
+
limits: ScoutLimits;
|
|
46
|
+
issues: ScoutIssueRow[];
|
|
47
|
+
sync: ScoutSync;
|
|
48
|
+
scan: ScoutState["scan"];
|
|
49
|
+
newestScanId: string | null;
|
|
50
|
+
nextAttemptAt: string | null;
|
|
51
|
+
lastError: string | null;
|
|
52
|
+
notice: string | null;
|
|
53
|
+
updatedAt: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ScoutActivityEntry {
|
|
57
|
+
text: string;
|
|
58
|
+
append: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ScoutBridgeOptions {
|
|
62
|
+
repoDir: string;
|
|
63
|
+
stateDir: string;
|
|
64
|
+
agentCommand: string;
|
|
65
|
+
runningVersion: string | null;
|
|
66
|
+
harnessDir: string;
|
|
67
|
+
send: (event: unknown, except?: { send(data: string): unknown }) => void;
|
|
68
|
+
log?: (line: string) => void;
|
|
69
|
+
environment?: Record<string, string | undefined>;
|
|
70
|
+
pollMs?: number;
|
|
71
|
+
/** Test seams: replace the start preflight and the detached spawn. */
|
|
72
|
+
preflight?: (options: {
|
|
73
|
+
repoDir: string;
|
|
74
|
+
stateDir: string;
|
|
75
|
+
environment: Record<string, string | undefined>;
|
|
76
|
+
}) => Promise<ScoutPreflightResult>;
|
|
77
|
+
spawnWorker?: (argv: string[], environment: Record<string, string | undefined>) => number | undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class ScoutBridge {
|
|
81
|
+
private readonly repoDir: string;
|
|
82
|
+
private readonly stateDir: string;
|
|
83
|
+
private readonly agentCommand: string;
|
|
84
|
+
private readonly runningVersion: string | null;
|
|
85
|
+
private readonly harnessDir: string;
|
|
86
|
+
private readonly send: (event: unknown, except?: { send(data: string): unknown }) => void;
|
|
87
|
+
private readonly environment: Record<string, string | undefined>;
|
|
88
|
+
private readonly repository: () => string | null;
|
|
89
|
+
private readonly preflight: NonNullable<ScoutBridgeOptions["preflight"]>;
|
|
90
|
+
private readonly spawnWorker: NonNullable<ScoutBridgeOptions["spawnWorker"]>;
|
|
91
|
+
private readonly log: (line: string) => void;
|
|
92
|
+
private readonly pollMsValue: number;
|
|
93
|
+
|
|
94
|
+
private timer: Timer | undefined;
|
|
95
|
+
private lastSignature = "";
|
|
96
|
+
private lastActivityScanId: string | null = null;
|
|
97
|
+
private lastActivity: string[] = [];
|
|
98
|
+
private stopping: { pid: number; timer: Timer } | undefined;
|
|
99
|
+
private starting = false;
|
|
100
|
+
|
|
101
|
+
constructor(options: ScoutBridgeOptions) {
|
|
102
|
+
this.repoDir = resolve(options.repoDir);
|
|
103
|
+
this.stateDir = resolve(options.stateDir);
|
|
104
|
+
this.agentCommand = options.agentCommand;
|
|
105
|
+
this.runningVersion = options.runningVersion;
|
|
106
|
+
this.harnessDir = options.harnessDir;
|
|
107
|
+
this.send = options.send;
|
|
108
|
+
this.environment = options.environment ?? process.env;
|
|
109
|
+
this.repository = cachedRepository(this.repoDir);
|
|
110
|
+
this.log = options.log ?? (() => undefined);
|
|
111
|
+
this.pollMsValue = Math.max(50, options.pollMs ?? 250);
|
|
112
|
+
this.preflight =
|
|
113
|
+
options.preflight ??
|
|
114
|
+
((request) =>
|
|
115
|
+
preflightScout({
|
|
116
|
+
repoDir: request.repoDir,
|
|
117
|
+
stateDir: request.stateDir,
|
|
118
|
+
environment: request.environment,
|
|
119
|
+
}));
|
|
120
|
+
this.spawnWorker = options.spawnWorker ?? ((argv, environment) => this.detach(argv, environment));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Starts observing the worker state; it never starts a worker itself. */
|
|
124
|
+
begin(): void {
|
|
125
|
+
if (this.timer !== undefined) return;
|
|
126
|
+
this.timer = setInterval(() => this.refresh(), this.pollMsValue);
|
|
127
|
+
void this.refresh();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
dispose(): void {
|
|
131
|
+
if (this.timer !== undefined) {
|
|
132
|
+
clearInterval(this.timer);
|
|
133
|
+
this.timer = undefined;
|
|
134
|
+
}
|
|
135
|
+
if (this.stopping !== undefined) {
|
|
136
|
+
clearTimeout(this.stopping.timer);
|
|
137
|
+
this.stopping = undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private detach(argv: string[], environment: Record<string, string | undefined>): number | undefined {
|
|
142
|
+
try {
|
|
143
|
+
const child = spawn(argv[0]!, argv.slice(1), {
|
|
144
|
+
cwd: this.repoDir,
|
|
145
|
+
env: environment,
|
|
146
|
+
detached: true,
|
|
147
|
+
stdio: "ignore",
|
|
148
|
+
});
|
|
149
|
+
child.unref();
|
|
150
|
+
return child.pid;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
this.log(
|
|
153
|
+
`could not start the scout worker: ${error instanceof Error ? error.message : String(error)}`,
|
|
154
|
+
);
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ------------------------------------------------------------------ */
|
|
160
|
+
/* Reading */
|
|
161
|
+
/* ------------------------------------------------------------------ */
|
|
162
|
+
|
|
163
|
+
private currentState(): ScoutState | undefined {
|
|
164
|
+
return readState(statePath(this.stateDir, this.repoDir));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private lockOwner(): LockOwner | undefined {
|
|
168
|
+
return liveLockOwner(lockPath(this.repoDir));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** True when there is a worker state or a live lock to replay. */
|
|
172
|
+
get present(): boolean {
|
|
173
|
+
return this.currentState() !== undefined || this.lockOwner() !== undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
snapshot(): ScoutSnapshot {
|
|
177
|
+
const state = this.currentState();
|
|
178
|
+
const owner = this.lockOwner();
|
|
179
|
+
const attached =
|
|
180
|
+
owner !== undefined && resolve(owner.stateDir) === this.stateDir && pidAlive(owner.pid);
|
|
181
|
+
const active = attached ? state : undefined;
|
|
182
|
+
const lifecycle =
|
|
183
|
+
active?.lifecycle === "stopped" || active === undefined ? "stopped" : active.lifecycle;
|
|
184
|
+
const worker = state?.worker ?? null;
|
|
185
|
+
const mismatch =
|
|
186
|
+
attached && worker !== null && worker.harnessVersion !== this.runningVersion
|
|
187
|
+
? `Auto Scout worker ${worker.harnessVersion ?? "unknown"} is running; this dashboard ` +
|
|
188
|
+
`loaded ${this.runningVersion ?? "unknown"}. It is left running; Stop it to launch the ` +
|
|
189
|
+
"current harness on the next Start."
|
|
190
|
+
: null;
|
|
191
|
+
const limits = readSettings(settingsPath(this.stateDir, this.repoDir)) ?? state?.limits;
|
|
192
|
+
return {
|
|
193
|
+
present: state !== undefined,
|
|
194
|
+
repo: state !== undefined && state.repo.length > 0 ? state.repo : this.repository(),
|
|
195
|
+
root: this.repoDir,
|
|
196
|
+
lifecycle,
|
|
197
|
+
enabled: active?.enabled === true && lifecycle !== "stopped",
|
|
198
|
+
attached,
|
|
199
|
+
worker: active?.worker ?? null,
|
|
200
|
+
limits: limits ?? { ...DEFAULT_LIMITS },
|
|
201
|
+
issues: state?.issues ?? [],
|
|
202
|
+
sync: state?.sync ?? { state: "stale", at: null, error: null },
|
|
203
|
+
scan: active?.scan ?? null,
|
|
204
|
+
newestScanId: state?.newestScanId ?? null,
|
|
205
|
+
nextAttemptAt: active?.nextAttemptAt ?? null,
|
|
206
|
+
lastError: active?.lastError ?? null,
|
|
207
|
+
notice: mismatch ?? active?.notice ?? null,
|
|
208
|
+
updatedAt: state?.updatedAt ?? null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A connect-time replay: state to the caller, then the newest scan's whole
|
|
214
|
+
* activity to the caller and every other client. The baseline is shared, so
|
|
215
|
+
* advancing it for one client alone would make the next delta skip what the
|
|
216
|
+
* older clients missed.
|
|
217
|
+
*/
|
|
218
|
+
replay(target?: { send(data: string): unknown }): void {
|
|
219
|
+
const deliver =
|
|
220
|
+
target === undefined
|
|
221
|
+
? this.send
|
|
222
|
+
: (event: unknown) => {
|
|
223
|
+
try {
|
|
224
|
+
target.send(JSON.stringify(event));
|
|
225
|
+
} catch {
|
|
226
|
+
// The socket closed before the replay finished.
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const snapshot = this.snapshot();
|
|
230
|
+
deliver({ type: "scoutState", ...snapshot });
|
|
231
|
+
const scanId = snapshot.scan?.scanId ?? snapshot.newestScanId;
|
|
232
|
+
if (scanId === null) return;
|
|
233
|
+
const activity = this.readActivity(scanId);
|
|
234
|
+
this.lastActivityScanId = scanId;
|
|
235
|
+
this.lastActivity = activity;
|
|
236
|
+
const event = {
|
|
237
|
+
type: "scoutActivity",
|
|
238
|
+
scanId,
|
|
239
|
+
replay: true,
|
|
240
|
+
entries: activity.map((text) => ({ text, append: false })),
|
|
241
|
+
};
|
|
242
|
+
deliver(event);
|
|
243
|
+
// A targeted replay also reaches the other clients: the baseline is shared,
|
|
244
|
+
// so a replay only the new client saw would make the next delta skip it.
|
|
245
|
+
if (target !== undefined) this.send(event, target);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private readActivity(scanId: string): string[] {
|
|
249
|
+
return readActivity(scanDirectory(this.repoDir, scanId));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private publishActivity(scanId: string, activity: string[]): void {
|
|
253
|
+
if (this.lastActivityScanId !== scanId) {
|
|
254
|
+
this.lastActivityScanId = scanId;
|
|
255
|
+
this.lastActivity = activity;
|
|
256
|
+
this.send({
|
|
257
|
+
type: "scoutActivity",
|
|
258
|
+
scanId,
|
|
259
|
+
replay: true,
|
|
260
|
+
entries: activity.map((text) => ({ text, append: false })),
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const delta = activityDelta(this.lastActivity, activity);
|
|
265
|
+
this.lastActivity = activity;
|
|
266
|
+
if (!delta.replay && delta.entries.length === 0) return;
|
|
267
|
+
this.send({
|
|
268
|
+
type: "scoutActivity",
|
|
269
|
+
scanId,
|
|
270
|
+
...(delta.replay ? { replay: true } : {}),
|
|
271
|
+
entries: delta.entries,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
refresh(): void {
|
|
276
|
+
const snapshot = this.snapshot();
|
|
277
|
+
const signature = JSON.stringify(snapshot);
|
|
278
|
+
if (signature !== this.lastSignature) {
|
|
279
|
+
this.lastSignature = signature;
|
|
280
|
+
this.send({ type: "scoutState", ...snapshot });
|
|
281
|
+
}
|
|
282
|
+
// The page shows the current scan, or the newest one once it finishes.
|
|
283
|
+
const scanId = snapshot.scan?.scanId ?? snapshot.newestScanId;
|
|
284
|
+
if (scanId === null) {
|
|
285
|
+
this.lastActivityScanId = null;
|
|
286
|
+
this.lastActivity = [];
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.publishActivity(scanId, this.readActivity(scanId));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ------------------------------------------------------------------ */
|
|
293
|
+
/* Commands */
|
|
294
|
+
/* ------------------------------------------------------------------ */
|
|
295
|
+
|
|
296
|
+
async start(): Promise<{ ok: true; attached: boolean } | { ok: false; message: string }> {
|
|
297
|
+
if (this.starting) return { ok: true, attached: false };
|
|
298
|
+
const owner = this.lockOwner();
|
|
299
|
+
if (owner !== undefined) {
|
|
300
|
+
if (resolve(owner.stateDir) !== this.stateDir) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
message:
|
|
304
|
+
`this checkout already has an Auto Scout worker (pid ${owner.pid}) using ` +
|
|
305
|
+
`${owner.stateDir}; this dashboard's STATE_DIR is ${this.stateDir}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
this.refresh();
|
|
309
|
+
return { ok: true, attached: true };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.starting = true;
|
|
313
|
+
try {
|
|
314
|
+
const result = await this.preflight({
|
|
315
|
+
repoDir: this.repoDir,
|
|
316
|
+
stateDir: this.stateDir,
|
|
317
|
+
environment: this.environment,
|
|
318
|
+
});
|
|
319
|
+
if (!result.ok) return result;
|
|
320
|
+
return await this.spawn();
|
|
321
|
+
} finally {
|
|
322
|
+
this.starting = false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async spawn(): Promise<{ ok: true; attached: boolean } | { ok: false; message: string }> {
|
|
327
|
+
const argv = [
|
|
328
|
+
process.execPath,
|
|
329
|
+
join(this.harnessDir, "issues", "scout", "worker.ts"),
|
|
330
|
+
"--repo",
|
|
331
|
+
this.repoDir,
|
|
332
|
+
"--state-dir",
|
|
333
|
+
this.stateDir,
|
|
334
|
+
];
|
|
335
|
+
const pid = this.spawnWorker(argv, {
|
|
336
|
+
...this.environment,
|
|
337
|
+
REPO_DIR: this.repoDir,
|
|
338
|
+
STATE_DIR: this.stateDir,
|
|
339
|
+
AGENT_COMMAND: this.agentCommand,
|
|
340
|
+
});
|
|
341
|
+
if (pid === undefined) {
|
|
342
|
+
return { ok: false, message: "the Auto Scout worker could not be started" };
|
|
343
|
+
}
|
|
344
|
+
return { ok: true, attached: false };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Saves both caps. The settings file is the durable source, so a save shows
|
|
349
|
+
* after a reload and is read by the next worker start; a running worker is
|
|
350
|
+
* told immediately through its command channel.
|
|
351
|
+
*/
|
|
352
|
+
setLimits(
|
|
353
|
+
maxPerHour: unknown,
|
|
354
|
+
maxPending: unknown,
|
|
355
|
+
): { ok: true } | { ok: false; message: string } {
|
|
356
|
+
if (!isValidLimit(maxPerHour) || !isValidLimit(maxPending)) {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
message: "Both limits must be integers from 1 through 100.",
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const limits: ScoutLimits = { maxPerHour, maxPending };
|
|
363
|
+
try {
|
|
364
|
+
writeSettings(settingsPath(this.stateDir, this.repoDir), limits);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
message: `Could not save the limits: ${error instanceof Error ? error.message : String(error)}`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const owner = this.lockOwner();
|
|
372
|
+
if (owner !== undefined && resolve(owner.stateDir) === this.stateDir) {
|
|
373
|
+
try {
|
|
374
|
+
writeCommand(controlDirectory(this.stateDir, this.repoDir), {
|
|
375
|
+
type: "limits",
|
|
376
|
+
maxPerHour,
|
|
377
|
+
maxPending,
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
message: `Could not reach the worker: ${error instanceof Error ? error.message : String(error)}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
this.refresh();
|
|
387
|
+
return { ok: true };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
stop(): { ok: true } | { ok: false; message: string } {
|
|
391
|
+
return this.command({ type: "stop" });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private command(command: ScoutCommand): { ok: true } | { ok: false; message: string } {
|
|
395
|
+
const owner = this.lockOwner();
|
|
396
|
+
if (owner === undefined) {
|
|
397
|
+
return { ok: false, message: "No Auto Scout worker is running for this checkout." };
|
|
398
|
+
}
|
|
399
|
+
if (resolve(owner.stateDir) !== this.stateDir) {
|
|
400
|
+
return {
|
|
401
|
+
ok: false,
|
|
402
|
+
message:
|
|
403
|
+
`the Auto Scout worker (pid ${owner.pid}) uses ${owner.stateDir}; this dashboard's ` +
|
|
404
|
+
`STATE_DIR is ${this.stateDir}`,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// The live lock proves a worker exists even when its state file is absent,
|
|
408
|
+
// unreadable, or left by another harness version; Stop must still reach it.
|
|
409
|
+
writeCommand(controlDirectory(this.stateDir, this.repoDir), command);
|
|
410
|
+
if (command.type === "stop") this.scheduleStopProof(owner.pid);
|
|
411
|
+
return { ok: true };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Stop must end a worker that is still alive after the command. Only a
|
|
416
|
+
* process the table proves is this checkout's scout is signalled.
|
|
417
|
+
*/
|
|
418
|
+
private scheduleStopProof(pid: number): void {
|
|
419
|
+
if (this.stopping !== undefined) clearTimeout(this.stopping.timer);
|
|
420
|
+
const timer = setTimeout(() => {
|
|
421
|
+
this.stopping = undefined;
|
|
422
|
+
if (!pidAlive(pid)) return;
|
|
423
|
+
if (!this.provesWorker(pid)) return;
|
|
424
|
+
try {
|
|
425
|
+
process.kill(pid, "SIGTERM");
|
|
426
|
+
} catch {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const killTimer = setTimeout(() => {
|
|
430
|
+
if (!pidAlive(pid)) return;
|
|
431
|
+
if (!this.provesWorker(pid)) return;
|
|
432
|
+
try {
|
|
433
|
+
process.kill(pid, "SIGKILL");
|
|
434
|
+
} catch {
|
|
435
|
+
// It exited between the proof and the signal.
|
|
436
|
+
}
|
|
437
|
+
}, 2000);
|
|
438
|
+
killTimer.unref?.();
|
|
439
|
+
}, 3000);
|
|
440
|
+
timer.unref?.();
|
|
441
|
+
this.stopping = { pid, timer };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private provesWorker(pid: number): boolean {
|
|
445
|
+
let argv: string[];
|
|
446
|
+
try {
|
|
447
|
+
argv = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\u0000").filter(Boolean);
|
|
448
|
+
} catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (!argv.some((entry) => entry.endsWith(join("scout", "worker.ts")))) return false;
|
|
452
|
+
if (!argv.includes(this.repoDir)) return false;
|
|
453
|
+
try {
|
|
454
|
+
return readlinkSync(`/proc/${pid}/cwd`) === this.repoDir;
|
|
455
|
+
} catch {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|