@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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Two layers. Identify yours before reading or editing.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
<repo root>/
|
|
7
|
+
.devboard/ <- harness: interface, prompts, wrappers, install.sh
|
|
8
|
+
adrs/ <- host data
|
|
9
|
+
GOAL.md <- host data
|
|
10
|
+
.omp/ <- installed harness wrappers
|
|
11
|
+
... <- host project
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
| Path | Identity |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `.devboard/` | Agentic development harness. Project-agnostic. Owns the browser interface and its toolchain. Works *on* the host. Never the host product. |
|
|
17
|
+
| `.devboard/omp/` | Canonical agent wrappers; installed into the host's `.omp/`. |
|
|
18
|
+
| `adrs/`, `GOAL.md` | Host data at the root; never harness artifacts. |
|
|
19
|
+
| everything else | Host project. |
|
|
20
|
+
|
|
21
|
+
## Containment boundary
|
|
22
|
+
|
|
23
|
+
The whole project — host, harness, toolchain, tests, fixtures, caches, logs, temporary
|
|
24
|
+
files, and worktrees — lives inside `<repo root>`. Nothing may be created, written, or kept
|
|
25
|
+
outside it: not under `/tmp`, not in a home directory, not in a parent, sibling, or other
|
|
26
|
+
checkout. This applies to every agent, script, and service, including throwaway
|
|
27
|
+
verification, "safe" sandbox copies, and manual test data.
|
|
28
|
+
|
|
29
|
+
- Point every path variable (`ADRS_DIR`, `REPO_DIR`, `GOAL_FILE`, `STATIC_DIR`) at a
|
|
30
|
+
location inside `<repo root>`.
|
|
31
|
+
- When a tool defaults outside the root, override it to a directory inside the root; never
|
|
32
|
+
accept the default.
|
|
33
|
+
- Asked to place a file, artifact, cache, log, or worktree outside the root? Refuse and
|
|
34
|
+
propose the in-repo path.
|
|
35
|
+
- Running system binaries (`bun`, `git`, `gh`, `omp`) is not a violation; writing their
|
|
36
|
+
output outside the root is.
|
|
37
|
+
|
|
38
|
+
A temporary directory is still project data. Create it under `<repo root>` (for example
|
|
39
|
+
under `.worktree/`), never outside.
|
|
40
|
+
|
|
41
|
+
## Scope
|
|
42
|
+
|
|
43
|
+
Default to the host project. "the repo", "the project", "the code", and unnamed files all mean the host project.
|
|
44
|
+
|
|
45
|
+
Enter harness mode only when the request names the harness, "devboard", or a path inside `.devboard/`.
|
|
46
|
+
|
|
47
|
+
| Mode | Trigger | Edit |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| host (default) | everything else | host project; leave `.devboard/` untouched |
|
|
50
|
+
| harness | explicit harness / `.devboard` reference | `.devboard/` only; host files untouched unless named |
|
|
51
|
+
|
|
52
|
+
A request naming both layers is two scopes; change each side by its own rules.
|
|
53
|
+
Never edit `.devboard/` for a host request. Never edit host files for a harness request. If a task couples them, surface it and state the scope change.
|
|
54
|
+
|
|
55
|
+
## Harness rules
|
|
56
|
+
|
|
57
|
+
1. **Containment.** Keep every harness file inside `.devboard/`: code, scripts, prompts, tests, fixtures, docs, configs, CI glue, artifacts. Nothing harness-related is created outside `<repo root>` — no temporary files, caches, logs, or test data. Asked to place one outside? Refuse; propose the `.devboard/` path. Host files *invoking* the harness at runtime are fine.
|
|
58
|
+
2. **Portability.** Harness code moves between unrelated hosts. Bake in nothing host-specific: no absolute paths, project names, identifiers, remotes, secrets, language/layout/build/test-runner assumptions. Discover host facts at runtime, take them as arguments, or read them from config under `.devboard/`. Emit host paths relative to the repo root.
|
|
59
|
+
3. **Fixed location, fixed name.** Install at `<repo root>/.devboard`, spelled exactly that. Never rename or relocate per host.
|
|
60
|
+
4. **Host-neutral deletion.** Deleting `.devboard/` must leave host build, tests, and run paths working. No host hard-dependency on the harness beyond `.devboard/` itself.
|
|
61
|
+
5. **The one exception.** `AGENTS.md` sits at the repo root by design: agents must find it there. Host-agnostic, copied verbatim between hosts, not a harness artifact. Nothing else gets this treatment.
|
|
62
|
+
|
|
63
|
+
## Interface
|
|
64
|
+
|
|
65
|
+
`.devboard/` owns the browser interface and everything needed to build, test, and run it: the Bun
|
|
66
|
+
server, the Svelte client, the tests and fixtures, `package.json`, `bun.lock`, the build
|
|
67
|
+
configuration, `index.html`, the `Dockerfile`, and `README.md`. Run its commands from `.devboard`:
|
|
68
|
+
`bun install`, `bun test`, `bun run build`, `bun run start`; build the container from the
|
|
69
|
+
`.devboard` context. The interface is a harness tool, not the host product.
|
|
70
|
+
|
|
71
|
+
The harness may define its own toolchain, but it must not assume host facts: no host language,
|
|
72
|
+
package manager, test runner, build system, or layout. Host data lives at the root — the decision
|
|
73
|
+
records in `adrs/` and the goal document `GOAL.md` — and reaches the interface only through paths
|
|
74
|
+
the host supplies: `ADRS_DIR`, `REPO_DIR`, `GOAL_FILE`, `STATIC_DIR`, `AGENT_COMMAND`. The
|
|
75
|
+
lifecycle prompts under `.devboard/prompts/` likewise treat the repository root as the host and
|
|
76
|
+
exclude `.devboard/` and `.worktree/` from every survey and every write.
|
|
77
|
+
|
|
78
|
+
## Releases and installation
|
|
79
|
+
|
|
80
|
+
The harness is the public npm package `@dsdenes/devboard`. One tarball carries the launcher, the
|
|
81
|
+
server, the client sources and built bundle, the prompts, the omp wrappers and extensions, the
|
|
82
|
+
templates, and the installer. npm is the authority for install and update versions and artifacts;
|
|
83
|
+
no local checkout, running dashboard, or required release tarball is involved. GitHub is only a
|
|
84
|
+
push mirror of this repository; it carries no release the dashboard reads.
|
|
85
|
+
|
|
86
|
+
A host installs a real npm prefix at `<host>/.devboard` with
|
|
87
|
+
`npx @dsdenes/devboard install`. `<host>/.omp/commands`, `<host>/.omp/extensions`, and
|
|
88
|
+
`<host>/AGENTS.md` are real copies from the installed package; no harness path is a symlink.
|
|
89
|
+
The canonical `.devboard/` in this checkout is the source tree and is never an npm install.
|
|
90
|
+
|
|
91
|
+
Every running dashboard checks the newest npm version at startup and every 60 seconds, installs a
|
|
92
|
+
newer artifact, and keeps serving the version it loaded. It reports `versionState` (`running`,
|
|
93
|
+
`installed`, `latest`, `stale`) and shows the reload control while `stale` is true. Only clicking
|
|
94
|
+
that control restarts the server from the installed package and reloads the page. When npm cannot
|
|
95
|
+
resolve or serve the artifact and `DEVBOARD_GITEA_URL` is configured, install and update fall back
|
|
96
|
+
to the Gitea release tarball and its `.sha256` for the same version. A failed download, checksum,
|
|
97
|
+
authentication, or install leaves the installed package and the running server intact; the
|
|
98
|
+
journal records the failure and doctor reports it.
|
|
99
|
+
|
|
100
|
+
Harness-owned issue reporting uses only the configured Gitea API. Without `DEVBOARD_GITEA_URL`
|
|
101
|
+
the report controls are not rendered and `POST /api/feedback` answers HTTP 503 naming that
|
|
102
|
+
variable; no GitHub request is made. Host-forge behavior for GitHub, GitLab, and Gitea origins is
|
|
103
|
+
unchanged.
|
|
104
|
+
|
|
105
|
+
## Publish after every change
|
|
106
|
+
|
|
107
|
+
Publishing is a release, not a restart. Merging into `main` and pushing triggers the
|
|
108
|
+
`.gitea/workflows/devboard-release.yml` workflow, which builds, tests, packs the complete public
|
|
109
|
+
package, publishes `@dsdenes/devboard@<version>` to npmjs, and creates the Gitea release with
|
|
110
|
+
notes plus the same tarball and its `.sha256` as a fallback. Nothing is published under
|
|
111
|
+
`@dsdenes/devboard-harness`.
|
|
112
|
+
|
|
113
|
+
A change is not finished until the workflow for the pushed commit succeeds, the npm package and
|
|
114
|
+
Gitea release assets exist, and the installed hosts have picked the release up. After the push, run
|
|
115
|
+
from the integration checkout — never from a worktree:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cd .devboard
|
|
119
|
+
bun run publish
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`bun run publish` is the verification path. It:
|
|
123
|
+
|
|
124
|
+
1. refuses a worktree, a branch other than `main`, or a checkout ahead of or diverged from its
|
|
125
|
+
canonical remote;
|
|
126
|
+
2. fetches the remote and fast-forwards `main` to it; uncommitted work is never discarded — an
|
|
127
|
+
overlapping local change stops the run and names the blocking paths;
|
|
128
|
+
3. runs `bun install --frozen-lockfile` and `bun run build`, then proves `dist/index.html` is
|
|
129
|
+
newer than every client source and that every referenced asset exists;
|
|
130
|
+
4. waits for the `devboard-release.yml` run of `HEAD` and requires a successful conclusion;
|
|
131
|
+
5. verifies the npm package and the Gitea release's fallback tarball and `.sha256`;
|
|
132
|
+
6. pushes the `github` mirror remote and proves its `main` names the published commit;
|
|
133
|
+
7. reports every running dashboard's running and installed versions without restarting it.
|
|
134
|
+
|
|
135
|
+
Options: `--wait-timeout N` bounds the workflow and propagation wait (default 1800 seconds).
|
|
136
|
+
It never restarts a dashboard, never writes into a host, and exits nonzero when the npm package
|
|
137
|
+
or the release assets are missing. Installed dashboards update themselves; an operator clicks the
|
|
138
|
+
reload control when they want the new code.
|
|
139
|
+
|
|
140
|
+
The integration checkout is advanced and rebuilt after a push; it is never a place for feature
|
|
141
|
+
work. If its uncommitted changes block the fast-forward, preserve them (copy or patch them
|
|
142
|
+
aside, apply the published revision, reapply the changes) and rerun `publish`. Never discard
|
|
143
|
+
them and never bypass the fast-forward.
|
|
144
|
+
|
|
145
|
+
Guardrails:
|
|
146
|
+
|
|
147
|
+
- Never rebuild only a worktree or only the client and call the change done.
|
|
148
|
+
- Never kill, spawn, or relaunch a dashboard server by hand; install, update, and the reload
|
|
149
|
+
control are the only paths.
|
|
150
|
+
- Never copy the harness into a host or symlink a host path to this checkout; install owns the
|
|
151
|
+
host prefix.
|
|
152
|
+
- Stale output is the first suspect, not the last: when the UI behaves like an older revision,
|
|
153
|
+
compare `versionState` and the installed package version before debugging client code.
|
|
154
|
+
- A UI check against a bundle or server older than the changed source is not a passing
|
|
155
|
+
verification.
|
|
156
|
+
|
|
157
|
+
## Wrappers
|
|
158
|
+
|
|
159
|
+
`.devboard/omp/` is canonical: agent commands live in `omp/commands/` and extensions in
|
|
160
|
+
`omp/extensions/`, never in the root `.omp/`. `.devboard/install.sh` copies every file under
|
|
161
|
+
`.devboard/omp/` to the same relative path under `<host>/.omp/`. The host path is an argument or
|
|
162
|
+
is discovered with `git rev-parse --show-toplevel`; the script refuses a host without a
|
|
163
|
+
`.devboard/` directory, refuses to overwrite a differing file without `--force`, never touches
|
|
164
|
+
unrelated files, and is safe to rerun. Edit `.devboard/omp/`, rerun `install.sh`, and never
|
|
165
|
+
hand-edit the installed `.omp/`.
|
|
166
|
+
|
|
167
|
+
## Cross-boundary changes
|
|
168
|
+
|
|
169
|
+
Split, never mix: host-specific detail in host files; generic mechanism in `.devboard/` taking that detail as input. A host name, path, or stack inside `.devboard/` is a bug. A harness dependency outside it is a bug.
|
|
170
|
+
|
|
171
|
+
## Worktree
|
|
172
|
+
|
|
173
|
+
Do all work — host and harness — in an isolated git worktree: `<repo root>/.worktree/<name>`.
|
|
174
|
+
Never outside the repo root. Never edit the opened checkout. One worktree per task; edit, build,
|
|
175
|
+
and test inside it.
|
|
176
|
+
|
|
177
|
+
Done means merged into main **and pushed to the upstream that tracks it**. A local commit, a local
|
|
178
|
+
merge, or a deleted worktree branch is not done. Before deleting the worktree, verify the push:
|
|
179
|
+
`git ls-remote <remote> refs/heads/main` must name the integration commit, and
|
|
180
|
+
`git status --short --branch` must report no commits ahead of the upstream. A local merge that is
|
|
181
|
+
never pushed is a failed task, even when every test passes. If a push is rejected, fetch the new
|
|
182
|
+
tip, integrate it, resolve every conflict yourself, rerun the checks it invalidates, and push
|
|
183
|
+
again. Never finish, hand off, or delete the worktree with local-only changes.
|
|
184
|
+
|
|
185
|
+
After the push, advance the integration checkout to the published revision without discarding
|
|
186
|
+
its uncommitted work, then run `cd .devboard && bun run publish` so every dashboard that serves
|
|
187
|
+
the checkout runs the new bundle and server. Deleting the worktree before the integration
|
|
188
|
+
checkout is published and verified is a failed task.
|
|
189
|
+
|
|
190
|
+
## Line length
|
|
191
|
+
|
|
192
|
+
Never create a line longer than 768 bytes (UTF-8) in a repository text file. Hard-wrap
|
|
193
|
+
prose, prompts, and Markdown list items; do not rely on a later reader re-opening a
|
|
194
|
+
truncated line. Aim for 100 columns or less. Generated files such as lockfiles are exempt;
|
|
195
|
+
never hand-edit them.
|
|
196
|
+
|
|
197
|
+
## Working rules
|
|
198
|
+
|
|
199
|
+
- State mode and edit root before the first edit.
|
|
200
|
+
- Decide which side a new file belongs on first. Two common bugs: a harness artifact in host code, a host fact baked into `.devboard/`.
|
|
201
|
+
- Run harness work from the repo root with the host path passed in or discovered at runtime.
|
|
202
|
+
- In harness mode, never "temporarily" write outside `.devboard/`; fix the layering.
|
|
203
|
+
- Host mode leaves `.devboard/` byte-identical. Harness mode leaves host behavior unchanged except where asked.
|
|
204
|
+
- Pushing is part of the task, not a follow-up. Never stop with a commit or merge that exists only
|
|
205
|
+
locally, even when every test passes and the worktree is clean.
|
|
206
|
+
|
|
207
|
+
## Done
|
|
208
|
+
|
|
209
|
+
- Both modes: done in `.worktree/<name>`, merged to main, pushed to the canonical upstream, and
|
|
210
|
+
verified that the upstream tip is the integrated commit. Only then are the worktree and branch
|
|
211
|
+
removed. Local-only main is never done.
|
|
212
|
+
- Host mode: host build/tests run; `.devboard/` unchanged.
|
|
213
|
+
- Harness mode: the pushed commit's release workflow succeeded and `bun run publish` verified
|
|
214
|
+
the npm package and release assets; installed dashboards report the release through
|
|
215
|
+
`versionState` and restart only from the reload control; the changed entry point runs from the
|
|
216
|
+
repo root against a supplied host path; no absolute paths or host specifics under
|
|
217
|
+
`.devboard/`; nothing created outside `.devboard/`.
|
|
218
|
+
|
|
219
|
+
## Map
|
|
220
|
+
|
|
221
|
+
- `.devboard/` — harness root: interface, toolchain, prompts, extensions, goals, tests,
|
|
222
|
+
`omp/`, `install.sh`, `scripts/publish.ts`.
|
|
223
|
+
- `.devboard/scripts/publish.ts` — verifies `main`, waits for the release workflow, proves the
|
|
224
|
+
npm package and release assets, and reports dashboards without restarting them.
|
|
225
|
+
- `.devboard/scripts/release.ts` — publishes the public package and the Gitea fallback release.
|
|
226
|
+
- `.devboard/templates/host-AGENTS.md` — host contract copied at release time; byte-identical to
|
|
227
|
+
this file.
|
|
228
|
+
- `.gitea/workflows/devboard-release.yml` — CI on the canonical Gitea instance: install, build,
|
|
229
|
+
test, typecheck, and release. GitHub receives a push mirror only.
|
|
230
|
+
- `.devboard/omp/` — canonical agent commands and extensions.
|
|
231
|
+
- `.omp/` — installed copy of `.devboard/omp/`; never edited by hand.
|
|
232
|
+
- `adrs/`, `GOAL.md` — host data at the root; not harness artifacts.
|
|
233
|
+
- `.devboard/AGENTS.md` — harness-internal instructions, when present.
|
|
234
|
+
- `.worktree/` — per-task worktrees; ephemeral.
|
|
235
|
+
- `AGENTS.md` — harness ⇄ host contract; at repo root by design.
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"types": ["bun"],
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"noEmit": true
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"client/**/*.ts",
|
|
15
|
+
"client/**/*.d.ts",
|
|
16
|
+
"client/**/*.svelte",
|
|
17
|
+
"server/**/*.ts",
|
|
18
|
+
"issues/**/*.ts",
|
|
19
|
+
"vite.config.ts"
|
|
20
|
+
]
|
|
21
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [tailwindcss(), svelte()],
|
|
7
|
+
build: {
|
|
8
|
+
outDir: "dist",
|
|
9
|
+
emptyOutDir: true,
|
|
10
|
+
chunkSizeWarningLimit: 12000,
|
|
11
|
+
rollupOptions: {
|
|
12
|
+
output: {
|
|
13
|
+
manualChunks(id: string) {
|
|
14
|
+
if (id.includes("monaco-editor")) return "monaco";
|
|
15
|
+
if (id.includes("@xterm")) return "xterm";
|
|
16
|
+
return undefined;
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { runCleanup } from "./cleanup";
|
|
4
|
+
import { discoverInstances } from "./discovery";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `bun run cleanup -- --dry-run` prints one week's retention plan for every
|
|
8
|
+
* covered state directory; `--once` applies it. The watchdog cycle performs
|
|
9
|
+
* the same sweep; this entry point exists for operators and verification.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const usage = `Usage: bun run cleanup [options]
|
|
13
|
+
|
|
14
|
+
Removes entries older than seven days from every devboard state directory the
|
|
15
|
+
watchdog covers: the canonical checkout, its direct siblings, and live
|
|
16
|
+
dashboards. Journal retention stays with the journal.
|
|
17
|
+
|
|
18
|
+
--once Delete every eligible entry. The default is dry-run.
|
|
19
|
+
--dry-run Print the plan and change nothing.
|
|
20
|
+
--root <path> Canonical repository root. Defaults to this checkout.
|
|
21
|
+
-h, --help Show this text.
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
let once = false;
|
|
26
|
+
let dryRun = false;
|
|
27
|
+
let rootOption: string | undefined;
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
30
|
+
const argument = args[index];
|
|
31
|
+
if (argument === "--once") once = true;
|
|
32
|
+
else if (argument === "--dry-run") dryRun = true;
|
|
33
|
+
else if (argument === "--root") {
|
|
34
|
+
rootOption = args[index + 1];
|
|
35
|
+
index += 1;
|
|
36
|
+
if (rootOption === undefined) {
|
|
37
|
+
console.error("cleanup: --root needs a path");
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
} else if (argument === "-h" || argument === "--help") {
|
|
41
|
+
console.log(usage);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
} else {
|
|
44
|
+
console.error(`cleanup: unknown option ${argument}`);
|
|
45
|
+
console.error(usage);
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (once && dryRun) {
|
|
51
|
+
console.error("cleanup: --once and --dry-run are exclusive");
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function gitTopLevel(): string {
|
|
56
|
+
const result = Bun.spawnSync({
|
|
57
|
+
cmd: ["git", "-C", dirname(import.meta.dir), "rev-parse", "--show-toplevel"],
|
|
58
|
+
stdin: "ignore",
|
|
59
|
+
stdout: "pipe",
|
|
60
|
+
stderr: "pipe",
|
|
61
|
+
});
|
|
62
|
+
const top = result.stdout.toString().trim();
|
|
63
|
+
if (result.exitCode !== 0 || top.length === 0) {
|
|
64
|
+
console.error("cleanup: the harness is not inside a git checkout");
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
return top;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const root = resolve(rootOption ?? gitTopLevel());
|
|
71
|
+
if (!existsSync(join(root, ".devboard"))) {
|
|
72
|
+
console.error(`cleanup: missing prerequisite: ${join(root, ".devboard")} does not exist`);
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const instances = discoverInstances({ selfPids: new Set([process.pid]) });
|
|
77
|
+
const report = runCleanup({ root, instances, dryRun: dryRun || !once });
|
|
78
|
+
const effectiveDryRun = dryRun || !once;
|
|
79
|
+
const verb = effectiveDryRun ? "would remove" : "removed";
|
|
80
|
+
let errors = 0;
|
|
81
|
+
|
|
82
|
+
for (const entry of report.roots) {
|
|
83
|
+
console.log(
|
|
84
|
+
`cleanup: ${entry.stateDir}: ${verb} ${entry.files} file(s), ` +
|
|
85
|
+
`${entry.directories} directory/directories, ${entry.bytes} byte(s)`,
|
|
86
|
+
);
|
|
87
|
+
if (effectiveDryRun) {
|
|
88
|
+
for (const candidate of entry.candidates) {
|
|
89
|
+
console.log(` ${candidate.kind} ${candidate.path} ${candidate.bytes}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const error of entry.errors) {
|
|
93
|
+
errors += 1;
|
|
94
|
+
console.error(`cleanup: ${entry.stateDir}: ${error}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
process.exit(errors === 0 ? 0 : 1);
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
realpathSync,
|
|
6
|
+
rmdirSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
10
|
+
import { ISSUES_DIRECTORY } from "../issues/paths";
|
|
11
|
+
import { JOURNAL_RETENTION_MS } from "../server/journal";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Age-based retention for everything a devboard state directory accumulates.
|
|
15
|
+
* The journal keeps its own rule in `server/journal.ts`; this module sweeps the
|
|
16
|
+
* rest of `.tmp`: run directories, loop logs, legacy dashboard files, and
|
|
17
|
+
* verification scratch. Nothing here decides what a dashboard should keep.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The one-week threshold; the journal uses the same number. */
|
|
21
|
+
export const CLEANUP_RETENTION_MS = JOURNAL_RETENTION_MS;
|
|
22
|
+
|
|
23
|
+
export type CleanupSource = "canonical" | "sibling" | "instance";
|
|
24
|
+
|
|
25
|
+
export interface CoveredStateDir {
|
|
26
|
+
stateDir: string;
|
|
27
|
+
source: CleanupSource;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CleanupCandidate {
|
|
31
|
+
path: string;
|
|
32
|
+
bytes: number;
|
|
33
|
+
kind: "file" | "symlink" | "directory";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CleanupRootReport {
|
|
37
|
+
stateDir: string;
|
|
38
|
+
source: CleanupSource;
|
|
39
|
+
/** Everything eligible under this directory, whether or not it was removed. */
|
|
40
|
+
candidates: CleanupCandidate[];
|
|
41
|
+
/** What the run removed; empty in a dry run. */
|
|
42
|
+
removed: CleanupCandidate[];
|
|
43
|
+
/** Bytes removed, or bytes a dry run would remove. */
|
|
44
|
+
bytes: number;
|
|
45
|
+
files: number;
|
|
46
|
+
directories: number;
|
|
47
|
+
errors: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CleanupReport {
|
|
51
|
+
now: string;
|
|
52
|
+
cutoff: string;
|
|
53
|
+
dryRun: boolean;
|
|
54
|
+
roots: CleanupRootReport[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CleanupInstance {
|
|
58
|
+
stateDir?: string;
|
|
59
|
+
journal: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CleanupRunOptions {
|
|
63
|
+
root: string;
|
|
64
|
+
now?: Date;
|
|
65
|
+
dryRun?: boolean;
|
|
66
|
+
instances?: CleanupInstance[];
|
|
67
|
+
/** Test seam: replace discovery outright. */
|
|
68
|
+
covered?: CoveredStateDir[];
|
|
69
|
+
/** The watchdog run directory of the cycle performing the sweep. */
|
|
70
|
+
watchdogRunDir?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stateDirectory(instance: CleanupInstance): string {
|
|
74
|
+
return instance.stateDir ?? dirname(dirname(instance.journal));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function realDirectory(path: string): string | undefined {
|
|
78
|
+
try {
|
|
79
|
+
if (!lstatSync(path).isDirectory()) return undefined;
|
|
80
|
+
return realpathSync(path);
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The state directories one sweep covers: the canonical harness, every direct
|
|
88
|
+
* sibling `<host>/.devboard/.tmp`, and every live dashboard's real state
|
|
89
|
+
* directory (derived from its journal, never from the serving package).
|
|
90
|
+
* De-duplicated by real path; a symlinked `.tmp` is never followed.
|
|
91
|
+
*/
|
|
92
|
+
export function coveredStateDirs(
|
|
93
|
+
root: string,
|
|
94
|
+
instances: CleanupInstance[] = [],
|
|
95
|
+
): CoveredStateDir[] {
|
|
96
|
+
const covered: CoveredStateDir[] = [];
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
const add = (path: string, source: CleanupSource): void => {
|
|
99
|
+
const real = realDirectory(path);
|
|
100
|
+
if (real === undefined || seen.has(real)) return;
|
|
101
|
+
seen.add(real);
|
|
102
|
+
covered.push({ stateDir: real, source });
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
add(join(root, ".devboard", ".tmp"), "canonical");
|
|
106
|
+
|
|
107
|
+
let names: string[] = [];
|
|
108
|
+
try {
|
|
109
|
+
names = readdirSync(dirname(root)).sort();
|
|
110
|
+
} catch {
|
|
111
|
+
names = [];
|
|
112
|
+
}
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
add(join(dirname(root), name, ".devboard", ".tmp"), "sibling");
|
|
115
|
+
}
|
|
116
|
+
for (const instance of instances) add(stateDirectory(instance), "instance");
|
|
117
|
+
|
|
118
|
+
return covered;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** True when a lock file's name records a pid that is still alive. */
|
|
122
|
+
function liveLegacyLock(path: string): boolean {
|
|
123
|
+
const match = /^devboard-(\d+)\.lock$/.exec(path.slice(path.lastIndexOf(sep) + 1));
|
|
124
|
+
if (match === null) return false;
|
|
125
|
+
return existsSync(`/proc/${match[1]}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function inside(directory: string, path: string): boolean {
|
|
129
|
+
const rest = relative(directory, path);
|
|
130
|
+
return rest.length > 0 && rest !== ".." && !rest.startsWith(`..${sep}`) && !rest.startsWith(sep);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface SweepOptions {
|
|
134
|
+
now: Date;
|
|
135
|
+
dryRun: boolean;
|
|
136
|
+
watchdogRunDir?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* One state directory. Every file or symlink older than the cutoff is a
|
|
141
|
+
* candidate; a directory is one when it would be empty and its own mtime, read
|
|
142
|
+
* before any deletion, is older. Protected paths are never even descended into.
|
|
143
|
+
*/
|
|
144
|
+
export function sweepStateDir(
|
|
145
|
+
stateDir: string,
|
|
146
|
+
options: SweepOptions,
|
|
147
|
+
): CleanupRootReport {
|
|
148
|
+
const cutoff = options.now.getTime() - CLEANUP_RETENTION_MS;
|
|
149
|
+
const errors: string[] = [];
|
|
150
|
+
const candidates: CleanupCandidate[] = [];
|
|
151
|
+
const report: CleanupRootReport = {
|
|
152
|
+
stateDir,
|
|
153
|
+
source: "canonical",
|
|
154
|
+
candidates,
|
|
155
|
+
removed: [],
|
|
156
|
+
bytes: 0,
|
|
157
|
+
files: 0,
|
|
158
|
+
directories: 0,
|
|
159
|
+
errors,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const info = realDirectory(stateDir);
|
|
163
|
+
if (info === undefined) {
|
|
164
|
+
errors.push("not a directory; nothing swept");
|
|
165
|
+
return report;
|
|
166
|
+
}
|
|
167
|
+
const directory = info;
|
|
168
|
+
const protectedPrefixes = [
|
|
169
|
+
join(directory, "state"),
|
|
170
|
+
join(directory, "journal"),
|
|
171
|
+
// Every repo-scoped issues record: state, ledger, runs, activity, control,
|
|
172
|
+
// and the repo-anchored lock a worker holds. A running worker's run
|
|
173
|
+
// directory is therefore never swept.
|
|
174
|
+
join(directory, ISSUES_DIRECTORY),
|
|
175
|
+
// Flow session records: machine state, reviewable artifacts, and the
|
|
176
|
+
// worktree/PR bookkeeping Flow restore and History read. Nothing
|
|
177
|
+
// recreates a swept session, so an untouched one must survive.
|
|
178
|
+
join(directory, "flow"),
|
|
179
|
+
...(options.watchdogRunDir === undefined ? [] : [resolve(options.watchdogRunDir)]),
|
|
180
|
+
];
|
|
181
|
+
const protectedExact = new Set([
|
|
182
|
+
join(directory, "watchdog", "state.json"),
|
|
183
|
+
join(directory, "watchdog", "latest.json"),
|
|
184
|
+
join(directory, "watchdog", "lock"),
|
|
185
|
+
join(directory, "orchestrator", "state.json"),
|
|
186
|
+
join(directory, "orchestrator", "ledger.json"),
|
|
187
|
+
join(directory, "orchestrator", "lock"),
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const isProtected = (path: string): boolean => {
|
|
191
|
+
if (protectedExact.has(path)) return true;
|
|
192
|
+
if (liveLegacyLock(path)) return true;
|
|
193
|
+
return protectedPrefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}${sep}`));
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const visit = (path: string): { removable: boolean; bytes: number } => {
|
|
197
|
+
if (isProtected(path)) return { removable: false, bytes: 0 };
|
|
198
|
+
let entry;
|
|
199
|
+
try {
|
|
200
|
+
entry = lstatSync(path);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
errors.push(`${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
203
|
+
return { removable: false, bytes: 0 };
|
|
204
|
+
}
|
|
205
|
+
if (entry.isSymbolicLink()) {
|
|
206
|
+
if (entry.mtimeMs < cutoff) {
|
|
207
|
+
candidates.push({ path, bytes: 0, kind: "symlink" });
|
|
208
|
+
return { removable: true, bytes: 0 };
|
|
209
|
+
}
|
|
210
|
+
return { removable: false, bytes: 0 };
|
|
211
|
+
}
|
|
212
|
+
if (entry.isFile()) {
|
|
213
|
+
if (entry.mtimeMs < cutoff) {
|
|
214
|
+
candidates.push({ path, bytes: entry.size, kind: "file" });
|
|
215
|
+
return { removable: true, bytes: entry.size };
|
|
216
|
+
}
|
|
217
|
+
return { removable: false, bytes: 0 };
|
|
218
|
+
}
|
|
219
|
+
if (!entry.isDirectory()) return { removable: false, bytes: 0 };
|
|
220
|
+
|
|
221
|
+
let names: string[] = [];
|
|
222
|
+
try {
|
|
223
|
+
names = readdirSync(path).sort();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
errors.push(`${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
226
|
+
return { removable: false, bytes: 0 };
|
|
227
|
+
}
|
|
228
|
+
let survivors = 0;
|
|
229
|
+
let bytes = 0;
|
|
230
|
+
for (const name of names) {
|
|
231
|
+
const child = visit(join(path, name));
|
|
232
|
+
bytes += child.bytes;
|
|
233
|
+
if (!child.removable) survivors += 1;
|
|
234
|
+
}
|
|
235
|
+
if (survivors === 0 && entry.mtimeMs < cutoff) {
|
|
236
|
+
candidates.push({ path, bytes: 0, kind: "directory" });
|
|
237
|
+
return { removable: true, bytes };
|
|
238
|
+
}
|
|
239
|
+
return { removable: false, bytes };
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
let top: string[] = [];
|
|
243
|
+
try {
|
|
244
|
+
top = readdirSync(directory).sort();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
errors.push(`${directory}: ${error instanceof Error ? error.message : String(error)}`);
|
|
247
|
+
return report;
|
|
248
|
+
}
|
|
249
|
+
for (const name of top) visit(join(directory, name));
|
|
250
|
+
|
|
251
|
+
let bytes = 0;
|
|
252
|
+
for (const candidate of candidates) bytes += candidate.bytes;
|
|
253
|
+
if (options.dryRun) {
|
|
254
|
+
report.bytes = bytes;
|
|
255
|
+
report.files = candidates.filter((candidate) => candidate.kind !== "directory").length;
|
|
256
|
+
report.directories = candidates.filter((candidate) => candidate.kind === "directory").length;
|
|
257
|
+
return report;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const candidate of candidates) {
|
|
261
|
+
if (!inside(directory, candidate.path)) {
|
|
262
|
+
errors.push(`${candidate.path}: refuses to delete outside ${directory}`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
if (candidate.kind === "directory") rmdirSync(candidate.path);
|
|
267
|
+
else unlinkSync(candidate.path);
|
|
268
|
+
report.removed.push(candidate);
|
|
269
|
+
report.bytes += candidate.bytes;
|
|
270
|
+
if (candidate.kind === "directory") report.directories += 1;
|
|
271
|
+
else report.files += 1;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
errors.push(
|
|
274
|
+
`${candidate.path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return report;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Finds every covered state directory and sweeps each one. */
|
|
282
|
+
export function runCleanup(options: CleanupRunOptions): CleanupReport {
|
|
283
|
+
const now = options.now ?? new Date();
|
|
284
|
+
const dryRun = options.dryRun === true;
|
|
285
|
+
const covered = options.covered ?? coveredStateDirs(options.root, options.instances ?? []);
|
|
286
|
+
const roots = covered.map((entry) => {
|
|
287
|
+
const report = sweepStateDir(entry.stateDir, {
|
|
288
|
+
now,
|
|
289
|
+
dryRun,
|
|
290
|
+
...(options.watchdogRunDir === undefined ? {} : { watchdogRunDir: options.watchdogRunDir }),
|
|
291
|
+
});
|
|
292
|
+
report.source = entry.source;
|
|
293
|
+
return report;
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
now: now.toISOString(),
|
|
297
|
+
cutoff: new Date(now.getTime() - CLEANUP_RETENTION_MS).toISOString(),
|
|
298
|
+
dryRun,
|
|
299
|
+
roots,
|
|
300
|
+
};
|
|
301
|
+
}
|