@dsdenes/devboard 0.1.560 → 0.1.563
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 +729 -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/README.md
CHANGED
|
@@ -1,34 +1,2505 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Devboard
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Local browser interface for a repository's goal, Flow implementation, and Discover artifacts.
|
|
4
|
+
It renders one full-viewport, dark-first administration workspace with an activity bar, a
|
|
5
|
+
contextual sidebar, one workspace page per mode, resizable split panes, Monaco editors, xterm
|
|
6
|
+
terminals, a command palette, and a status bar. Every mode is a client-side view over the existing
|
|
7
|
+
WebSocket contract; the server still owns all work and durable state.
|
|
8
|
+
|
|
9
|
+
## Interface
|
|
10
|
+
|
|
11
|
+
The interface is a desktop tool. It requires a viewport of at least 1024×720 CSS pixels. Below
|
|
12
|
+
either threshold the shell is not rendered: a notice carrying
|
|
13
|
+
`data-testid="unsupported-viewport"` states the minimum instead, so the application never
|
|
14
|
+
degrades into a clipped mobile layout.
|
|
15
|
+
|
|
16
|
+
### Application shell
|
|
17
|
+
|
|
18
|
+
`data-testid="app-shell"` fills the viewport exactly. It contains, in order:
|
|
19
|
+
|
|
20
|
+
|Region|Test hook|Role|
|
|
21
|
+
|---|---|---|
|
|
22
|
+
|Mode rail|`activity-bar`|Three labeled mode tabs and their Lucide icons|
|
|
23
|
+
|Sidebar|`sidebar`|Contextual navigation for the active mode|
|
|
24
|
+
|Workspace|`workspace`|The open surface; absorbs all remaining space|
|
|
25
|
+
|Status bar|`status-bar`|Connection, project name, lane count, issues state, watchdog, versions, reload|
|
|
26
|
+
|
|
27
|
+
The status bar stays visible while panes resize, and the document itself never scrolls
|
|
28
|
+
vertically or horizontally. Each mode's panel keeps its scroll position, focus, open page, and
|
|
29
|
+
unsaved editor text while another mode is shown.
|
|
30
|
+
|
|
31
|
+
### Mode rail
|
|
32
|
+
|
|
33
|
+
The 72px rail presents exactly four modes in this order as `role="tab"` controls:
|
|
34
|
+
`data-testid="mode-goal"`, `data-testid="mode-issues"`, `data-testid="mode-flow"`, and
|
|
35
|
+
`data-testid="mode-discover"`. Each is a 64×52px control with a Lucide icon above a visible 10px
|
|
36
|
+
uppercase label (`Goal`, `Issues`, `Flow`, `Discover`), an accessible name that also names the
|
|
37
|
+
shortcut, `aria-selected`, and `aria-controls` naming its workspace panel. The labels never
|
|
38
|
+
collapse to icon-only and never overflow the tab at the 1024×720 boundary. A fresh profile starts
|
|
39
|
+
on Goal. Clicking a tab or pressing Alt+1, Alt+2, Alt+3, or Alt+4 changes only which mounted view
|
|
40
|
+
is shown; it never sends a WebSocket frame.
|
|
41
|
+
|
|
42
|
+
The sidebar has no header: it starts with the active mode's navigation, because the rail already
|
|
43
|
+
names the mode and the status bar names the project.
|
|
44
|
+
|
|
45
|
+
### Sidebar
|
|
46
|
+
|
|
47
|
+
The sidebar is always visible and has no hide control or collapse shortcut. It is initially 300
|
|
48
|
+
CSS px wide, constrained to 200–600 CSS px, and resized by dragging its separator or pressing the
|
|
49
|
+
arrow keys with that separator focused. Double-clicking the separator restores the default width.
|
|
50
|
+
The width survives a reload.
|
|
51
|
+
|
|
52
|
+
- **Goal** lists the goal workspace surfaces, a **Skills** entry that discloses every prepared
|
|
53
|
+
skill, the pending ADR records (title, status, next marker, file), and the lifecycle lanes. The
|
|
54
|
+
ADR records group is absent while no record is pending.
|
|
55
|
+
- **Flow** lists the workspace surfaces — overview, decision board, steering, and history — the
|
|
56
|
+
session and PR state, widgets, changed files, checkpoints, and blockers. The overview entry
|
|
57
|
+
carries the goal's `unsaved` marker. Blockers open the overview at the blocker they name.
|
|
58
|
+
- **Discover** lists the overview and glossary sections, the five skills under their own group,
|
|
59
|
+
and warnings only while at least one exists. The glossary entry carries the published term
|
|
60
|
+
count. A skill carries a `generated` state silently and shows `edited` or `missing` as trailing
|
|
61
|
+
meta, so an exception is visible before the page opens. The Glossary entry is a plain section
|
|
62
|
+
link: it has no disclosure control and no nested per-term menu.
|
|
63
|
+
|
|
64
|
+
Selecting an entry opens or focuses its workspace surface. Informational entries open the
|
|
65
|
+
surface that holds the same facts. The Flow overview opens with the flow-goal editor, which takes
|
|
66
|
+
the full page height; while a goal interview is running the editor collapses to its header and the
|
|
67
|
+
interview takes the next block. The overview ends with the implementation activity console, which
|
|
68
|
+
also takes a full page height so agent output never shrinks to a thumbnail.
|
|
69
|
+
|
|
70
|
+
### Workspace page
|
|
71
|
+
|
|
72
|
+
Each mode shows exactly one page at a time and renders no tab strip. Opening an ADR, a Flow
|
|
73
|
+
widget, changed file, or checkpoint, or a Discover skill replaces the visible page; the previous
|
|
74
|
+
selection is reachable again from the sidebar or the palette. Close controls inside a page return
|
|
75
|
+
to an empty page, and a dirty page closes only after the confirmation dialog restores or discards
|
|
76
|
+
the draft.
|
|
77
|
+
|
|
78
|
+
A reload restores the active mode, each mode's open page, sidebar and split sizes, and theme. All
|
|
79
|
+
of that state lives in `localStorage`; none of it is sent to the server.
|
|
80
|
+
|
|
81
|
+
### Split panes
|
|
82
|
+
|
|
83
|
+
PaneForge groups provide the sidebar/workspace split and the workspace/detail split used by Flow
|
|
84
|
+
widgets and diffs and by the Discover skill detail. Separators are focusable, carry
|
|
85
|
+
`role="separator"` with `aria-orientation`, `aria-valuemin`, `aria-valuemax`, and `aria-valuenow`,
|
|
86
|
+
resize by drag and arrow keys, reset to their defaults on double-click, keep both panes at least
|
|
87
|
+
160 CSS px wide, persist their sizes, and never cover the status bar.
|
|
88
|
+
|
|
89
|
+
### Command palette
|
|
90
|
+
|
|
91
|
+
Ctrl/Cmd+K opens `data-testid="command-palette"`. It has a search input
|
|
92
|
+
(`command-palette-input`) and command items (`command-item`), filters as you type, moves with
|
|
93
|
+
ArrowUp/ArrowDown/Home/End, runs with Enter, closes with Escape, and restores focus to the
|
|
94
|
+
previously focused element. Every actionable control carries a stable `data-command`, and the
|
|
95
|
+
palette exposes the same command ids; disabled commands show the same reason as their control.
|
|
96
|
+
Commands that need text focus the relevant input rather than inventing a value.
|
|
97
|
+
|
|
98
|
+
The palette covers navigation, opening every record, lane, widget, file, checkpoint, term, and
|
|
99
|
+
skill, lifecycle start/pause/resume/stop, Goal commands, Flow goal and decision editing, Flow
|
|
100
|
+
start/stop and PR actions, Discover start/stop and skill editing, the theme toggle, reset layout,
|
|
101
|
+
reload when stale, and the `Report UI issue`, `Remove element`, and `Redesign element` actions
|
|
102
|
+
(`app.report`, `app.remove`, `app.redesign`, and `app.report.cancel`).
|
|
103
|
+
|
|
104
|
+
### Keyboard shortcuts
|
|
105
|
+
|
|
106
|
+
|Shortcut|Action|
|
|
107
|
+
|---|---|
|
|
108
|
+
|Ctrl/Cmd+K|Open the command palette|
|
|
109
|
+
|Alt+1 / Alt+2 / Alt+3 / Alt+4|Switch to Goal / Issues / Flow / Discover|
|
|
110
|
+
|Ctrl/Cmd+S|Save the focused editor when it has unsaved changes|
|
|
111
|
+
|Escape|Close the palette, dialogs, and overlays|
|
|
112
|
+
|
|
113
|
+
Shortcuts are inert while text is being typed, except palette, save, and Escape. Every shortcut
|
|
114
|
+
is listed in the palette or in the control's tooltip. Focus rings are always visible; dialogs
|
|
115
|
+
trap focus and restore it on close.
|
|
116
|
+
|
|
117
|
+
### Theme
|
|
118
|
+
|
|
119
|
+
A fresh profile starts dark with `data-theme="dark"` on the root element, regardless of the OS
|
|
120
|
+
preference. The status-bar control and the palette toggle between dark and light; the choice
|
|
121
|
+
persists and an inline head script applies it before the first paint, so there is no wrong-theme
|
|
122
|
+
flash. Both themes define every token used by the shell, tables, editors, terminals, diffs,
|
|
123
|
+
status indicators, and overlays. Body text and muted text meet 4.5:1 contrast; interactive
|
|
124
|
+
borders and focus rings meet 3:1; no state is conveyed by color alone. `prefers-reduced-motion:
|
|
125
|
+
reduce` disables nonessential transitions.
|
|
126
|
+
|
|
127
|
+
### Dense data views
|
|
128
|
+
|
|
129
|
+
TanStack Table renders the ADR record list and the Discover command, testing, architecture,
|
|
130
|
+
dependency, unknown-field, and glossary tables. The ADR table keeps title, status,
|
|
131
|
+
pending or resolved state, next marker, file, and decision summary; selecting a row opens its
|
|
132
|
+
detail page without navigating away. Body and table text is 12–14 CSS px, paths, code, diffs,
|
|
133
|
+
terminal output, and IDs are monospace, one-line data rows are at most 32 CSS px tall, and wrapped
|
|
134
|
+
data rows stay within 120 CSS px. The shell uses
|
|
135
|
+
the full viewport with no application max-width.
|
|
136
|
+
|
|
137
|
+
### Editors and terminals
|
|
138
|
+
|
|
139
|
+
Monaco Editor provides the Goal document (`goal-text`), Flow goal (`flow-goal-text`), Flow
|
|
140
|
+
decision board (`flow-decisions`), and Discover skill editor (`discover-skill-text`). Each
|
|
141
|
+
supports multiline Markdown or plain-text editing, its Save control, Ctrl/Cmd+S, a dirty marker,
|
|
142
|
+
and an accessible label; a save sends the editor's exact text. The Goal document page fills the
|
|
143
|
+
workspace: the editor takes every remaining pixel and the Revision guidance field is a tall
|
|
144
|
+
multiline text area pinned to the page bottom. Flow diffs remain rendered by
|
|
145
|
+
`diff2html`; the client never parses unified diff hunks.
|
|
146
|
+
|
|
147
|
+
xterm.js renders read-only agent output for Goal current activity and history, lane current
|
|
148
|
+
activity and history, Flow goal creation, implementation activity, conversation activity, and
|
|
149
|
+
Discover activity. Terminals preserve ANSI output, are selectable and copyable, auto-scroll
|
|
150
|
+
unless the developer scrolls away, keep a bounded scrollback, and expose their full buffer to
|
|
151
|
+
assistive technology.
|
|
152
|
+
|
|
153
|
+
### Notifications and status bar
|
|
154
|
+
|
|
155
|
+
Server errors and notices appear in the notification region (`data-testid="notifications"`) with
|
|
156
|
+
severity, text, and dismissal, and remain in a history opened from the status bar. Every error and
|
|
157
|
+
warning card also carries `File issue`: it files that notification's text and card context as a UI
|
|
158
|
+
issue through `/api/feedback` at once, then replaces itself with the issue URL. Notifications
|
|
159
|
+
never replace mode content or steal focus, and several arrivals are all retained. Flow blockers
|
|
160
|
+
and verification results also remain in their mode context.
|
|
161
|
+
|
|
162
|
+
The 26px status bar is two groups. The left group states the WebSocket connection and the
|
|
163
|
+
project's own name (the last path segment, with a folder mark and the full path in its tooltip),
|
|
164
|
+
separated by a hairline rule. The right group states the running lane count, the issues worker
|
|
165
|
+
state with a state dot, the watchdog's last action and age, and the running and installed
|
|
166
|
+
versions; each metric carries a 10px uppercase label so its meaning never rests on color. When
|
|
167
|
+
`versionState.stale` is true it shows the existing reload control (`reload-devboard`), whose
|
|
168
|
+
accessible name and title name both versions; reload behavior is unchanged. A mode states its
|
|
169
|
+
status in its sidebar and workspace, never a second time in the status bar, except the issues
|
|
170
|
+
worker, which is detached from every dashboard and reports through `issues-indicator`. The theme
|
|
171
|
+
toggle and the notification indicator live in the activity bar's action group, not the status
|
|
172
|
+
bar.
|
|
173
|
+
|
|
174
|
+
### Verifying the interface
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
cd .devboard
|
|
178
|
+
bun run verify:interface
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The script builds the client, seeds a fixture host under `.devboard/.tmp` with eight ADRs and
|
|
182
|
+
real Discover artifacts produced by the stub ACP peer, starts a source dashboard, launches
|
|
183
|
+
headless Chrome with a profile under the same tree, and asserts the shell, modes, sidebar,
|
|
184
|
+
pages, splits, palette, shortcuts, theme and first paint, layout persistence and reset, the
|
|
185
|
+
1024×720 boundary, dense tables, Monaco editors, xterm terminals, contrast, reduced motion,
|
|
186
|
+
notifications, status bar, preserved test hooks, and the absence of exceptions, console errors,
|
|
187
|
+
failed requests, and overflow. It writes screenshots under `.devboard/.tmp/interface/`.
|
|
188
|
+
|
|
189
|
+
## Stack
|
|
190
|
+
|
|
191
|
+
- Backend: TypeScript on Bun, serving the built client and one WebSocket at `/ws`. Each run
|
|
192
|
+
spawns one `omp acp` subprocess over stdio.
|
|
193
|
+
- Client: Svelte 5 built with Vite, with shadcn-svelte components and its Tailwind CSS 4 setup,
|
|
194
|
+
Bits UI dialogs, tooltips, and popovers, PaneForge split panes, TanStack Table data grids,
|
|
195
|
+
Monaco Editor, xterm.js, and Lucide icons. All workers and assets are bundled locally; the
|
|
196
|
+
page makes no network request beyond its WebSocket.
|
|
197
|
+
- Density: 13 px body text with 12 px tables and monospace paths, code, diffs, terminals, and IDs.
|
|
198
|
+
- Records: read from `adrs/` through adrkit on every `listRecords` command and after every run, so
|
|
199
|
+
files added, removed, or resolved while the server runs appear without a restart. Only records
|
|
200
|
+
adrkit accepts are listed. Each record carries the first paragraph of its `Decision` section as
|
|
201
|
+
a short summary.
|
|
202
|
+
- State: the dashboard's server-owned state is written atomically to
|
|
203
|
+
`<stateDir>/state/<24-hex hash of REPO_DIR>.json` (default `stateDir` is `<harness>/.tmp`), so it
|
|
204
|
+
survives a backend restart without leaking between repositories that share one harness install.
|
|
205
|
+
See [Durable state](#durable-state).
|
|
206
|
+
|
|
207
|
+
## Install
|
|
208
|
+
|
|
209
|
+
The harness is the public npm package `@dsdenes/devboard`. One tarball carries the launcher, the
|
|
210
|
+
server, the client sources and built bundle, the prompts, the omp wrappers and extensions, the
|
|
211
|
+
templates, and the installer. `install`, `update`, and the running dashboard resolve versions and
|
|
212
|
+
artifacts from npm; no local checkout, running dashboard, or release tarball is required. When npm
|
|
213
|
+
cannot resolve or serve the artifact and `DEVBOARD_GITEA_URL` is configured, the canonical Gitea
|
|
214
|
+
release tarball and its `.sha256` are used for the same version. GitHub carries only a push mirror
|
|
215
|
+
of this repository.
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npx @dsdenes/devboard install # install or refresh this host
|
|
219
|
+
npx @dsdenes/devboard update # install the newest npm version; do not restart
|
|
220
|
+
npx @dsdenes/devboard doctor # one PASS or FAIL line per check
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`install` writes a real npm prefix at `<host>/.devboard`, replacing `.devboard`,
|
|
224
|
+
`.omp/commands`, and `.omp/extensions` symlinks with real directories that hold the installed
|
|
225
|
+
package's files. It keeps `.devboard/.tmp`, copies `templates/host-AGENTS.md` to
|
|
226
|
+
`<host>/AGENTS.md` when that file is a symlink or absent (an existing regular file is untouched
|
|
227
|
+
and reported as drift), adds the three ignored paths to `.gitignore`, and leaves exactly one
|
|
228
|
+
dashboard running from the installed package. A reinstall keeps the version and `.tmp`; `update`
|
|
229
|
+
never starts, stops, or restarts the dashboard.
|
|
230
|
+
|
|
231
|
+
`doctor` checks Node 20+, Bun, npm, the git root, npm's version and artifact metadata (or the
|
|
232
|
+
configured Gitea fallback and its digest), the installed package, the installed version against
|
|
233
|
+
the newest version, `omp` and its authenticated probe, wrapper byte-equality, `AGENTS.md`, the
|
|
234
|
+
dashboard's package and HTTP response, a non-symlinked `.devboard`, and a recorded update failure.
|
|
235
|
+
Drift in a regular `AGENTS.md` is reported without failing the check; every other mismatch exits
|
|
236
|
+
nonzero. Gitea is never required: with no address configured the checks consult npm only.
|
|
237
|
+
|
|
238
|
+
`--host <path>` targets another repository. `DEVBOARD_RELEASE_DIR` points the launcher and the
|
|
239
|
+
server at a local directory of tarballs for offline verification and CI fixtures. `DEVBOARD_NPM`
|
|
240
|
+
selects a different npm executable for those fixtures.
|
|
241
|
+
|
|
242
|
+
`install.sh` still copies the agent wrappers under `omp/` into an existing host:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
./install.sh /path/to/host
|
|
246
|
+
./install.sh --force /path/to/host
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The host must already contain a `.devboard/` directory. Every file under `omp/` is copied to the
|
|
250
|
+
same relative path under `<host>/.omp/`; matching files are left alone, unrelated host files are
|
|
251
|
+
never removed, and a differing file stops the run unless `--force` is given.
|
|
252
|
+
|
|
253
|
+
The global `/setup-devboard` command runs the launcher against the invoking session's working
|
|
254
|
+
directory. It lives here as `omp/commands/setup-devboard.md` and is linked into the user-level
|
|
255
|
+
command directory (`$HOME/.omp/agent/commands/setup-devboard.md`) so every session sees it.
|
|
256
|
+
|
|
257
|
+
### Model selection
|
|
258
|
+
|
|
259
|
+
`omp/config.yml` is installed as `<host>/.omp/config.yml`, the host's omp project settings.
|
|
260
|
+
Every harness peer starts its ACP process with `<REPO_DIR>` as the working directory, so each
|
|
261
|
+
one, and the interactive agent started in that repository, uses the same policy:
|
|
262
|
+
|
|
263
|
+
|Setting|Value|Meaning|
|
|
264
|
+
|---|---|---|
|
|
265
|
+
|`modelRoles`|`opencode-go/deepseek-v4.1-flash` for `default`, `plan`, `smol`, `slow`, and `task`|Model for every chat role the harness uses.|
|
|
266
|
+
|
|
267
|
+
The model is served by OpenCode Go and reads `OPENCODE_API_KEY`. No fallback chain is
|
|
268
|
+
configured. Editing `omp/config.yml` here and rerunning `install.sh` changes the installed copy.
|
|
269
|
+
|
|
270
|
+
## Build and run
|
|
271
|
+
|
|
272
|
+
Run the interface from this directory:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
cd .devboard
|
|
276
|
+
bun install
|
|
277
|
+
bun run build
|
|
278
|
+
bun run check
|
|
279
|
+
bunx tsc --noEmit
|
|
280
|
+
ADRS_DIR="<host>/adrs" bun run start
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`bun run check` type-checks every `.svelte` and `.ts` file under `client/` and `server/`;
|
|
284
|
+
`bunx tsc --noEmit` type-checks the TypeScript sources.
|
|
285
|
+
|
|
286
|
+
Open `http://127.0.0.1:8080/`. The server binds to `127.0.0.1` by default.
|
|
287
|
+
|
|
288
|
+
## Decision records
|
|
289
|
+
|
|
290
|
+
`adrs/` is an adrkit corpus. Every record opens with YAML frontmatter — `schemaVersion: 0.2.0`,
|
|
291
|
+
`id`, `title`, `status`, `date`, `deciders`, and `affects` — and keeps its markdown body below the
|
|
292
|
+
closing fence. The server reads the corpus through `@adrkit/core`'s `lintCorpus`, so a `.md` file
|
|
293
|
+
whose frontmatter adrkit rejects never appears in the listing, in `pending`, or in `next`.
|
|
294
|
+
|
|
295
|
+
A record is pending when its frontmatter `status` is `draft` or `proposed`; every other status is
|
|
296
|
+
resolved. Records are ordered by file name, `next` is the first pending file in that order, and the
|
|
297
|
+
interface renders the status with a capital first letter.
|
|
298
|
+
|
|
299
|
+
Run the corpus tooling from `.devboard`:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
bun run adr:lint # adr lint --dir ../adrs
|
|
303
|
+
bun run adr:graph # adr graph --dir ../adrs
|
|
304
|
+
bun run adr:queue # adr queue --dir ../adrs
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`.github/workflows/adr.yml` installs the harness dependencies and runs `bun run adr:lint` on every
|
|
308
|
+
push to `main` and every pull request. On a pull request it also runs the `mbeacom/adrkit` action
|
|
309
|
+
with `dir: adrs`, which comments the pull request's governing decisions.
|
|
310
|
+
|
|
311
|
+
`.devboard/omp/mcp.json` declares a stdio MCP server named `adrkit` that runs
|
|
312
|
+
`bunx @adrkit/mcp --cwd . --dir adrs`. It answers `tools/list` with `search_decisions`,
|
|
313
|
+
`get_decision`, `get_decision_context`, and `list_superseded`.
|
|
314
|
+
|
|
315
|
+
## Durable state
|
|
316
|
+
|
|
317
|
+
The dashboard keeps the state that drives the page in one file per host repository and restores
|
|
318
|
+
it from a new process after a graceful stop, a launcher restart, or a `SIGKILL`. The file is
|
|
319
|
+
`<stateDir>/state/<key>.json`, where `key` is the 24-hex SHA-256 prefix of the resolved
|
|
320
|
+
`REPO_DIR` (the same key the journal uses) and `stateDir` is the harness state directory,
|
|
321
|
+
`<harness>/.tmp` by default and overridable with `STATE_DIR`. Two repositories that share one
|
|
322
|
+
harness therefore never read or overwrite each other's lifecycle, lane, pass, or foreground
|
|
323
|
+
state, and a repository with no stored state starts at pass 1 with no lanes and no
|
|
324
|
+
`lifecycleState` until a run starts. The file is never committed, never copied between
|
|
325
|
+
repositories, and never written outside the repository root.
|
|
326
|
+
|
|
327
|
+
Every change a client can observe is written before the event that announces it is broadcast.
|
|
328
|
+
Writes replace the file atomically (temporary file plus rename), so the last complete state stays
|
|
329
|
+
readable at every instant, a kill during a write leaves the previous state, and a client never
|
|
330
|
+
sees an event that the replacement process cannot replay. The stored state covers the lifecycle
|
|
331
|
+
stage, pass, work, and paused flag; every lane with its id, kind, epic or record, attempt, status,
|
|
332
|
+
activity, question, blocked/failure bookkeeping, and remaining retention; epic attempts,
|
|
333
|
+
`mainAtFailure`, status, and the parked and retrying sets; the last reported lifecycle state
|
|
334
|
+
including `finished`; the manual ADR sequence; goal creation; goal revision with its guidance;
|
|
335
|
+
skills preparation with the pre-run rollback snapshot; an interrupted implementation reset; and an
|
|
336
|
+
accepted `whenIdle` restart. Browser-only state (open page per mode, drafts, layout, theme,
|
|
337
|
+
notifications, connection status) and full ACP transcripts, raw stderr, and secrets stay out of the file.
|
|
338
|
+
|
|
339
|
+
Restored activity is bounded per foreground run and per lane at the first of 1,000 entries or
|
|
340
|
+
1 MiB of UTF-8 text. Within that bound no line is lost across a kill; beyond it the newest
|
|
341
|
+
entries are kept and one marker entry stating that earlier activity was trimmed precedes them.
|
|
342
|
+
The total serialized state for one repository stays under 32 MiB with 20 concurrent lanes at
|
|
343
|
+
full bounds.
|
|
344
|
+
|
|
345
|
+
A restored lifecycle that was advancing re-spawns every incomplete lane with the same id, kind,
|
|
346
|
+
epic or record, attempt, recorded activity (as an exact prefix), and lifecycle position:
|
|
347
|
+
implementation lanes receive `/implement-next-epic <epic>`, a generation lane receives
|
|
348
|
+
`/epic-next`, and an ADR lane receives the same `agentPrompt(adrDir, file)`. The replacement
|
|
349
|
+
never creates a second lane for an epic that still owns one, `laneSequence` continues above the
|
|
350
|
+
highest restored id, a retrying epic retries after the configured backoff, and a parked epic
|
|
351
|
+
starts no peer until `main` advances past its recorded revision. A queued generation pass runs
|
|
352
|
+
exactly once after the restored in-flight pass. A run that was paused stays paused: its lanes
|
|
353
|
+
replay status `paused`, no stage transition, tracker read, retry, generation pass, or new peer
|
|
354
|
+
happens until `resumeLifecycle`, and the resume then re-spawns each interrupted lane.
|
|
355
|
+
|
|
356
|
+
Foreground work that was live at death resumes with its original input. A manual ADR sequence
|
|
357
|
+
continues from its persisted record, re-spawning that record only while it is still pending
|
|
358
|
+
and otherwise advancing to the next pending record; goal creation re-spawns `/goal-outcome`; a
|
|
359
|
+
revision re-spawns the same prompt including its original guidance; skills preparation re-spawns
|
|
360
|
+
`/prepare-skills` after restoring the same pre-run snapshot, so a resumed preparation that ends in
|
|
361
|
+
failure restores the exact bytes captured before the original run. Restored activity is kept and
|
|
362
|
+
new lines append. A pending lane or foreground question is replayed to every connecting client
|
|
363
|
+
after the restored activity; answering a restored question that the replacement peer has not
|
|
364
|
+
re-issued is refused with `The question is no longer pending.` and the page updates without a
|
|
365
|
+
reload. Before re-spawning a unit, the replacement ends any recorded ACP child of the killed
|
|
366
|
+
process whose process table entry proves it is that peer; it never signals a process it cannot
|
|
367
|
+
prove.
|
|
368
|
+
|
|
369
|
+
A reset that was active at death is never re-run. The replacement performs no git commit, push,
|
|
370
|
+
deletion, or staging, emits `implementationResetStopped` with `completed: false` and one `error`
|
|
371
|
+
naming the interruption, and leaves the working tree and refs as the killed process left them.
|
|
372
|
+
After a successful reset the stored state holds pass 1 and the idle `goal` stage, so a restart
|
|
373
|
+
restores neither the old lanes nor the old pass. An accepted `restartServer` with
|
|
374
|
+
`whenIdle: true` survives the process death and is honored exactly once after the restored work
|
|
375
|
+
becomes idle; the pending flag is cleared before the launcher is invoked, so the replacement
|
|
376
|
+
never restarts itself again for the same request. With no launcher configured, the existing
|
|
377
|
+
unavailable-restart error is reported and the flag is cleared.
|
|
378
|
+
|
|
379
|
+
A closed or failed lane whose retention window expired while the server was down is not
|
|
380
|
+
replayed; the remaining window is honored after a restart. An unreadable, truncated, or
|
|
381
|
+
unsupported-version state file never prevents startup: the server renames it aside inside
|
|
382
|
+
`stateDir` (`.damaged-<timestamp>`), starts from the initial state, and reports exactly one
|
|
383
|
+
`error` to the first connecting client and on stderr, leaving the preserved bytes in place. A
|
|
384
|
+
read-only state directory never stops the server: it keeps serving from memory and reports one
|
|
385
|
+
`error` on stderr and in the journal.
|
|
386
|
+
|
|
387
|
+
## Flow Mode
|
|
388
|
+
|
|
389
|
+
The activity bar carries four client-only modes, `Goal`, `Issues`, `Flow`, and `Discover`
|
|
390
|
+
(`data-testid="mode-goal"`, `data-testid="mode-flow"`, and `data-testid="mode-discover"`).
|
|
391
|
+
Switching is a view change only: it sends no WebSocket frame and
|
|
392
|
+
starts, stops, pauses, or resumes no work in any mode. Goal Mode is the goal, ADR, epic,
|
|
393
|
+
lifecycle, and skills interface described above. Flow Mode is one implementation session that the
|
|
394
|
+
developer steers while it works: the developer interviews for a goal, starts an implementation
|
|
395
|
+
agent in an isolated worktree, reviews the changes as color-coded widgets, discusses a widget with
|
|
396
|
+
a read-only agent, and writes steering notes and decisions that the agent consumes between
|
|
397
|
+
checkpoints.
|
|
398
|
+
|
|
399
|
+
### One mode at a time
|
|
400
|
+
|
|
401
|
+
Both modes may be viewed at any time, but only one may hold the checkout. Starting Flow work —
|
|
402
|
+
creating its goal, starting its implementation — is refused with one `error` while any Goal Mode
|
|
403
|
+
work is live; starting a Goal Mode run, lifecycle, goal creation or revision, or skills
|
|
404
|
+
preparation is refused with one `error` while any Flow work is live. Merging a Flow pull request
|
|
405
|
+
is likewise refused while Goal Mode work is live. Flow work means a goal interview, an
|
|
406
|
+
implementation turn, a change analysis, a widget conversation, or pull-request work. View
|
|
407
|
+
switching and read-only commands stay available in both cases. Goal Mode's implementation reset is
|
|
408
|
+
refused while a Flow session exists whose worktree or pull request is not merged.
|
|
409
|
+
|
|
410
|
+
### Commands and events
|
|
411
|
+
|
|
412
|
+
Flow Mode uses one WebSocket at `/ws`. The client sends `createFlowGoal`,
|
|
413
|
+
`startFlowImplementation`, `stopFlowImplementation`, `saveFlowGoal`, `retryFlowPr`,
|
|
414
|
+
`markFlowPrReady`, `mergeFlowPr`, `sendFlowSteering`, `saveFlowDecisions`, `acceptFlowWidget`,
|
|
415
|
+
`refuseFlowWidget`, `openFlowWidget`, `closeFlowWidget`, `sendFlowConversation`,
|
|
416
|
+
`sendFlowConclusion`, `dismissFlowBlocker`, and `flowAnswer` (with `target` naming the goal,
|
|
417
|
+
implementation, or conversation question). The server broadcasts `flowSnapshot`,
|
|
418
|
+
`flowGoalStarted`, `flowGoalStopped`, `flowImplementationStarted`, `flowStopped`, `flowActivity`,
|
|
419
|
+
`flowQuestion`, `flowFiles`, `flowWidgets`, `flowSteering`, `flowDecisions`, `flowCheckpoints`,
|
|
420
|
+
`flowVerification`, `flowBlockers`, `flowPr`, `flowNotice`, `flowError`,
|
|
421
|
+
`flowConversationStarted`, `flowConversationActivity`, `flowConversationQuestion`,
|
|
422
|
+
`flowConversationMessage`, and `flowConversationStopped`. A connecting client with a Flow session
|
|
423
|
+
receives one `flowSnapshot` holding the whole state followed by every section event, so a reload
|
|
424
|
+
never loses a widget, checkpoint, verification result, blocker, steering note, decision board, or
|
|
425
|
+
pull-request state; a server with no Flow work sends nothing and the view starts empty.
|
|
426
|
+
|
|
427
|
+
Flow's four agent commands are `/flow-goal <session-dir>`, `/flow-implement <session-dir>`,
|
|
428
|
+
`/flow-change <session-dir>`, and `/flow-widget <session-dir> <widget-id>`. The server spawns each
|
|
429
|
+
as its own ACP peer; the goal interview runs in the repository root, the implementation runs with
|
|
430
|
+
the isolated worktree as its working directory, and the change analyst and widget conversation are
|
|
431
|
+
read-only.
|
|
432
|
+
|
|
433
|
+
### Goal and implementation
|
|
434
|
+
|
|
435
|
+
`createFlowGoal` creates a session when none is unmerged and starts `/flow-goal <session-dir>` in
|
|
436
|
+
one ACP peer. The prompt interviews the developer one question at a time until the desired end
|
|
437
|
+
result and every load-bearing boundary are settled, then writes only `<session-dir>/goal.md`; its
|
|
438
|
+
questions arrive as `flowQuestion` with `target: "goal"` and are answered with `flowAnswer`. The
|
|
439
|
+
goal text is end-result only: no implementation, technology, path, step, or internal criterion.
|
|
440
|
+
The interview is bounded by `goalTimeoutMs` (120 s by default). `<session-dir>/goal.md` is removed
|
|
441
|
+
before each interview, so only a file that interview writes can be accepted. A timeout kills the
|
|
442
|
+
peer, discards whatever draft it wrote, and reports `flowGoalStopped` with `completed: false`; the
|
|
443
|
+
session's accepted goal is left unchanged, exactly as when the interview writes no goal.
|
|
444
|
+
`saveFlowGoal` replaces the goal text while the implementation is stopped. `startFlowImplementation`
|
|
445
|
+
requires a non-empty goal and starts `/flow-implement`; `stopFlowImplementation` ends that peer and
|
|
446
|
+
reports the session `stopped`. A session whose pull request has merged cannot be restarted: create
|
|
447
|
+
a new goal.
|
|
448
|
+
|
|
449
|
+
The implementation commits each coherent unit itself and never pushes. The server reads its
|
|
450
|
+
commits, records the checkpoints, follows the diff, and runs the change analyst whenever the
|
|
451
|
+
diff changes. The developer watches activity, answers implementation questions, accepts or refuses
|
|
452
|
+
widgets, and writes steering notes and decisions while it works. A refused widget becomes a
|
|
453
|
+
steering note telling the agent to abandon that change and implement another solution.
|
|
454
|
+
|
|
455
|
+
### Session state
|
|
456
|
+
|
|
457
|
+
A session lives at `<stateDir>/flow/<24-hex journalKey(REPO_DIR)>/<session-id>/`, keyed exactly
|
|
458
|
+
like the dashboard state, so repositories sharing one harness never see each other's sessions.
|
|
459
|
+
`stateDir` is the harness state directory (`<harness>/.tmp` by default, overridable with
|
|
460
|
+
`STATE_DIR`); `<session-id>` is an 8-hex id chosen when the session is created. The directory is
|
|
461
|
+
never committed and never written into the host.
|
|
462
|
+
|
|
463
|
+
|File|Contents|
|
|
464
|
+
|---|---|
|
|
465
|
+
|`goal.md`|The goal text `/flow-goal` wrote, or the text the developer saved.|
|
|
466
|
+
|`decisions.md`|The decision board: developer text plus appended timestamped sections.|
|
|
467
|
+
|`steering.jsonl`|Append-only developer steering notes.|
|
|
468
|
+
|`steering-ack.jsonl`|The ids of steering notes the implementation consumed.|
|
|
469
|
+
|`checkpoints.jsonl`|One JSON line per commit: id, time, summary, files, sha, verification.|
|
|
470
|
+
|`verification.json`|Latest test, lint, or build result: command, status, timestamp, output.|
|
|
471
|
+
|`changes.json`|The current changed files with status, insertion and deletion counts, and diffs.|
|
|
472
|
+
|`accepted.json`|The widget signatures the developer accepted.|
|
|
473
|
+
|`blockers.jsonl`|Every `DEVBOARD-BLOCKED:` line an implementation printed.|
|
|
474
|
+
|`session.json`|Machine state: ids, status, worktree, branch, base, widgets, checkpoints, PR.|
|
|
475
|
+
|`request.json`|Analyst input: goal, decisions, changed files and diffs, previous widgets.|
|
|
476
|
+
|`widgets.json`|The change analyst's complete widget list.|
|
|
477
|
+
|
|
478
|
+
`goal.md`, `decisions.md`, `verification.json`, and `checkpoints.jsonl` are re-read on restore,
|
|
479
|
+
so text a developer or agent changed while the server was down is the text the replacement uses.
|
|
480
|
+
`session.json` is the source of truth for status, worktree, branch, widgets, accepted signatures,
|
|
481
|
+
changed files, blockers, and the pull request.
|
|
482
|
+
|
|
483
|
+
### Change widgets
|
|
484
|
+
|
|
485
|
+
The change analyst groups every changed file into one or more widgets and writes the complete list
|
|
486
|
+
to `widgets.json`. Each widget has a fixed category, a concept, a summary, a rationale, and the
|
|
487
|
+
files it covers. The seven categories are:
|
|
488
|
+
|
|
489
|
+
- `contract` — an interface another part of the system or an external caller depends on.
|
|
490
|
+
- `entity` — the identity, lifecycle, or ownership of a domain object.
|
|
491
|
+
- `naming` — a rename or terminology change with behavior otherwise unchanged.
|
|
492
|
+
- `data` — a migration, schema, persistence, or stored-data shape change.
|
|
493
|
+
- `dependency` — an added, removed, or changed external package, service, or tool.
|
|
494
|
+
- `irreversible` — a change that is costly or impossible to undo after shipping.
|
|
495
|
+
- `structure` — file layout, module boundaries, or code organization, and the fallback for a
|
|
496
|
+
file no clearer category explains.
|
|
497
|
+
|
|
498
|
+
Every category has its own color token, each with a light and a dark variant:
|
|
499
|
+
`--flow-contract-border` and `--flow-contract-surface` through
|
|
500
|
+
`--flow-structure-border` and `--flow-structure-surface`, defined for the day theme in `:root` and
|
|
501
|
+
overridden in the dark theme. A widget carries `data-category="<category>"` and always renders its
|
|
502
|
+
category label as text, so the color is never the only signal.
|
|
503
|
+
|
|
504
|
+
A widget's id is a signature: the first 16 hex characters of a SHA-256 over its category, its
|
|
505
|
+
sorted file paths, and those files' normalized diffs. The concept, summary, and rationale are
|
|
506
|
+
display metadata and never part of it. Acceptance is keyed by that signature and persisted in
|
|
507
|
+
`accepted.json`; a re-analysis that finds the same category, files, and diff keeps the same
|
|
508
|
+
signature and therefore stays accepted, while any edit to those files yields a new signature and
|
|
509
|
+
an unaccepted widget. A widget whose files leave the diff disappears, and its stale signature is
|
|
510
|
+
dropped from `accepted.json`. Every file left unnamed by the analyst is still shown, as a
|
|
511
|
+
`structure` widget, so no changed file is ever hidden.
|
|
512
|
+
|
|
513
|
+
### Steering and the decision board
|
|
514
|
+
|
|
515
|
+
`sendFlowSteering` appends a timestamped note to `steering.jsonl`; the feed is append-only and
|
|
516
|
+
never rewritten. Notes carry `new` until the implementation appends the consumed ids to
|
|
517
|
+
`steering-ack.jsonl`, after which the server reports them `read`. The implementation reads only
|
|
518
|
+
the lines it has not consumed, appends the ids it acted on, and applies every direction before
|
|
519
|
+
its next checkpoint. Refusing a widget records the refusal as a steering note, and concluding a
|
|
520
|
+
widget conversation appends its conclusion to the decision board and adds one steering note.
|
|
521
|
+
|
|
522
|
+
The decision board is `<session-dir>/decisions.md`. The developer's `Save decisions` replaces
|
|
523
|
+
the document and is authoritative: it is exactly the text the agent reads next. Sections the
|
|
524
|
+
server appends — a widget conclusion, for example — are timestamped and source-named, and only
|
|
525
|
+
ever append: they never edit, reflow, or remove the developer's text or an earlier section. An
|
|
526
|
+
appended section is a record, not a vote: the developer's saved text still governs, and the
|
|
527
|
+
implementation must not rewrite either form.
|
|
528
|
+
|
|
529
|
+
### Worktree, pull request, and merge
|
|
530
|
+
|
|
531
|
+
Starting an implementation creates the worktree `<repo>/.worktree/flow-<session-id>` on branch
|
|
532
|
+
`flow/<session-id>`, based on `origin/main` when it exists and on `main` otherwise, and runs
|
|
533
|
+
`/flow-implement <session-dir>` with that worktree as its working directory. The agent works and
|
|
534
|
+
commits only there; the server watches the branch and records every checkpoint it finds.
|
|
535
|
+
|
|
536
|
+
The first checkpoint beyond the base revision makes the server push the branch and open one draft
|
|
537
|
+
pull request or merge request against `main`, using the forge named by `origin`; later checkpoints
|
|
538
|
+
update its title and body with the goal and the decision board. The developer marks it ready
|
|
539
|
+
manually, only after the implementation is stopped and the worktree is clean. The developer then
|
|
540
|
+
merges it manually through the forge, and only as a merge commit — never squash, never rebase, and
|
|
541
|
+
only after both modes' work has stopped and the worktree is clean. Flow never merges on a
|
|
542
|
+
schedule, never force-pushes, and never rewrites the branch.
|
|
543
|
+
|
|
544
|
+
After a successful merge the server fast-forwards the host's `main` checkout when it is on `main`
|
|
545
|
+
and clean, removes the worktree, and deletes the local and remote branch once the branch tip is an
|
|
546
|
+
ancestor of `origin/main`. Whatever it could not remove is reported as a leftover and as one
|
|
547
|
+
`error`; the session still records the merge, and the leftover names each path or ref so nothing is
|
|
548
|
+
silently abandoned.
|
|
549
|
+
|
|
550
|
+
### Restart and resume
|
|
551
|
+
|
|
552
|
+
A graceful stop, launcher restart, or `SIGKILL` leaves the session on disk. The replacement reads
|
|
553
|
+
the session directory and re-emits its complete state. A session that was running is reported
|
|
554
|
+
`stopped`, its pending question is cleared, and the server sends one `flowNotice` saying the Flow
|
|
555
|
+
implementation stopped and that starting it again resumes it. It never re-spawns a Flow agent on
|
|
556
|
+
its own: no goal interview, implementation turn, change analysis, or widget conversation resumes
|
|
557
|
+
automatically. Selecting `Start implementation` re-spawns `/flow-implement` with the same session
|
|
558
|
+
directory, and the agent reads the persisted goal, decisions, steering acks, checkpoints, and
|
|
559
|
+
verification before continuing. A session that was idle stays idle. The replacement recomputes the
|
|
560
|
+
diff and widgets from the worktree without spawning an analyst, so a restart can never lose review
|
|
561
|
+
state or overwrite a newer widget list.
|
|
562
|
+
|
|
563
|
+
### What Flow never does
|
|
564
|
+
|
|
565
|
+
Flow never creates an issue or an epic, never queries the tracker, and never labels, closes, or
|
|
566
|
+
comments on one. Its forge use is limited to the session's draft pull request or merge request and
|
|
567
|
+
that request's branch: create, update, mark ready, view, merge, and delete the remote branch. Flow
|
|
568
|
+
never writes `GOAL.md` and never writes `adrs/`; the flow goal belongs only to its session
|
|
569
|
+
directory, and the decision board is never an ADR record.
|
|
570
|
+
|
|
571
|
+
## Discover Mode
|
|
572
|
+
|
|
573
|
+
The Discover mode is the fourth client-only view, opened from the activity bar. It exists for an existing host project: one
|
|
574
|
+
read-only crawl produces an evidence-backed project sheet, a UbiLang glossary for each detected
|
|
575
|
+
bounded context, and five project-specific Agent Skills that every devboard implementation agent in
|
|
576
|
+
that host discovers. Switching to Discover, and switching its pages, sends no WebSocket frame
|
|
577
|
+
and starts, stops, resets, or clears nothing; all three views stay mounted, so native details,
|
|
578
|
+
scroll state, and focus survive. Discover events render only in this view and never touch Goal
|
|
579
|
+
Mode's activity, question, or error state.
|
|
580
|
+
|
|
581
|
+
The Discover sidebar opens pages for `Overview` and `Glossary` without a group heading. Once
|
|
582
|
+
Discover artifacts are published it also lists the five skills `Testing Strategy`,
|
|
583
|
+
`Naming Conventions`, `Coding Conventions`, `Architecture & Boundaries`, and `Glossary` under
|
|
584
|
+
**Skills** (`data-testid="discover-tab"` with `data-tab` values `overview`, `glossary`,
|
|
585
|
+
`testing-strategy`, `naming-conventions`, `coding-conventions`, `architecture-boundaries`, and
|
|
586
|
+
`glossary-skill`); with no published artifacts the **Skills** group and its header do not render.
|
|
587
|
+
The sidebar has no header, so its first entry is its mode's navigation. The active panel carries
|
|
588
|
+
`data-testid="discover-panel"` and the same `data-tab`.
|
|
589
|
+
Warnings appears as its own group with a count only while at least one warning exists; the sidebar
|
|
590
|
+
keeps no freshness group and no recent-notice list.
|
|
591
|
+
|
|
592
|
+
Every Discover page shares one frame: an optional uppercase eyebrow, one `h1`, a subtitle, an
|
|
593
|
+
optional meta row, and a right-aligned action group. The Overview frame holds the repository as
|
|
594
|
+
`owner/name` from its GitHub remote (falling back to the directory name when no remote names one;
|
|
595
|
+
the full path stays in the tooltip), the one-line definition, the discovery date, and the single
|
|
596
|
+
filled run action. Staleness is not repeated on the sheet: the activity bar and the status bar
|
|
597
|
+
report it. While a run exists, every Discover page carries a run tag above its notice stack: a
|
|
598
|
+
status dot, `Discover run in progress`, the age, the run identifier as a code chip, the latest
|
|
599
|
+
activity line, and Stop. Below the Overview frame, the active run region adds the full activity
|
|
600
|
+
terminal: a tinted header with `Run in progress`, `Started 3 minutes ago`, and Stop, followed by
|
|
601
|
+
the terminal. A queued question region follows while a question waits.
|
|
602
|
+
The sheet follows: Platform, Delivery, and Stack in a bounded rail on the left, Commands and
|
|
603
|
+
Testing in the main column on the right, and Bounded contexts below them. The manual-testing strip
|
|
604
|
+
is the last region of the page, so a source checkout never pushes it above the project identity. An
|
|
605
|
+
error, an interrupted-run notice, and a stop notice render above the page in one notice stack.
|
|
606
|
+
|
|
607
|
+
The Glossary row is a plain section link: it has no disclosure control and no nested submenu, so
|
|
608
|
+
individual terms are never repeated as sidebar menu items. A term opens from its row on the
|
|
609
|
+
Glossary page.
|
|
610
|
+
|
|
611
|
+
With no published artifacts the Overview page shows the header's first-run explanation and a
|
|
612
|
+
`Discover project` button (`data-testid="discover-start"`); with artifacts it shows `Re-discover`.
|
|
613
|
+
The button is disabled when the socket is not open, while a Discover run is active,
|
|
614
|
+
or while Goal or Flow work is active, and its title names the reason. It never starts on page
|
|
615
|
+
load, reconnect, mode switch, or page switch. Selecting it sends `{ "type": "discoverProject" }`;
|
|
616
|
+
the server broadcasts `discoverStarted` and starts exactly one ACP peer whose prompt is exactly
|
|
617
|
+
`/discover <staging-dir>`, whose working directory is the active target root, and whose staging
|
|
618
|
+
directory is `<target>/.omp/discover/staging/<run-id>/`. A second start while one runs is refused
|
|
619
|
+
with one `discoverError` and spawns no peer. A start requires no `GOAL.md`, no prepared skill, a
|
|
620
|
+
clean tree, or a forge account; it is accepted on a host with a dirty tree and no goal document.
|
|
621
|
+
|
|
622
|
+
The run is server-owned: closing the browser or switching views does not stop it. The view shows
|
|
623
|
+
an active marker (`data-testid="discover-active"`) and the xterm activity output, and a reconnect receives
|
|
624
|
+
`discoverState`, `discoverStarted`, every recorded activity line, and any pending question before
|
|
625
|
+
its own command replies. The crawl is not an interview: an ambiguity is recorded as `unknown` with
|
|
626
|
+
a reason, never a blocking question. `answerDiscover` with the pending id and a non-empty value
|
|
627
|
+
answers it once; a second answer, a stale id, or an empty value is refused with one
|
|
628
|
+
`discoverError`. `stopDiscover` closes the peer, clears the pending question, removes staging,
|
|
629
|
+
broadcasts `discoverStopped { completed: false }`, and leaves the last published artifacts
|
|
630
|
+
byte-identical; it spawns no replacement peer.
|
|
631
|
+
|
|
632
|
+
Discover uses the same WebSocket at `/ws`. The client sends `discoverProject`, `stopDiscover`,
|
|
633
|
+
`answerDiscover` (with `id` and `value`), `getDiscover`, `saveDiscoverSkill` (with `name` and
|
|
634
|
+
`text`), and `restoreDiscoverSkill` (with `name`). The protocol also accepts
|
|
635
|
+
`confirmGlossaryTerm` (with `context` and `id`) and `rejectGlossaryTerm` (with `context`, `id`, and
|
|
636
|
+
a non-empty `reason`) for tooling, but the Glossary page does not use them. `getDiscover` asks for
|
|
637
|
+
the current snapshot and is answered with `discoverState`. The server
|
|
638
|
+
broadcasts `discoverState`, `discoverStarted`, `discoverActivity`, `discoverQuestion`,
|
|
639
|
+
`discoverStopped`, and `discoverError`; it never sends `activity`, `question`, `error`,
|
|
640
|
+
`runStarted`, or `runStopped` for Discover. `discoverState` carries the active and interrupt flags,
|
|
641
|
+
staleness, warnings, the busy reason, the last read error, the manifest, freshness, sheet,
|
|
642
|
+
repository links for installed packages, glossaries, per-concept usage and examples measured from
|
|
643
|
+
the host's documentation and source, `glossarySignificance` keyed by bounded context and then
|
|
644
|
+
concept id with `{ score, reason }` values, decisions, and the five skills with baseline text, live
|
|
645
|
+
text, hashes, and state.
|
|
646
|
+
`discoverQuestion` carries `id`, `message`, and `options` of `{ label, description?, recommended }`.
|
|
647
|
+
|
|
648
|
+
When the peer exits, the server validates every staged artifact and the whole staged set before
|
|
649
|
+
publishing. Success publishes atomically, removes staging, broadcasts `discoverState` with the new
|
|
650
|
+
artifacts, and broadcasts `discoverStopped { completed: true }`. Any invalid, missing, unreadable,
|
|
651
|
+
or incomplete artifact, schema violation, write outside staging, or collision with a non-Discover
|
|
652
|
+
skill fails the run: staging is removed, one `discoverError` names the reason,
|
|
653
|
+
`discoverStopped { completed: false }` follows, and the previous sheet, glossary, manifest,
|
|
654
|
+
baselines, and live skills stay byte-identical.
|
|
655
|
+
Published artifacts are read from disk when a client connects, so no prior run in the process is
|
|
656
|
+
required; an unreadable or invalid published artifact is reported with one `discoverError` and the
|
|
657
|
+
files are never overwritten.
|
|
658
|
+
|
|
659
|
+
Artifacts live only under the host: `.omp/discover/manifest.json`, `.omp/discover/sheet.json`,
|
|
660
|
+
`.omp/discover/decisions.json`, `.omp/discover/significance.json`,
|
|
661
|
+
`.omp/discover/glossary/<bounded-context>.glossary.yaml`,
|
|
662
|
+
`.omp/discover/skills/<skill>/SKILL.md` for the generated baselines, and the live, editable
|
|
663
|
+
`.omp/skills/<skill>/SKILL.md`. Staging mirrors the crawl's relative layout without `manifest.json`
|
|
664
|
+
or `decisions.json`, and the staged sheet omits freshness. The manifest carries a schema version,
|
|
665
|
+
the discovery fingerprint, the publication time, the run id, the sheet's structural digest, one
|
|
666
|
+
structural digest per glossary, the five skill names, each live skill's baseline and current hash,
|
|
667
|
+
each skill's edited or missing state, and artifact paths relative to the host; it contains no
|
|
668
|
+
secrets and no absolute path. A manifest written by this code carries
|
|
669
|
+
`paths.significance: ".omp/discover/significance.json"`; a manifest without that path predates the
|
|
670
|
+
significance sidecar and is read as legacy, with no error.
|
|
671
|
+
|
|
672
|
+
`sheet.json` is the canonical data sheet, with top-level keys in the fixed order `schemaVersion`,
|
|
673
|
+
`definition`, `stack`, `deployment`, `environments`, `commands`, `branching`, `testing`,
|
|
674
|
+
`architecture`, `ci`, `runtime`, `dependencyMap`, and `delivery`. It has a freshness header naming
|
|
675
|
+
the project, the short commit, the dirty flag, the discovery time, and the stored fingerprint. The
|
|
676
|
+
Overview page is a project sheet, not a card grid. A masthead
|
|
677
|
+
(`data-testid="discover-freshness"`) carries the project name, the one-sentence definition, and the
|
|
678
|
+
discovery date. The commit, the dirty flag, and the staleness flag stay in the sheet and the state
|
|
679
|
+
payload but render no header badge. Below the masthead the sheet is one bounded two-column spine:
|
|
680
|
+
`Platform`, `Delivery`, and `Stack` in a left rail, and Commands, Testing, Quality gates, and
|
|
681
|
+
Bounded contexts in the main column on the right. Every section carries
|
|
682
|
+
`data-testid="discover-card"` and `data-field`; the commands, testing, and quality-gate panels are
|
|
683
|
+
raised surfaces and the remaining sections are flat, separated by hairlines. Below 1240px viewport
|
|
684
|
+
the columns collapse into one in reading order.
|
|
685
|
+
Tables grow with the page; neither a panel nor a card shrinks below its rows, and none scrolls
|
|
686
|
+
inside the page. Every non-`unknown` leaf
|
|
687
|
+
carries at least one sorted, unique repository-relative evidence path, symbol anchor, or safe git
|
|
688
|
+
fact and a confidence of `observed`, `declared`, `inferred`, or `unknown`; `unknown` carries empty
|
|
689
|
+
evidence and a short reason. Confidence labels, evidence paths, the `Not observed` ledger, the
|
|
690
|
+
repeated dependency map, and the static lead and note prose are not rendered; the stored facts and
|
|
691
|
+
the sheet's confidence and evidence stay unchanged. The definition is exactly one sentence of at
|
|
692
|
+
most 240 characters; every value and list entry is a single line of at most 160 characters. The
|
|
693
|
+
sheet covers the short platform stack (languages, runtimes, package managers, frameworks, and
|
|
694
|
+
major tools, not individual dependencies), customer-facing deployment (artifact class, artifact,
|
|
695
|
+
target, and pipeline sources), every environment (at least `production`, whose purpose states how
|
|
696
|
+
changes reach it), exact main commands (with `unknown` for an unobserved command), branching
|
|
697
|
+
strategy and model, test levels and pyramid, architecture contexts and their directed dependencies
|
|
698
|
+
plus boundaries, CI provider, triggers, jobs, and quality gates, and the three mandatory sections:
|
|
699
|
+
runtime inventory, entry-point and dependency map, and delivery and versioning. It never guesses,
|
|
700
|
+
invents an additional environment, copies README prose, or records a configuration value; a claim
|
|
701
|
+
it cannot ground is `unknown`.
|
|
702
|
+
|
|
703
|
+
Main commands render as a two-column `Task`/`Command` table of the observed commands, `$`-prefixed,
|
|
704
|
+
with no header row, under an `N of 8 commands found` count. That count is a disclosure
|
|
705
|
+
(`data-testid="discover-command-coverage"`): activating it lists every one of the eight command
|
|
706
|
+
slots, and a slot the crawl did not find renders a muted `not found` placeholder whose title
|
|
707
|
+
carries the unknown reason. A `tryOut` command is listed only when it is a hands-on entry point
|
|
708
|
+
that no other recorded command aliases. A command the host manifest declares as a script is shown
|
|
709
|
+
as its invocation — `npm test` for `scripts.test`, `npm run <name>` for any other script — instead
|
|
710
|
+
of the program the script expands to; the recorded program is the chip's tooltip. Every command,
|
|
711
|
+
path, package, and boundary value uses the same inline code chip (`.code-chip`); no surface has a
|
|
712
|
+
second inline-code presentation. Testing shows the test, level, and
|
|
713
|
+
happy-path totals and the whole-suite command (`data-testid="discover-suite-command"`), then a
|
|
714
|
+
`Level`/`Tests`/`Happy path`/`Command` table (`data-testid="discover-pyramid"`). Happy path is the
|
|
715
|
+
part of a level's count that exercises the primary success path, recorded as an exact count from
|
|
716
|
+
`0` to the level's total; the summary percentage appears only when every listed level reports one.
|
|
717
|
+
`Command` is the narrowest declared invocation for that level. Only a level that carries a test is
|
|
718
|
+
listed and counted; a recorded level with no test case is omitted, so a sheet cannot claim a
|
|
719
|
+
second level while its table holds none. Every listed level states an exact test count, never
|
|
720
|
+
`count unknown`. No pyramid graphic is drawn. Quality gates render as a `Quality gate`/`What it
|
|
721
|
+
checks` table (`data-testid="discover-quality-gates"`): one row per required CI gate, its name, and
|
|
722
|
+
one short line saying what it verifies. A gate recorded without a check states `No check was
|
|
723
|
+
recorded.`, and the gates are not repeated among the Delivery facts. Runtime, deployment, CI, and
|
|
724
|
+
branching use compact `dt`/`dd` facts whose list values are comma-separated.
|
|
725
|
+
Architecture renders under the heading `Bounded contexts` as context rows showing the name,
|
|
726
|
+
responsibility, and entry points, followed by external-boundary and shared-kernel chips and a
|
|
727
|
+
rendered Mermaid dependency graph. The repository path of a bounded context is not rendered. The
|
|
728
|
+
graph draws one node per bounded context and one arrow for every `architecture.dependencies` edge,
|
|
729
|
+
pointing from the context that depends to the context it reaches; it is drawn with `flowchart LR`
|
|
730
|
+
through Mermaid at render time, and a sheet without recorded edges draws no graph.
|
|
731
|
+
External boundaries name only external systems — a remote API or service, a message broker, a
|
|
732
|
+
database, or an identity provider the host integrates with across its own process and artifact.
|
|
733
|
+
Language and runtime built-ins describe internal architecture, so `node:child_process`,
|
|
734
|
+
`node:worker_threads`, and other concurrency primitives are never boundaries; a host that integrates
|
|
735
|
+
with no external system shows `None detected.` in that block instead of an invented entry or
|
|
736
|
+
`unknown`.
|
|
737
|
+
`Stack` renders one full-width chip band after the two columns; a chip whose package is installed
|
|
738
|
+
under `node_modules/` becomes a link to the repository its manifest declares, and every other chip
|
|
739
|
+
stays plain text. Environments is `unknown` here and is not listed. The delivery section, the
|
|
740
|
+
deployment pipeline sources, and the runtime `versions` fact stay hidden because they repeat other
|
|
741
|
+
facts. A sheet with no observed test level and a glossary with no matching term keep an explicit
|
|
742
|
+
empty state.
|
|
743
|
+
|
|
744
|
+
The Glossary page and `.omp/discover/glossary/` implement the UbiLang glossary format: one YAML
|
|
745
|
+
document per bounded context named `<bounded-context>.glossary.yaml`, top-level `boundedContext`,
|
|
746
|
+
`titles`, and `concepts`, and concepts carrying `id`, `prefLabels`, `altLabels`, `hiddenLabels`,
|
|
747
|
+
`definitions`, `scopeNote`, `example`, `historyNote`, `status`, `origin`, `standardRef`, `kind`,
|
|
748
|
+
`grounding`, `broader`, and `related` in that order. Fields are camelCase, enum values use the
|
|
749
|
+
standard's snake_case spellings, ids are unique kebab case, every concept has an English preferred
|
|
750
|
+
label and at least one `en` definition, and every label language is `en` or `de`. No undeclared
|
|
751
|
+
field is written. Every concept has at least one grounding of the form
|
|
752
|
+
`code:<repository-relative path that exists>` or `model:<name that appears in the host>`; an
|
|
753
|
+
ungrounded candidate is omitted. The crawl stages
|
|
754
|
+
`status: unconfirmed` with `origin: owner` or `standard` for a term found in code or documentation,
|
|
755
|
+
and `status: proposed` with `origin: llm` for a name it proposes for an existing unnamed concept;
|
|
756
|
+
it never writes `confirmed`, `deprecated`, or `invalidated`. On publication the server confirms
|
|
757
|
+
every newly crawled term, so the published glossary contains only confirmed terms unless an
|
|
758
|
+
earlier operator decision invalidated one. The page groups terms by context and offers search; it
|
|
759
|
+
shows no status filter, status badge, or per-term control, and it hides invalidated terms. Each
|
|
760
|
+
context card holds a table with `Kind`, `Term`, `Significance`, `Definition`, and an unnamed
|
|
761
|
+
examples column; the Kind, Term, Significance, and examples columns have the same fixed widths in
|
|
762
|
+
every context card, the term reads in bold, and the examples cell lists the identifiers that name
|
|
763
|
+
the concept in the host's code. The table grows with the page, neither the card nor the table
|
|
764
|
+
scrolls inside a panel, and long definitions wrap; each term carries `data-testid="discover-term"`,
|
|
765
|
+
`data-context`, `data-id`, `data-status`, `data-kind`, `data-significance`, and `data-examples`, and
|
|
766
|
+
each context carries `data-testid="discover-context"`. The context heading shows its title, falling
|
|
767
|
+
back to the context id only when no title exists. Every Kind value carries a Lucide icon colored by
|
|
768
|
+
kind: entity, event, field, role, actor, value, and rule each use a stable token. The search field
|
|
769
|
+
centers its placeholder vertically.
|
|
770
|
+
|
|
771
|
+
The crawl authors each concept's significance; the server never derives a score from occurrence
|
|
772
|
+
counts. The crawl includes only project-significant entities in a glossary, omits generic framework,
|
|
773
|
+
library, language, and UI vocabulary such as `option`, `hook`, or `command` unless the host defines
|
|
774
|
+
that exact term as a project concept grounded in at least two distinct host files, and stages a
|
|
775
|
+
score from `0` to `100` with a one-line reason for every included concept, where 50 is the
|
|
776
|
+
significance floor and a lower score means an incidental term. `glossary/significance.json` is the
|
|
777
|
+
staged sidecar; the server validates it, then publishes it atomically as
|
|
778
|
+
`.omp/discover/significance.json` beside the glossary artifacts. Its shape is
|
|
779
|
+
`{"schemaVersion": 1, "contexts": {"<boundedContext>": {"<concept-id>": {"score": 0, "reason":
|
|
780
|
+
"One line of evidence."}}}}`. The run fails with one `discoverError` and publishes nothing when the
|
|
781
|
+
sidecar is absent, is not valid JSON or not an object, carries a top-level key other than
|
|
782
|
+
`schemaVersion` and `contexts`, carries a `schemaVersion` other than `1`, names a context or concept
|
|
783
|
+
the staged glossaries do not stage, omits a staged context or concept, carries an entry key other
|
|
784
|
+
than `score` and `reason`, carries a score that is not an integer from `0` through `100`, or carries
|
|
785
|
+
a reason that is empty, multi-line, longer than 160 characters, an absolute path, or a secret. A
|
|
786
|
+
rejected sidecar leaves the previously published sheet, glossaries, manifest, decisions, sidecar,
|
|
787
|
+
baselines, and live skills byte-identical. A concept preserved from an earlier human decision but
|
|
788
|
+
absent from the staged sidecar is published unscored rather than rejected. Scores are excluded from
|
|
789
|
+
the glossary digest and the sheet digest, so changing only a score moves no digest.
|
|
790
|
+
|
|
791
|
+
The Glossary page orders each context's rows by significance descending, with unscored concepts last
|
|
792
|
+
and ties broken by the displayed term label, and it renders the score in the `Significance` column;
|
|
793
|
+
an unscored concept reads `—`. When at least one published concept is scored, a `Significant only`
|
|
794
|
+
control (`data-testid="discover-glossary-significant"`) is present and checked by default, and the
|
|
795
|
+
floor slider (`data-testid="discover-glossary-threshold"`) spans `min="0"` to `max="100"` with
|
|
796
|
+
`value="50"`. With the control checked, a concept is listed only when it matches the search and
|
|
797
|
+
either has no score or scores at or above the slider value; clearing it lists every term matching
|
|
798
|
+
the search and disables the slider. The subtitle reports the visible terms, the total terms, and the
|
|
799
|
+
active floor, the sidebar glossary count stays the published term count, and the search, filter, and
|
|
800
|
+
slider compose. The search field, the `Significant only` switch, and the valued floor sit in one
|
|
801
|
+
bounded toolbar row. A glossary published before the sidecar existed keeps its concepts and stays
|
|
802
|
+
unfiltered: every term is visible, each Significance cell reads `—`, and the switch and slider are
|
|
803
|
+
disabled. An invalidated term never appears. A glossary row is read-only: it carries no activation
|
|
804
|
+
command, no focus target, and no single-term page. The server still measures per-concept usage and
|
|
805
|
+
examples for the row's examples cell, but `glossaryUsage` never decides which term is listed or in
|
|
806
|
+
which order.
|
|
807
|
+
|
|
808
|
+
Confirmation is automatic, so the Glossary page offers no `Confirm` or `Reject` control.
|
|
809
|
+
`confirmGlossaryTerm` still succeeds only for a term that is proposed or unconfirmed, and
|
|
810
|
+
`rejectGlossaryTerm` requires a non-empty reason and changes a proposed, unconfirmed, or confirmed
|
|
811
|
+
term to `invalidated`; neither is reachable from the page. Both append a dated decision to
|
|
812
|
+
`decisions.json` with the context, id, action, reason when rejecting, and date, never adding a
|
|
813
|
+
field to the YAML, and the change survives reload, reconnect, and restart. A later crawl preserves
|
|
814
|
+
every confirmed or invalidated concept's labels, definitions, notes, status, origin, standard
|
|
815
|
+
reference, kind, and related ids; it may refresh grounding and add confirmed concepts, but it never
|
|
816
|
+
resurrects an invalidated id, never overwrites a human decision, and removes an undecided concept
|
|
817
|
+
only when the new crawl no longer grounds it. A malformed or structurally invalid glossary fails
|
|
818
|
+
the run atomically with one `discoverError` naming the file and rule.
|
|
819
|
+
|
|
820
|
+
A successful run publishes exactly five skills under `.omp/skills/`: `discover-testing-strategy`,
|
|
821
|
+
`discover-naming-conventions`, `discover-coding-conventions`,
|
|
822
|
+
`discover-architecture-boundaries`, and `discover-glossary`. Each is a directory containing
|
|
823
|
+
`SKILL.md` whose frontmatter has `name` equal to the directory, a non-empty `description` of at most
|
|
824
|
+
1024 characters, `metadata.devboard-managed: "true"`, `metadata.devboard-family: "discover"`, and
|
|
825
|
+
`metadata.devboard-skill` naming its role. The bodies are project-specific and directive: one
|
|
826
|
+
imperative bullet per rule, and no bullet names a source, test, manifest, or CI path — an exact
|
|
827
|
+
command may still contain one. A bullet never says where a rule was observed: no evidence clause,
|
|
828
|
+
file name, directory name, or path outside an exact command, so the skill carries policy and the
|
|
829
|
+
sheet carries provenance. Testing strategy states levels, pyramid, commands, fixture roles,
|
|
830
|
+
coverage expectations, and how to add a test; naming conventions state canonical terms, casing, and
|
|
831
|
+
rejected aliases; coding conventions state patterns, error handling, typing, async, logging,
|
|
832
|
+
dependencies, formatting, and prohibited constructs; architecture and boundaries state contexts,
|
|
833
|
+
entry points, dependency direction, persistence, integrations, and prohibited cross-boundary
|
|
834
|
+
imports; the glossary skill names the canonical glossary directory, the confirmed and rejected
|
|
835
|
+
rules, and the proposal process. From the host
|
|
836
|
+
root, a new `omp` session lists the five (`omp skill list`) and loads their bodies as skills, so
|
|
837
|
+
every implementation agent in that host discovers the set with no extra configuration.
|
|
838
|
+
|
|
839
|
+
Each skill has a page that is one full-height Monaco Markdown editor for the whole `SKILL.md`
|
|
840
|
+
(`data-testid="discover-skill-text"`), with no rendered preview and no split pane. The page header
|
|
841
|
+
holds only the skill's page title. The editor panel header names the skill's relative path,
|
|
842
|
+
`<skill>/SKILL.md`, with the skill name emphasized and the file name muted; a second 11px line
|
|
843
|
+
states the draft (`No unsaved changes`, `Unsaved changes`, or `Live file missing`). The header
|
|
844
|
+
also holds `discover-skill-save`, disabled until the draft differs from the live text, and, while
|
|
845
|
+
the live text differs from the generated version or the file is missing,
|
|
846
|
+
`discover-skill-restore`. A `discover-skill-dirty` marker replaces the clean state line while the
|
|
847
|
+
draft has unsaved changes; the page renders no state badge and no purpose line, because the
|
|
848
|
+
sidebar already flags `edited` or `missing`.
|
|
849
|
+
`saveDiscoverSkill` writes the submitted bytes verbatim and updates the manifest's current hash and
|
|
850
|
+
edited state; it is refused with one
|
|
851
|
+
`discoverError` when the name is not one of the five, the skill is missing, the frontmatter name
|
|
852
|
+
no longer matches the directory, the description is empty or too long, or a marker was removed,
|
|
853
|
+
and invalid text leaves the previous file byte-identical. `restoreDiscoverSkill` copies the current
|
|
854
|
+
generated version over the live skill, recreating it when it was deleted; it spawns no agent. A
|
|
855
|
+
later run compares each live hash with its last baseline: an unedited skill is replaced,
|
|
856
|
+
an edited skill is preserved byte for byte with its new baseline published beside it, and a skill
|
|
857
|
+
the developer deleted is not recreated but shows the `Recreate generated version` action. A
|
|
858
|
+
generated name colliding with a prepare-family, unmanaged, or different Discover skill fails the
|
|
859
|
+
run and publishes nothing.
|
|
860
|
+
|
|
861
|
+
The goal lifecycle's skill prerequisite accepts a managed skill from either family: at least one
|
|
862
|
+
prepare-family or Discover skill is enough for `Start implementation`. `getSkills` and `skillsState`
|
|
863
|
+
carry `name`, `description`, `family` (`prepare` or `discover`), and `state` (`generated`, `edited`,
|
|
864
|
+
or `missing`), and a managed skill with no `devboard-family` field belongs to `prepare`. The Skills
|
|
865
|
+
panel lists both families with their family and Discover state, and every prepare-family row
|
|
866
|
+
carries a Delete control that confirms before removing that one skill. Prepare Skills replaces only its
|
|
867
|
+
own family, and its contradiction review covers its family plus hand-authored unmanaged skills, so
|
|
868
|
+
a project-specific Discover directive cannot fail a project-agnostic preparation. Discover's
|
|
869
|
+
generated baselines must agree with each other and with the glossary; a contradiction there fails
|
|
870
|
+
the run, while a contradiction introduced by a developer's skill edit is a non-blocking warning in
|
|
871
|
+
the skill editor and never discards the edit.
|
|
872
|
+
|
|
873
|
+
The sheet and every glossary are canonicalized before publication: fixed schema key order, arrays
|
|
874
|
+
sorted by stable identity, evidence sorted and de-duplicated, and enums in fixed spelling. The
|
|
875
|
+
structural digest excludes volatile fields (`discoveredAt`, fingerprint timestamps, run id, and
|
|
876
|
+
publication time) and bounded prose (the definition, glossary titles, definitions, scope notes,
|
|
877
|
+
history notes, and unknown reasons); skill bodies and descriptions are not part of it. Two runs
|
|
878
|
+
against the same unchanged host fingerprint produce the same sheet digest and the same digest for
|
|
879
|
+
every unchanged glossary, while bounded prose may differ. The fingerprint covers the host `HEAD`
|
|
880
|
+
and the content of tracked and untracked non-ignored files, excluding `.devboard/`, `.worktree/`,
|
|
881
|
+
`.omp/discover/`, `.omp/skills/`, VCS internals, and ignored files; the manifest stores it, and
|
|
882
|
+
`stale` is true only when the current fingerprint is readable and differs. Discover caches nothing:
|
|
883
|
+
every accepted start re-crawls.
|
|
884
|
+
|
|
885
|
+
The crawl is read-only with respect to the host. Its tools read files and safe read-only git facts
|
|
886
|
+
(`git rev-parse`, `git log`, `git branch`, `git status`, `git ls-files`) only; they never run a
|
|
887
|
+
project command, test, build, package manager, install, web fetch, or forge query, and the only
|
|
888
|
+
network traffic is the model endpoint that serves the peer. The crawl excludes `.devboard/`,
|
|
889
|
+
`.worktree/`, `.git/` internals, `.omp/`, dependency caches, build outputs, and ignored files, but
|
|
890
|
+
reads `README`, `AGENTS.md`, `GOAL.md`, `adrs/`, manifests, configuration, source, tests, and CI
|
|
891
|
+
files that remain. It never modifies `GOAL.md`, `adrs/`, or host source and never writes
|
|
892
|
+
`.omp/discover` or `.omp/skills` directly; its only write is the staging directory. Environment
|
|
893
|
+
and configuration names may be recorded, but their values, tokens, credentials, private URLs, and
|
|
894
|
+
URLs with user info are never copied into an artifact.
|
|
895
|
+
|
|
896
|
+
A Discover run is active work. While it runs, `startRun`, `startLifecycle`, `resumeLifecycle`,
|
|
897
|
+
`saveGoal`, `reviseGoal`, `createGoal`, `prepareSkills`, every Flow start, and a second
|
|
898
|
+
`discoverProject` are refused with one error naming the active run and spawn no peer; view and
|
|
899
|
+
tab switching, read-only commands, and `resetImplementation` stay available. While any Goal or
|
|
900
|
+
Flow work is active, `discoverProject` is refused with one `discoverError` naming the active work.
|
|
901
|
+
`restartServer` with `whenIdle: true` treats a Discover run as active and answers `upgradePending`,
|
|
902
|
+
and an immediate restart ends the recorded child, removes staging, publishes nothing, sends one
|
|
903
|
+
`discoverError` naming the interruption, sets the interrupted flag, and never auto-resumes.
|
|
904
|
+
`resetImplementation` cancels a run the same way and leaves `.omp/discover` and the Discover skills
|
|
905
|
+
untouched because reset preserves `.omp`. Only one run exists at a time; a stopped or interrupted
|
|
906
|
+
run leaves the previous artifacts readable, and a later start uses a fresh staging directory.
|
|
907
|
+
|
|
908
|
+
### Remote targets for manual testing
|
|
909
|
+
|
|
910
|
+
When the dashboard serves a harness source checkout — `<REPO_DIR>/.devboard/package.json` names
|
|
911
|
+
`@dsdenes/devboard` and `<REPO_DIR>/.devboard/node_modules/@dsdenes/devboard` does not exist —
|
|
912
|
+
Discover's Overview offers one extra target: a public GitHub repository supplied as a
|
|
913
|
+
URL. An installed host renders no such control and both new commands are refused. The predicate
|
|
914
|
+
reads no repository name, remote, or other host fact.
|
|
915
|
+
|
|
916
|
+
The field accepts exactly `https://github.com/<owner>/<repo>`, with an optional trailing `/` and an
|
|
917
|
+
optional `.git` suffix. Owner and repository are 1–100 letters, digits, dots, underscores, or
|
|
918
|
+
hyphens, and the whole URL is at most 2048 bytes. `http://`, `git@github.com:`, `ssh://`, another
|
|
919
|
+
host, a port, user information, a query, a fragment, an empty or whitespace-only value, an empty
|
|
920
|
+
owner or repository, `.`, `..`, more than two path segments, and absolute or Windows path shapes are
|
|
921
|
+
refused with one `discoverError` naming the reason; nothing is spawned and no directory is created.
|
|
922
|
+
Branch or ref selection, private repositories, and credentials are out of scope; any number of
|
|
923
|
+
public repositories may be retained.
|
|
924
|
+
|
|
925
|
+
The retained root is `<STATE_DIR>/discover-remote/` and must resolve inside `<REPO_DIR>`. It holds
|
|
926
|
+
one directory per discovered repository, named `<owner>-<repo>` in lowercase, and nothing else once
|
|
927
|
+
a run ends. Each directory keeps the bytes the crawl published — `.omp/discover/` and
|
|
928
|
+
`.omp/skills/` — beside `project.json`, which records `url`, `owner`, `repo`, and the ISO
|
|
929
|
+
`completedAt` time. A run's checkout is a transient `.staging-<run-id>` directory beside those
|
|
930
|
+
projects; it never survives the run. While a run is live it owns a claim at
|
|
931
|
+
`.runs/<checkout-name>.json` naming its process, and the claim and the `.runs/` directory leave
|
|
932
|
+
with the checkout.
|
|
933
|
+
|
|
934
|
+
A submit broadcasts `discoverState` with `cloning = { url }` before spawning anything, then runs
|
|
935
|
+
exactly one child: `git clone --depth 1 --single-branch --no-tags -- <url> .staging-<run-id>` with
|
|
936
|
+
the retained root as its working directory, `GIT_TERMINAL_PROMPT=0`, `GIT_ASKPASS=true`,
|
|
937
|
+
`NO_COLOR=1`, and a 300-second timeout. On success the checkout becomes the crawl root and exactly
|
|
938
|
+
one crawl runs against it. Publication writes into the checkout's `.omp`; when the crawl succeeds,
|
|
939
|
+
the two published trees are copied into `<owner>-<repo>/`, `project.json` is rewritten with the
|
|
940
|
+
submitted URL and the completion time, and the checkout is removed. The active target moves to the
|
|
941
|
+
new project only after its bytes are retained, so a failed clone or a failed crawl leaves the
|
|
942
|
+
previous target, its bytes, its entry, and its completion time untouched and reports exactly one
|
|
943
|
+
`discoverError`. A second `discoverRemote`, `discoverProject`, `switchDiscoverRemote`,
|
|
944
|
+
`removeDiscoverRemote`, or `clearDiscoverRemote` while `cloning` is non-null is refused with one
|
|
945
|
+
`discoverError`, as it is while a Discover run or other Goal/Flow work is active. A clone is never
|
|
946
|
+
resumed.
|
|
947
|
+
|
|
948
|
+
With a remote target every Discover read and write uses the retained directory: the Overview,
|
|
949
|
+
staleness, warnings, skill saves and restores, glossary decisions, and the manifest all come from
|
|
950
|
+
`<owner>-<repo>/.omp`. A crawl is the only path that needs a checkout; starting Discover on a
|
|
951
|
+
retained target — `discoverProject`, or `discoverRemote` with its URL — first clones the project
|
|
952
|
+
again, runs exactly one crawl, replaces the snapshot on success, and removes the checkout. Remote
|
|
953
|
+
grounding evidence is stored in the glossary, so the retained snapshot renders without the source
|
|
954
|
+
tree. This checkout's `.omp/discover` and `.omp/skills` stay byte-identical, and a remote operation
|
|
955
|
+
writes nothing outside `<STATE_DIR>/discover-remote/` apart from the dashboard's own state file.
|
|
956
|
+
|
|
957
|
+
Every discovery is retained, so the list holds exactly one entry per lowercased `owner/repo`:
|
|
958
|
+
discovering the same repository again in any letter case, with a trailing slash, or with a `.git`
|
|
959
|
+
suffix updates that entry's bytes, URL, and completion time and moves it to the top. Entries are
|
|
960
|
+
listed newest completed discovery first. `discoverState` carries this list as `retained`
|
|
961
|
+
(`{url, owner, repo, completedAt}[]`) and `target.checkout`, which is true only while the active
|
|
962
|
+
project's transient clone exists.
|
|
963
|
+
|
|
964
|
+
Durable state stores the active target as URL, owner, and repository only. A replacement restores
|
|
965
|
+
it only while `<owner>-<repo>` holds a readable `project.json` and both `.omp` trees, without a
|
|
966
|
+
clone or a re-crawl; otherwise it starts on the host target, drops that one stored target, keeps
|
|
967
|
+
every other retained entry, and sends one `discoverError` naming the missing project to the next
|
|
968
|
+
client. Startup removes every `.staging-*` directory and any leftover directory containing `.git`,
|
|
969
|
+
so a restart never resumes a run and never deletes a retained project. The one exception is a
|
|
970
|
+
transient another live dashboard still claims: two dashboards may serve the same repository with
|
|
971
|
+
one `STATE_DIR`, and a starting dashboard never deletes a checkout a running one owns. A restart
|
|
972
|
+
during a remote crawl interrupts it as a host run does: the recorded child ends, the checkout's
|
|
973
|
+
staging is removed, its retained snapshot stays, and nothing is published or resumed. A stopped
|
|
974
|
+
dashboard removes its own checkout and claim.
|
|
975
|
+
|
|
976
|
+
The protocol adds `{ "type": "discoverRemote", "url": string }`,
|
|
977
|
+
`{ "type": "switchDiscoverRemote", "url": string }`,
|
|
978
|
+
`{ "type": "removeDiscoverRemote", "url": string }`, and `{ "type": "clearDiscoverRemote" }`. A
|
|
979
|
+
malformed payload, a missing or non-string `url`, and an unknown command receive the existing
|
|
980
|
+
unknown-command treatment and change nothing; an unknown or malformed URL in a switch or removal
|
|
981
|
+
receives one `discoverError` naming it. `switchDiscoverRemote` changes the active target to a
|
|
982
|
+
retained project and renders its retained bytes without cloning, crawling, or network access;
|
|
983
|
+
`removeDiscoverRemote` deletes exactly that project's directory and list entry, returning to the
|
|
984
|
+
host target when it was the active one; `clearDiscoverRemote` returns to the host target and keeps
|
|
985
|
+
every project and its bytes. All three are refused with one `discoverError`, changing nothing,
|
|
986
|
+
while a clone or a Discover run is active or while the existing busy rules refuse
|
|
987
|
+
`clearDiscoverRemote`.
|
|
988
|
+
|
|
989
|
+
`discoverState` keeps every existing field and adds `remoteAvailable`, `retained`, `target`
|
|
990
|
+
(`{ "kind": "host" }` or `{ "kind": "remote", "url", "owner", "repo", "checkout" }`), and
|
|
991
|
+
`cloning` (`{ "url" }` or `null`). `getDiscover` answers with the same snapshot, and a connecting
|
|
992
|
+
client still receives it before its own command replies.
|
|
993
|
+
|
|
994
|
+
While `remoteAvailable` is true the Overview renders a target strip as its first region, above the
|
|
995
|
+
project header and the sheet or the empty state: a retained-project dropdown
|
|
996
|
+
(`data-testid="discover-remote-select"`) whose options are the retained URLs, newest first, with the
|
|
997
|
+
active project selected and no host option; one row per retained project with a remove control
|
|
998
|
+
(`data-testid="discover-remote-remove"`, carrying the URL in `data-url`); an input
|
|
999
|
+
(`data-testid="discover-remote-url"`) labelled for a GitHub repository URL with the placeholder
|
|
1000
|
+
`https://github.com/owner/repo`; a submit control (`data-testid="discover-remote-start"`) labelled
|
|
1001
|
+
`Discover from URL`; a banner (`data-testid="discover-remote-target"`) naming the active URL with a
|
|
1002
|
+
`Manual testing` marker; and a clear control (`data-testid="discover-remote-clear"`) only while the
|
|
1003
|
+
target is remote. The strip is secondary in weight yet first in reading order, so an operator sees
|
|
1004
|
+
the active target before anything it affects. A source checkout with nothing published still learns
|
|
1005
|
+
the gate: the server replays one `discoverState` on connect when `remoteAvailable` is true, even
|
|
1006
|
+
with no artifacts and no run. The submit control is disabled while the socket is closed, `cloning`
|
|
1007
|
+
is non-null, a run is active, or `discoverState.busy` is set, and its `title` names the reason; it
|
|
1008
|
+
is also disabled for empty or whitespace-only input. Submitting sends exactly one frame with the
|
|
1009
|
+
trimmed URL and starts nothing else; selecting an option sends exactly one `switchDiscoverRemote`
|
|
1010
|
+
and starts nothing else; removing sends exactly one `removeDiscoverRemote`; clearing sends exactly
|
|
1011
|
+
one `clearDiscoverRemote`. Rendering the strip on load, reconnect, mode switch, or page switch sends
|
|
1012
|
+
no frame. While cloning, the strip shows the cloning URL and the activity area shows no run; errors
|
|
1013
|
+
reach the Discover error and notification surfaces only.
|
|
1014
|
+
|
|
1015
|
+
`server/discover-remote.test.ts` proves retention after a successful crawl with the checkout
|
|
1016
|
+
removed, restore and rendering without a clone, newest-first ordering and one entry per lowercased
|
|
1017
|
+
`owner/repo`, switching with zero clone invocations across a restart, one clone and one crawl on
|
|
1018
|
+
re-discovery, unchanged bytes after a failed clone or crawl, clear keeping every project, removal
|
|
1019
|
+
of exactly one directory with a restart, busy refusals, the missing-retained-directory fallback, the
|
|
1020
|
+
gate refusing every remote command without touching the retained root, a byte-identical host
|
|
1021
|
+
`.omp`, and startup hygiene that keeps a live run's checkout when a second dashboard starts on the
|
|
1022
|
+
same state directory. `verify:interface` drives the built UI through a retained render after
|
|
1023
|
+
restart, the dropdown's two entries and selection, removal of one entry, and the gate-off host with
|
|
1024
|
+
no remote control.
|
|
1025
|
+
|
|
1026
|
+
Operator smoke procedure, which needs network and a model endpoint and is therefore not a pass gate:
|
|
1027
|
+
|
|
1028
|
+
```bash
|
|
1029
|
+
cd .devboard
|
|
1030
|
+
ADRS_DIR=../adrs REPO_DIR=.. bun run start
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
Open Discover, enter `https://github.com/octocat/Hello-World`, and confirm the cloning state, a
|
|
1034
|
+
completed crawl, the checkout gone from `.devboard/.tmp/discover-remote/`, the retained project at
|
|
1035
|
+
`.devboard/.tmp/discover-remote/octocat-hello-world/` with its `.omp/discover`, `.omp/skills`, and
|
|
1036
|
+
`project.json`, its sheet and glossaries, and a skill save landing under the retained `.omp/skills/`.
|
|
1037
|
+
Restart the server, reload, and confirm the same project is active with its artifacts and no
|
|
1038
|
+
missing-clone error. Discover a second repository, switch between the two from the dropdown, and
|
|
1039
|
+
confirm no clone appears. Remove one entry and confirm only its directory disappears. Hash `.omp`
|
|
1040
|
+
from the repository root before and after; the two hashes match.
|
|
1041
|
+
|
|
1042
|
+
## Publish a change
|
|
1043
|
+
|
|
1044
|
+
Merging into `main` and pushing to the canonical Gitea remote triggers
|
|
1045
|
+
`.gitea/workflows/devboard-release.yml`, which runs on the repository's registered Gitea
|
|
1046
|
+
`act_runner` — never a hosted runner, and never a fallback — and builds the client, runs the
|
|
1047
|
+
harness suite and typecheck, packs the complete public package, publishes
|
|
1048
|
+
`@dsdenes/devboard@<version>` to npmjs, and creates the Gitea release `v<version>` with notes and
|
|
1049
|
+
the same tarball plus its `.sha256` as a fallback. The version is `0.1.<commit-count>` over the
|
|
1050
|
+
full history; a rerun on the same commit leaves the existing npm version and release assets
|
|
1051
|
+
untouched. Nothing is published under `@dsdenes/devboard-harness`.
|
|
1052
|
+
|
|
1053
|
+
After the push, verify from the integration checkout:
|
|
1054
|
+
|
|
1055
|
+
```bash
|
|
1056
|
+
cd .devboard
|
|
1057
|
+
bun run publish
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
`bun run publish` refuses a worktree, a branch other than `main`, or a checkout ahead of or
|
|
1061
|
+
diverged from its canonical remote; fetches and fast-forwards `main` without discarding
|
|
1062
|
+
uncommitted work; installs dependencies and rebuilds `dist/`; waits for the release workflow of
|
|
1063
|
+
`HEAD`; verifies the Gitea release assets and `npm view @dsdenes/devboard@<version> version`;
|
|
1064
|
+
pushes the `github` mirror remote and proves its `main` names the published commit; and reports
|
|
1065
|
+
every running dashboard's running and installed version. It never restarts a dashboard and never
|
|
1066
|
+
writes into a host. `--wait-timeout N` bounds the workflow and propagation wait (default 1800
|
|
1067
|
+
seconds).
|
|
1068
|
+
|
|
1069
|
+
Installed dashboards update themselves: each checks the newest npm version at startup and every
|
|
1070
|
+
60,000 ms (`DEVBOARD_UPDATE_INTERVAL_MS` overrides it), installs the npm artifact through a
|
|
1071
|
+
staging prefix, and keeps serving its loaded code. If npm cannot serve the artifact and
|
|
1072
|
+
`DEVBOARD_GITEA_URL` is configured, the same version's Gitea tarball is verified against its
|
|
1073
|
+
`.sha256` and installed instead. The npm child always runs in
|
|
1074
|
+
that staging directory, and the dashboard starts from the host root rather than the replaceable
|
|
1075
|
+
package directory, so replacing the package never leaves a child with a deleted working
|
|
1076
|
+
directory. `versionState` reports `running`, `installed`, `latest`, and `stale`; the reload
|
|
1077
|
+
control appears while `stale` is true and restarts the server from the installed package. A
|
|
1078
|
+
dashboard serving the canonical source checkout reports `0.1.<commit-count>` on both sides, the
|
|
1079
|
+
version its `main` commit releases as, rather than the checked-in `0.1.0` placeholder.
|
|
1080
|
+
|
|
1081
|
+
A check that cannot use the feed — an offline host, an unreachable Gitea API, a DNS or proxy
|
|
1082
|
+
failure, a refused or reset connection, a timeout, or an answer that is not a readable release
|
|
1083
|
+
description (a proxy page, a truncated body, an unexpected payload) — leaves the previous package
|
|
1084
|
+
and server intact, writes an `activity` record naming the skipped check, and retries with
|
|
1085
|
+
exponential backoff (`DEVBOARD_UPDATE_BACKOFF_MAX_MS` caps it); it is context for the operator,
|
|
1086
|
+
never an `update-error` finding. A check that reaches the feed and fails, a release without a
|
|
1087
|
+
tag, a checksum mismatch, an authentication failure, or a failed npm install writes `update-error`
|
|
1088
|
+
and is retried the same way.
|
|
1089
|
+
|
|
1090
|
+
|Variable|Default|Purpose|
|
|
1091
|
+
|---|---|---|
|
|
1092
|
+
|`HOST`|`127.0.0.1`|Listen address.|
|
|
1093
|
+
|`PORT`|`8080`|Listen port.|
|
|
1094
|
+
|`ADRS_DIR`|`/repo/adrs`|Directory holding the ADR files.|
|
|
1095
|
+
|`REPO_DIR`|parent of `ADRS_DIR`|Agent working directory and default goal location.|
|
|
1096
|
+
|`GOAL_FILE`|`<REPO_DIR>/GOAL.md`|Goal document read on load and replaced on save.|
|
|
1097
|
+
|`STATIC_DIR`|`dist`|Built client directory.|
|
|
1098
|
+
|`AGENT_COMMAND`|`omp acp`|ACP peer command, split on whitespace. `server/fixtures/stub-agent.ts` is a deterministic substitute for tests and local verification.|
|
|
1099
|
+
|`STATE_DIR`|`<harness>/.tmp`|State and journal directory; an installed dashboard passes `<host>/.devboard/.tmp`.|
|
|
1100
|
+
|`DEVBOARD_UPDATE_INTERVAL_MS`|`60000`|Automatic update check interval.|
|
|
1101
|
+
|`DEVBOARD_UPDATE_BACKOFF_MAX_MS`|`3600000`|Ceiling for the exponential retry delay after a failed check or install.|
|
|
1102
|
+
|`DEVBOARD_RELEASE_DIR`|unset|Local release directory for offline verification; unset reads the newest Gitea release over the public HTTP API.|
|
|
1103
|
+
|`DEVBOARD_GITEA_URL` / `GITEA_URL`|unset|Gitea HTTP origin; an HTTP(S) `origin` remote supplies it when unset.|
|
|
1104
|
+
|`GITEA_TOKEN`|unset|Gitea credential for issue, PR, scout, and watchdog writes; public reads need none.|
|
|
1105
|
+
|
|
1106
|
+
## Self-hosted runner
|
|
1107
|
+
|
|
1108
|
+
The release workflow's only job runs on an `act_runner` registered to one machine and selected by
|
|
1109
|
+
the `host` label the runner advertises. A machine that lacks the label never picks up the job, and
|
|
1110
|
+
the job never falls back to a hosted runner. The registration is a one-time operator procedure
|
|
1111
|
+
against the canonical Gitea instance at `http://localhost:3001`; substitute the repository
|
|
1112
|
+
(`<owner>/<repo>`), the dedicated account (`<user>`), its home directory (`<home>`), and the
|
|
1113
|
+
runner name (`<runner-name>`) everywhere they appear.
|
|
1114
|
+
|
|
1115
|
+
The account is separate from every human account and holds no group that grants privilege — not
|
|
1116
|
+
`sudo`, `docker`, or `adm` — because the runner executes repository code and every secret the
|
|
1117
|
+
workflow is given.
|
|
1118
|
+
|
|
1119
|
+
```bash
|
|
1120
|
+
sudo useradd --create-home --shell /bin/bash --user-group <user>
|
|
1121
|
+
sudo passwd --lock <user>
|
|
1122
|
+
sudo install -d -m 700 -o <user> -g <user> <home>
|
|
1123
|
+
sudo install -d -m 755 -o <user> -g <user> <home>/actions-runner
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
Download the official runner release and unpack it into that directory as `<user>`. The machine
|
|
1127
|
+
needs `git`, `curl`, and `tar`; everything else the workflow uses is installed by its `uses:`
|
|
1128
|
+
steps into the runner's own tool cache.
|
|
1129
|
+
|
|
1130
|
+
```bash
|
|
1131
|
+
VERSION=$(gh api repos/actions/runner/releases/latest --jq '.tag_name | ltrimstr("v")')
|
|
1132
|
+
sudo -u <user> curl -fsSL -o <home>/actions-runner/runner.tar.gz \
|
|
1133
|
+
"https://github.com/actions/runner/releases/download/v$VERSION/actions-runner-linux-x64-$VERSION.tar.gz"
|
|
1134
|
+
sudo -u <user> tar -xzf <home>/actions-runner/runner.tar.gz -C <home>/actions-runner
|
|
1135
|
+
sudo -u <user> rm <home>/actions-runner/runner.tar.gz
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
Register with a registration token minted for the repository. The token is valid for one hour;
|
|
1139
|
+
never write it into a file, a commit, or a log.
|
|
1140
|
+
|
|
1141
|
+
```bash
|
|
1142
|
+
TOKEN=$(gh api -X POST repos/<owner>/<repo>/actions/runners/registration-token --jq .token)
|
|
1143
|
+
sudo -u <user> -- bash -c "cd <home>/actions-runner && ./config.sh \
|
|
1144
|
+
--url https://github.com/<owner>/<repo> --token $TOKEN \
|
|
1145
|
+
--name <runner-name> --labels devboard --work _work --unattended"
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
`--labels devboard` adds to the automatic labels rather than replacing them, `--work _work` keeps
|
|
1149
|
+
checkouts under `<home>/actions-runner/_work`, and `--unattended` keeps the setup non-interactive.
|
|
1150
|
+
Install the runner as a system service owned by `<user>`, so it starts at boot and restarts after
|
|
1151
|
+
a failure without an operator session:
|
|
1152
|
+
|
|
1153
|
+
```bash
|
|
1154
|
+
sudo bash -c "cd <home>/actions-runner && ./svc.sh install <user>"
|
|
1155
|
+
sudo systemctl enable --now actions.runner.<owner>-<repo>.<runner-name>.service
|
|
1156
|
+
systemctl status actions.runner.<owner>-<repo>.<runner-name>.service
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
Verify the registration from any machine whose `gh` is authenticated against the repository; the
|
|
1160
|
+
runner must be `online`, idle, and carry exactly the four labels the job asks for:
|
|
1161
|
+
|
|
1162
|
+
```bash
|
|
1163
|
+
gh api repos/<owner>/<repo>/actions/runners \
|
|
1164
|
+
--jq '.runners[] | {name, status, busy, labels: [.labels[].name]}'
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
Because the service restarts on failure and at boot, `systemctl is-active` and `systemctl
|
|
1168
|
+
is-enabled` are the ongoing health check; a restart of the unit must return `status` to `online`
|
|
1169
|
+
within two minutes. The job's own checkouts stay in `<home>/actions-runner/_work/<repo>/<repo>`
|
|
1170
|
+
and are readable evidence that a run executed here.
|
|
1171
|
+
|
|
1172
|
+
To remove the runner, stop the service before deregistering, then delete the account:
|
|
1173
|
+
|
|
1174
|
+
```bash
|
|
1175
|
+
sudo bash -c "cd <home>/actions-runner && ./svc.sh uninstall"
|
|
1176
|
+
TOKEN=$(gh api -X POST repos/<owner>/<repo>/actions/runners/remove-token --jq .token)
|
|
1177
|
+
sudo -u <user> -- bash -c "cd <home>/actions-runner && ./config.sh remove --token $TOKEN"
|
|
1178
|
+
sudo userdel --remove <user>
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
## Harness watchdog
|
|
1182
|
+
|
|
1183
|
+
A machine-level systemd user timer runs the watchdog independently of every dashboard: at
|
|
1184
|
+
minutes 0, 10, 20, 30, 40, and 50 of every hour (`OnCalendar=*:0/10`, `Persistent=true`), the
|
|
1185
|
+
`devboard-watchdog.service` oneshot runs `bun run watchdog -- --once` from the canonical harness.
|
|
1186
|
+
It survives logout (`Linger=yes`) and never starts, stops, or restarts a dashboard itself.
|
|
1187
|
+
|
|
1188
|
+
Each cycle detects and reports; it never fixes. It reads the ten minutes before cycle start, both
|
|
1189
|
+
ends inclusive, from:
|
|
1190
|
+
|
|
1191
|
+
- every live dashboard's JSONL journal, `<STATE_DIR>/journal/<24-hex>.jsonl`, whose key is the
|
|
1192
|
+
first 24 hex characters of `sha256(REPO_DIR)`. `STATE_DIR` is the dashboard's environment
|
|
1193
|
+
value, `<host>/.devboard/.tmp` for an installed host. Each record carries `ts` (UTC ISO
|
|
1194
|
+
8601), `repo`, `port`, `pid`, `kind`, and `text`; kinds are `activity`, `question`,
|
|
1195
|
+
`run-start`, `run-stop`, `lane-start`, `lane-stop`, `lifecycle`, `acp-stderr`,
|
|
1196
|
+
`server-error`, `update`, and `update-error`. A `question` record also carries `scope`, the
|
|
1197
|
+
run, lane, or peer that asked, so questions from separate work streams never accumulate into
|
|
1198
|
+
one finding. A live journal rotates at 8 MiB instead of truncating.
|
|
1199
|
+
- omp's own warnings and errors under `~/.omp/logs/omp.*.log`, attributed to an instance only
|
|
1200
|
+
when that pid descends from the instance's server process. A shared working directory never
|
|
1201
|
+
attributes a line, so an operator's own omp session in the project directory is not dashboard
|
|
1202
|
+
evidence.
|
|
1203
|
+
|
|
1204
|
+
Retention is bounded at cycle start. The journal keeps its own rule: journal files older than
|
|
1205
|
+
seven days are deleted, and the total under `<STATE_DIR>/journal` is capped at 1 GiB by deleting
|
|
1206
|
+
the oldest complete files first. A file still holding records inside the live window always wins
|
|
1207
|
+
over that bound, so the current cycle never loses evidence. The target is each install's real
|
|
1208
|
+
state directory — `<host>/.devboard/.tmp/journal` for an installed host, never the serving package
|
|
1209
|
+
directory under `node_modules`.
|
|
1210
|
+
|
|
1211
|
+
### State-directory cleanup
|
|
1212
|
+
|
|
1213
|
+
Every cycle also sweeps the rest of each covered state directory. Covered directories are the
|
|
1214
|
+
canonical checkout's `.devboard/.tmp`, every `*/.devboard/.tmp` directly under the canonical
|
|
1215
|
+
root's parent, and the `STATE_DIR` of every live dashboard the cycle discovers, de-duplicated by
|
|
1216
|
+
real path. A symlinked `.tmp` is never followed, and a missing directory is skipped.
|
|
1217
|
+
|
|
1218
|
+
An entry is deleted when its last-modified time is older than seven days: files and symlinks
|
|
1219
|
+
individually, and a directory only when it is empty after that deletion and its own mtime, read
|
|
1220
|
+
before the sweep, is older than the cutoff. A fresh file inside an old directory survives and
|
|
1221
|
+
keeps its ancestors. Old watchdog run directories, legacy orchestrator run files and
|
|
1222
|
+
`orchestrator.log`, legacy `devboard-*.{log,state,lock}` files, verification scratch, and stray
|
|
1223
|
+
stale files are therefore removed after a week. These entries are never deleted at any age:
|
|
1224
|
+
|
|
1225
|
+
- `.tmp/state/` and everything below it.
|
|
1226
|
+
- `.tmp/journal/`, which the journal rule above governs.
|
|
1227
|
+
- `.tmp/watchdog/state.json`, `.tmp/watchdog/latest.json`, and `.tmp/watchdog/lock`.
|
|
1228
|
+
- `.tmp/orchestrator/state.json`, `.tmp/orchestrator/ledger.json`, and `.tmp/orchestrator/lock`,
|
|
1229
|
+
which the first Issues Start still reads to migrate handled issue numbers.
|
|
1230
|
+
- `.tmp/issues/` in full: every repo-scoped state, ledger, run, activity journal, control file,
|
|
1231
|
+
and the repo-anchored worker lock, including the run directory a running worker names.
|
|
1232
|
+
- `.tmp/flow/` in full: every repository key and session record, including the machine state,
|
|
1233
|
+
reviewable artifacts, and worktree/PR bookkeeping Flow restore and History read.
|
|
1234
|
+
- the run directory of the cycle performing the sweep.
|
|
1235
|
+
- a `devboard-<pid>.lock` whose pid is still alive.
|
|
1236
|
+
|
|
1237
|
+
Run it by hand from `.devboard`:
|
|
1238
|
+
|
|
1239
|
+
```bash
|
|
1240
|
+
bun run cleanup -- --dry-run --root <root> # list every candidate path and byte size
|
|
1241
|
+
bun run cleanup -- --once --root <root> # delete and print one summary per directory
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
`--dry-run` is also the default when neither flag is given, and it writes nothing. The watchdog
|
|
1245
|
+
cycle performs the same sweep and appends one `cleanup <state-dir>: removed N file(s), ...` line
|
|
1246
|
+
per covered directory to `coordinator.log`, including directories with zero removals. The sweep
|
|
1247
|
+
deletes outside the canonical checkout only inside the `.devboard/.tmp` directories it covers;
|
|
1248
|
+
it creates nothing outside the checkout.
|
|
1249
|
+
|
|
1250
|
+
Discovery scans `/proc` for every process running `server/index.ts` that supplies `REPO_DIR` and
|
|
1251
|
+
`PORT`, de-duplicates by `(REPO_DIR, PORT)`, resolves the serving harness from `STATIC_DIR` or
|
|
1252
|
+
the process working directory, and reads the journal from the process's `STATE_DIR`. Instances
|
|
1253
|
+
from installed hosts and the canonical source checkout are covered alike; an instance whose
|
|
1254
|
+
state directory has no journal is reported `uninstrumented`.
|
|
1255
|
+
|
|
1256
|
+
Findings derive only from machine-checkable evidence: a `server-error`, `update-error`, or
|
|
1257
|
+
`acp-stderr` journal record; a `lane-stop` whose status token is `failed`; an omp log record at
|
|
1258
|
+
`error`, `warn`, or `fatal`; a `run-start` or `lane-start` at least five minutes before window end
|
|
1259
|
+
with no `run-stop` or `lane-stop` at or after it for the same instance (a stall); and three or
|
|
1260
|
+
more `question` records for one instance's run, lane, or peer in one window (a question loop,
|
|
1261
|
+
quoting the last).
|
|
1262
|
+
`activity`, `lifecycle`, `update`, ordinary run and lane text, and questions below the loop
|
|
1263
|
+
threshold never produce a finding, whatever words they contain.
|
|
1264
|
+
|
|
1265
|
+
A fingerprint is the first 32 hex characters of `sha256(category + "\n" + normalized line)`. The
|
|
1266
|
+
normalized line drops a leading ISO timestamp and collapses whitespace, and the source path is
|
|
1267
|
+
excluded, so the same error on two dashboards is one fingerprint and one issue.
|
|
1268
|
+
|
|
1269
|
+
Each cycle files one issue per new fingerprint through the operator's authenticated `gh`:
|
|
1270
|
+
`gh issue create --repo dsdenes/devboard --title "[watchdog] <category>: <line>" --body-file -`.
|
|
1271
|
+
The title is one line of at most 80 characters. The body carries the category, fingerprint,
|
|
1272
|
+
quote, first and last sighting, count, window, every contributing instance (repository, port,
|
|
1273
|
+
pid, journal path), and the cycle report path. There is no per-cycle cap. A failing `gh` call
|
|
1274
|
+
reports `failed` with its first stderr line and leaves the fingerprint in `pendingFiling`; the
|
|
1275
|
+
next cycle retries pending fingerprints before new ones, so a transient failure never loses
|
|
1276
|
+
evidence. A fingerprint already `filed:<url>` is never filed again: when it recurs at least 24
|
|
1277
|
+
hours after the last comment (or after the filing when no comment exists), the cycle comments
|
|
1278
|
+
once on the existing issue with the new window, sighting, and count. No cycle closes an issue.
|
|
1279
|
+
|
|
1280
|
+
Each cycle writes one run directory, `<harness>/.tmp/watchdog/runs/<UTC timestamp>/`, containing
|
|
1281
|
+
`report.json`, `report.md`, and `coordinator.log`; the newest report is also
|
|
1282
|
+
`.tmp/watchdog/latest.json`. A version 2 ledger at `.tmp/watchdog/state.json` records each
|
|
1283
|
+
finding's first and last sighting, count, contributing instances, and `filed:<url>`; an unchanged
|
|
1284
|
+
window reports `action: "duplicate"` and makes no `gh` call. A version 1 ledger's prose
|
|
1285
|
+
`blocked:*` and `published:<sha>` dispositions are discarded on load, never migrated into issues.
|
|
1286
|
+
|
|
1287
|
+
A singleton lock at `.tmp/watchdog/lock` serializes every invocation: a second manual run or a
|
|
1288
|
+
`systemctl --user start devboard-watchdog.service` while a cycle is live prints `already running`
|
|
1289
|
+
and exits without filing. A stale lock whose pid is gone is reclaimed.
|
|
1290
|
+
|
|
1291
|
+
The cycle never spawns an agent, creates a worktree, commits, merges, pushes, or publishes; it
|
|
1292
|
+
leaves `HEAD`, `origin/main`, the worktree list, and the working tree exactly as it found them.
|
|
1293
|
+
|
|
1294
|
+
### The Watchdog page
|
|
1295
|
+
|
|
1296
|
+
The Issues sidebar lists `Watchdog` (`open-issues-watchdog`) directly after Auto Scout. Activating
|
|
1297
|
+
it opens the full workspace page `watchdog-page`, titled `Watchdog`, in place of the Overview tab;
|
|
1298
|
+
it stays the Issues mode's active tab until another Issues page is opened. The page reads only
|
|
1299
|
+
`GET /api/watchdog` and sends no command that files, comments, closes, starts, stops, or fixes
|
|
1300
|
+
anything.
|
|
1301
|
+
|
|
1302
|
+
The page is one framed header, one full-width `watchdog-last-cycle` summary band, and two panels
|
|
1303
|
+
under it. The summary leads with the action badge and the reason, then states the exact start, the
|
|
1304
|
+
findings, and the pending filings as one compact facts row. The filed-issue table takes the wider
|
|
1305
|
+
column on a viewport at least 1501px wide; below that the two panels stack as flexible rows. Below
|
|
1306
|
+
1100px the issue `Category` column is hidden before any value is clipped (the category repeats
|
|
1307
|
+
inside the generated title); it stays in the DOM. Both panels own their scrolling, so the 100-row
|
|
1308
|
+
history never pushes the filed issues below the page.
|
|
1309
|
+
|
|
1310
|
+
`watchdog-last-cycle` is the newest cycle from `latest.json`: `watchdog-last-cycle-at` (a `<time>`,
|
|
1311
|
+
or `never` before the first cycle), `watchdog-last-action`, `watchdog-last-findings`,
|
|
1312
|
+
`watchdog-last-pending`, and `watchdog-last-reason` only when the report carries a reason. The
|
|
1313
|
+
panel's `watchdog-last-age` shows that cycle's age.
|
|
1314
|
+
|
|
1315
|
+
`watchdog-cycles` lists the retained run directories under `<STATE_DIR>/watchdog/runs`, newest
|
|
1316
|
+
first, capped at the newest 100 directories. Its table labels the three columns `Started`, `Action`,
|
|
1317
|
+
and `Issues`, and its header sticks while the panel scrolls. Every `watchdog-cycle` row carries
|
|
1318
|
+
`data-action` and renders `watchdog-cycle-at`, `watchdog-cycle-action`, and, only when the cycle's
|
|
1319
|
+
findings map to a filed issue, a `watchdog-cycle-issues` group of `watchdog-cycle-issue` anchors to
|
|
1320
|
+
those GitHub issues. Each anchor reads `#<number>` and opens in a new tab; the row repeats no issue
|
|
1321
|
+
and keeps the finding order. A cycle row renders no window interval, finding count, or reason. A
|
|
1322
|
+
run directory whose `report.json` is missing or unparsable is skipped without hiding the other
|
|
1323
|
+
rows. With no run directory at all, the `latest.json` cycle is the single row; with no cycle
|
|
1324
|
+
anywhere, `watchdog-cycles-empty` renders `No cycles yet` and the ten-minute cadence.
|
|
1325
|
+
|
|
1326
|
+
`watchdog-issues` lists every ledger entry whose disposition is `filed:<url>`, newest `filedAt`
|
|
1327
|
+
first and ties ordered by URL. Each `watchdog-issue` row carries `data-url`, `data-state`, and
|
|
1328
|
+
`data-version` and renders `watchdog-issue-title` (an anchor to the issue reading
|
|
1329
|
+
`[watchdog] <category>: <line>`), `watchdog-issue-category`, `watchdog-issue-filed` (a `<time>`),
|
|
1330
|
+
`watchdog-issue-state`, and `watchdog-issue-version` (the tag, or `—`). A pending-only fingerprint
|
|
1331
|
+
is counted in `watchdog-last-pending` and never becomes a row; `watchdog-issues-empty` renders
|
|
1332
|
+
when nothing is filed. `watchdog-issues-count` summarizes the rows and pending filings.
|
|
1333
|
+
|
|
1334
|
+
The server prints `watchdog-issue-state` as `pending` for an open issue and `implemented` for a
|
|
1335
|
+
closed one, whatever its `stateReason`; a reopened issue returns to `pending` within one refresh
|
|
1336
|
+
interval. State is read from the repository in each ledger URL. For an implemented issue the
|
|
1337
|
+
server resolves the closing commit from the issue timeline, or the last 7–40 character hex SHA in
|
|
1338
|
+
the newest comment carrying one, resolved to one full commit. The version is the earliest
|
|
1339
|
+
published release of that issue's repository whose history contains the commit, ordered by
|
|
1340
|
+
`publishedAt` and breaking a tie by tag, or `—` when no commit or release resolves. A blank
|
|
1341
|
+
version is recomputed on every read, so a release published later fills it.
|
|
1342
|
+
|
|
1343
|
+
The server refreshes its GitHub projection at most once per 60 seconds. Any `GET /api/watchdog`
|
|
1344
|
+
request triggers a due refresh in the background and is answered from the last projection without
|
|
1345
|
+
waiting for `gh`; the refresh result appears on a later request. A failed read keeps every last
|
|
1346
|
+
known state and version and the whole table, and sets `watchdog-sync` to `data-state="error"` with
|
|
1347
|
+
the reason in `watchdog-sync-error`. A success sets `data-state="ok"`, clears the reason, and
|
|
1348
|
+
updates `watchdog-sync-at`; a success older than 120 seconds with no later failure reports
|
|
1349
|
+
`stale`. `watchdog-sync-at` reads `never` before the first success. The page polls every 15
|
|
1350
|
+
seconds, so a newly written run directory or filed ledger entry appears without a reload.
|
|
1351
|
+
|
|
1352
|
+
`GET /api/watchdog` is GET-only and leaves `<STATE_DIR>/watchdog/` byte-identical: no run
|
|
1353
|
+
directory, ledger write, or lock. Its existing fields keep their meaning: `cycle` (ISO 8601 or
|
|
1354
|
+
null), `action`, the optional `reason`, the `findings` count, `issues`
|
|
1355
|
+
(`{url, category, line, filedAt, state, version}`) newest first, and the `pending` count; it adds
|
|
1356
|
+
`cycles` (each `{at, window, action, findings, issues, reason?}`, where `issues` lists the
|
|
1357
|
+
connected GitHub issue URLs) and `sync`. A dashboard-endpoint failure renders `watchdog-error`.
|
|
1358
|
+
|
|
1359
|
+
The status bar's watchdog indicator is unchanged: `status-watchdog` shows the last action and its
|
|
1360
|
+
age; activating it opens `watchdog-popover` listing up to ten filed issues, each link opening in a
|
|
1361
|
+
new tab, and Escape closes it.
|
|
1362
|
+
|
|
1363
|
+
Operate it from the canonical `.devboard`:
|
|
1364
|
+
|
|
1365
|
+
```bash
|
|
1366
|
+
bun run watchdog:install # write the two units, enable and start the timer
|
|
1367
|
+
bun run watchdog -- --once # run one cycle now
|
|
1368
|
+
bun run watchdog -- --collect --dry-run # print this cycle's report; change nothing
|
|
1369
|
+
bun run watchdog:uninstall # stop, disable, and remove the units
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
`watchdog:install` refuses a worktree, verifies `Linger=yes` before writing anything, discovers
|
|
1373
|
+
the absolute Bun executable plus the `omp` and `git` directories for the unit's `PATH`, and is
|
|
1374
|
+
idempotent. `watchdog:uninstall` changes no repository file. To pause the watchdog without
|
|
1375
|
+
uninstalling, `systemctl --user stop devboard-watchdog.timer`; `systemctl --user start
|
|
1376
|
+
devboard-watchdog.timer` resumes it. `bun run verify:watchdog` builds the client, seeds run
|
|
1377
|
+
directories, a ledger, and a stub `gh`, and proves the Watchdog page's empty states, cycle history,
|
|
1378
|
+
state and version columns, sync badge and failure text, 60-second refresh, freshness, and the
|
|
1379
|
+
unchanged footer indicator, popover, issue links, and Escape. `bun run verify:watchdog-live`
|
|
1380
|
+
starts a seeded dashboard, waits for the installed timer to file a real issue, and closes it as not
|
|
1381
|
+
planned.
|
|
1382
|
+
|
|
1383
|
+
## Issues mode
|
|
1384
|
+
|
|
1385
|
+
Issues is the fourth mode in the activity rail, below Goal. Each dashboard controls exactly one
|
|
1386
|
+
repository: its own `REPO_DIR`. Starting Issues in one checkout's dashboard polls that checkout's
|
|
1387
|
+
origin open issues and starts one ACP agent for every open, unhandled issue in that same poll. Each
|
|
1388
|
+
issue without the exact `goal` label receives exactly `/implement-issue <number>`; each
|
|
1389
|
+
`goal`-labeled issue receives exactly `/implement-goal-issue <number>`. There is no concurrency
|
|
1390
|
+
cap: ten ready issues start ten peers, and no peer waits for another to finish. A detached worker
|
|
1391
|
+
owns the poll loop, the peers, the ledger, the lock, and the activity for one repository, so it
|
|
1392
|
+
survives a dashboard restart and two repositories never share a worker. No systemd unit, timer, or
|
|
1393
|
+
machine-level scheduler is involved, and neither starting nor restarting a dashboard starts a
|
|
1394
|
+
worker.
|
|
1395
|
+
|
|
1396
|
+
Start first proves the facts it needs: a canonical checkout root that is not a linked worktree,
|
|
1397
|
+
a GitHub `origin`, `omp` and `gh` on `PATH`, the `/implement-issue` command installed for the
|
|
1398
|
+
operator's omp, and the `/implement-goal-issue` extension installed beside it. Commands are
|
|
1399
|
+
discovered in `<REPO_DIR>/.omp/commands/`, `<REPO_DIR>/.omp/agent/commands/`,
|
|
1400
|
+
`$HOME/.omp/commands/`, and `$HOME/.omp/agent/commands/`; the extension in
|
|
1401
|
+
`<REPO_DIR>/.omp/extensions/` or `$HOME/.omp/extensions/`. A refusal names the missing file, every
|
|
1402
|
+
path searched, and `.devboard/install.sh` as the remedy, spawns no peer, and writes no run. Start
|
|
1403
|
+
also requires `gh api repos/<owner>/<repo> --jq .permissions.push` to return `true`. A live legacy
|
|
1404
|
+
`devboard-orchestrator` lock is refused too; the harness never stops that process. Issues mode no
|
|
1405
|
+
longer requires `/implement-all-issues`; that command stays usable by hand outside the mode.
|
|
1406
|
+
|
|
1407
|
+
The worker is spawned detached in its own session from the harness package the server loaded,
|
|
1408
|
+
with `REPO_DIR` as its working directory. It holds `<REPO_DIR>/.devboard/.tmp/issues/lock`, which
|
|
1409
|
+
records its pid, argv, state directory, and harness version. A lock whose owner is gone is
|
|
1410
|
+
reclaimed before a new worker starts; a lock with a live owner is never taken over. A second
|
|
1411
|
+
dashboard with the same `REPO_DIR` and `STATE_DIR` attaches to that worker. A second dashboard
|
|
1412
|
+
with the same `REPO_DIR` and another `STATE_DIR` cannot attach and is refused, because the
|
|
1413
|
+
repo-anchored lock names the live worker. Different repositories get independent workers.
|
|
1414
|
+
|
|
1415
|
+
The poll is the GitHub issues endpoint through the operator's `gh` credential every 10 seconds,
|
|
1416
|
+
conditional on the previous `ETag` (`If-None-Match`). A `304` is no change; `403`/`429` honor
|
|
1417
|
+
`Retry-After` or `X-RateLimit-Reset`. Pull requests returned by the endpoint are excluded, and
|
|
1418
|
+
every issue's labels are part of the poll result. Every open issue in the queue carries `handled`,
|
|
1419
|
+
the run id that last handled it, and `agentRunId` while an agent works it. An issue that already
|
|
1420
|
+
has a running agent never receives a second peer, so a poll while four peers run starts none of
|
|
1421
|
+
them again. An issue first seen while three peers run gets its own peer on the next poll without
|
|
1422
|
+
pausing, restarting, or shortening the others. A handled issue is never retriggered while it stays
|
|
1423
|
+
open; a closed issue leaves the ledger, so a reopened one is eligible again. A poll with no
|
|
1424
|
+
unhandled open issue starts no peer and publishes `idle`.
|
|
1425
|
+
|
|
1426
|
+
Session updates stream into the mode, permission requests are auto-approved by the ACP client,
|
|
1427
|
+
elicitation questions are answered per agent, and every peer closes when its run ends or Stop is
|
|
1428
|
+
clicked. The agents own every worktree, commit, push, issue closure, and repository mutation; the
|
|
1429
|
+
worker passes no scope beyond the issue number and never edits code itself.
|
|
1430
|
+
|
|
1431
|
+
### The `/implement-issue` contract
|
|
1432
|
+
|
|
1433
|
+
`.devboard/prompts/implement-issue.md` holds the whole contract and
|
|
1434
|
+
`.devboard/omp/commands/implement-issue.md` is its command wrapper: it carries a `description` and
|
|
1435
|
+
an `argument-hint`, reads the prompt, and passes `$ARGUMENTS` as the issue number. No other
|
|
1436
|
+
harness file duplicates the contract.
|
|
1437
|
+
|
|
1438
|
+
The prompt requires the argument to be exactly one positive integer issue number and refuses a
|
|
1439
|
+
missing, non-numeric, or multi-token argument before changing anything. It fetches exactly that
|
|
1440
|
+
one issue and never fetches, surveys, or builds a dependency graph for the rest of the backlog.
|
|
1441
|
+
It creates and works in one worktree under `<REPO_DIR>/.worktree/` named for the issue, never in
|
|
1442
|
+
the canonical checkout and never in another agent's worktree, and writes nothing outside
|
|
1443
|
+
`<REPO_DIR>`. It changes only that issue's implementation, tests, and documentation; it may read
|
|
1444
|
+
other issues and code but never edits, comments on, closes, or reopens another issue. When the
|
|
1445
|
+
issue declares an open prerequisite (`depends on`, `blocked by`, or a task-list issue reference),
|
|
1446
|
+
it writes `blocked: #<prerequisite>` as the first stderr line, exits nonzero, leaves the issue open
|
|
1447
|
+
and unhandled, and never implements the prerequisite in another agent's place. It is
|
|
1448
|
+
non-interactive: no `question` tool, no confirmation, no interactive editor, no login prompt. It
|
|
1449
|
+
fetches the canonical integration line, integrates concurrent changes, resolves every conflict
|
|
1450
|
+
itself, reruns every check the integration invalidates, and publishes directly to that line with
|
|
1451
|
+
bounded retry when a push is rejected; it never creates a pull request or merge request and never
|
|
1452
|
+
force-pushes. It closes the issue only after verifying the pushed revision is on the canonical
|
|
1453
|
+
line and the required checks pass, and comments on that issue with the revision and the
|
|
1454
|
+
verification evidence. A proven duplicate, invalid, or already-fixed issue is closed with evidence
|
|
1455
|
+
and without committing unrelated work. A successful agent removes only the worktree and branch it
|
|
1456
|
+
created; a failed or blocked agent leaves its worktree in place and reports its path.
|
|
1457
|
+
|
|
1458
|
+
The `goal` label keeps its goal-mode implementation contract: every goal-labeled issue runs
|
|
1459
|
+
`/implement-goal-issue <number>` concurrently with the rest.
|
|
1460
|
+
|
|
1461
|
+
### Per-agent state, lifecycle, and history
|
|
1462
|
+
|
|
1463
|
+
Each agent has its own run directory, `run.json`, `activity.json`, first-stderr line, question,
|
|
1464
|
+
ACP child record, and retry gate keyed by issue number. No agent shares a run record or question
|
|
1465
|
+
slot with another. The published state carries `agents`, one entry per running issue with `runId`,
|
|
1466
|
+
`issueNumber`, `startedAt`, `question`, and `acp`; the queue carries each issue's `handled`, last
|
|
1467
|
+
handling `runId`, and `agentRunId`; and `cooldowns` maps an issue number to its ISO 8601 retry
|
|
1468
|
+
time. A failed agent finishes its run as `failed` with its first stderr line, leaves its issue
|
|
1469
|
+
open and unhandled, and writes only its own cooldown. A successful agent finishes its run as
|
|
1470
|
+
`succeeded`, records only its issue as handled, and does not touch another issue. The five-minute
|
|
1471
|
+
cooldown remains: an issue in cooldown is retried when it expires, and other issues keep running
|
|
1472
|
+
and start normally.
|
|
1473
|
+
|
|
1474
|
+
`Check now` clears every cooldown and polls immediately; while paused it writes a notice and polls
|
|
1475
|
+
nothing. Lifecycle is `running` while any peer runs, `pausing` while paused with any peer,
|
|
1476
|
+
`paused` when paused with none, `failed` when enabled and unpaused with no peer and an unhandled
|
|
1477
|
+
issue still cooling down, `idle` when enabled and unpaused with no peer and no unhandled issue,
|
|
1478
|
+
and `stopped` after Stop. Pause, Resume, Check now, and Stop apply to the whole fleet. Pause starts
|
|
1479
|
+
no peer and no poll and lets every peer finish. Stop closes every peer, records every active run as
|
|
1480
|
+
`stopped`, leaves every captured issue unhandled, writes no cooldown, releases the lock, and stops
|
|
1481
|
+
the ticker; no peer survives Stop. A fresh worker start terminates every ACP child the previous
|
|
1482
|
+
state recorded when `/proc` proves the process is that child, closes every interrupted run as
|
|
1483
|
+
`stopped`, clears `agents`, and starts no peer from stale state.
|
|
1484
|
+
|
|
1485
|
+
State, ledger, runs, and activity live under `<STATE_DIR>/issues/<journalKey(REPO_DIR)>/`:
|
|
1486
|
+
`state.json` is atomic, `ledger.json` records handled numbers with the run id,
|
|
1487
|
+
`runs/<runId>/run.json` and `runs/<runId>/activity.json` keep the newest 50 finished runs plus
|
|
1488
|
+
every currently running run, and `control/` carries dashboard commands as atomically renamed
|
|
1489
|
+
files. Each run's activity is persisted before the dashboard can read it, bounded at the first of
|
|
1490
|
+
1,000 entries or 1 MiB with the trimmed marker, and is never merged with another run's activity.
|
|
1491
|
+
On connect, the bridge replays `issuesState` and then one `issuesActivity` with `replay: true` for
|
|
1492
|
+
every running agent, so a dashboard connecting mid-flight sees every agent and all its output. The
|
|
1493
|
+
state document version is 2; the parser still accepts version 1, loading it with empty `agents`,
|
|
1494
|
+
`cooldowns` populated from its `goalCooldowns`, and its queue, labels, ledger, and runs intact. A
|
|
1495
|
+
damaged state file is renamed aside and never prevents the mode from opening.
|
|
1496
|
+
|
|
1497
|
+
The worker records the harness version it loaded, its pid, and its argv; a mismatched dashboard
|
|
1498
|
+
shows a notice and never kills the worker, and the state records each ACP child's pid, argv, and
|
|
1499
|
+
cwd so Stop or a fresh Start can terminate a proven orphan. The first worker to find no repo-keyed
|
|
1500
|
+
ledger imports handled numbers from `<STATE_DIR>/orchestrator/state.json` and `ledger.json` when
|
|
1501
|
+
the legacy state parses and its `root` resolves to this `REPO_DIR`. A missing, damaged, or
|
|
1502
|
+
mismatched legacy file is ignored. The legacy files are otherwise left untouched.
|
|
1503
|
+
|
|
1504
|
+
An already-installed legacy unit is removed by hand, once:
|
|
7
1505
|
|
|
8
1506
|
```bash
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1507
|
+
systemctl --user disable --now devboard-orchestrator.service
|
|
1508
|
+
rm -f ~/.config/systemd/user/devboard-orchestrator.service
|
|
1509
|
+
rm -f ~/.config/systemd/user/default.target.wants/devboard-orchestrator.service
|
|
1510
|
+
systemctl --user daemon-reload
|
|
12
1511
|
```
|
|
13
1512
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1513
|
+
### The Agents surface
|
|
1514
|
+
|
|
1515
|
+
The Issues sidebar has no `Agents` entry and the Overview has no Open Agents control; the command
|
|
1516
|
+
palette lists `open.issues.agents` in the `Workspace` group with the title `Open Agents` and opens
|
|
1517
|
+
the tab from any mode.
|
|
1518
|
+
|
|
1519
|
+
The tab is `{ id: "agents", kind: "agents", resourceId: "agents", title: "Agents" }` and is
|
|
1520
|
+
restored after a reload. The page renders `data-testid="issues-agents-page"` with a page header
|
|
1521
|
+
`data-testid="issues-agents-header"` that carries the repository, the running count
|
|
1522
|
+
`data-testid="issues-agents-count"`, and, when any agent is blocked, a warning badge
|
|
1523
|
+
`data-testid="issues-agents-asking"` naming how many need an answer. With no active agent it
|
|
1524
|
+
renders `data-testid="issues-agents-empty"` and an `Open Overview` control
|
|
1525
|
+
`data-testid="issues-agents-overview"` instead of the grid.
|
|
1526
|
+
|
|
1527
|
+
The grid `data-testid="issues-agents"` holds one card per running agent, sorted by issue number
|
|
1528
|
+
ascending. Each card has `data-testid="issues-agent"`, `data-issue`, `data-run`, `data-status`, and
|
|
1529
|
+
`data-asked` while its agent waits for an answer; its head shows the issue number linked to its
|
|
1530
|
+
tracker URL and the `Focus` control, and it does not repeat the fleet's status or waiting state as
|
|
1531
|
+
a badge. Below it the issue title reserves two lines, then one compact elapsed and started-time
|
|
1532
|
+
line whose tooltip carries the exact timestamp. Cards in one grid row share a height. The run id
|
|
1533
|
+
appears in the Focus tab, not on the card. Every card renders its agent's full streamed output in
|
|
1534
|
+
`data-testid="issues-agent-activity"` with `data-run`, under the `Latest activity` label; the log
|
|
1535
|
+
scrolls, follows its newest line while the operator is at the bottom, and stays put when scrolled
|
|
1536
|
+
back. It is 64px on a waiting card and fills the remaining card body otherwise. A pending question
|
|
1537
|
+
renders inside only that agent's card as `data-testid="issues-agent-question"` with `data-run`, its
|
|
1538
|
+
options with a recommended marker, and a free-text answer. The controls are
|
|
1539
|
+
`data-testid="issues-agent-choice"`,
|
|
1540
|
+
`data-testid="issues-agent-answer-input"`, and `data-testid="issues-agent-answer-submit"`, each
|
|
1541
|
+
carrying `data-run`. Answering sends `{ type: "answerIssues", runId, id, value }`; only the agent
|
|
1542
|
+
with that run id receives the value, only its question clears, and a missing or unknown run id is
|
|
1543
|
+
refused with an error that reaches no peer. Two agents asking at once keep two independent
|
|
1544
|
+
questions.
|
|
1545
|
+
|
|
1546
|
+
Activating a card (its `Focus` control) opens
|
|
1547
|
+
`{ id: "agent:<runId>", kind: "agent", resourceId: <runId>, title: "#<issue> <title>" }`, rendering
|
|
1548
|
+
`data-testid="issues-agent-focus"` with `data-testid="issues-agent-focus-status"`, the same full
|
|
1549
|
+
activity in `data-testid="issues-agent-focus-activity"`, its question in
|
|
1550
|
+
`data-testid="issues-agent-focus-question"`, and `data-testid="issues-agent-focus-close"`. The
|
|
1551
|
+
output panel header names its run once: a `Run` label, the identifier as a `.code-chip` in
|
|
1552
|
+
`data-testid="issues-agent-focus-run-id"` (capped at 220px; the full id is in its `title`), and
|
|
1553
|
+
`data-testid="issues-agent-focus-run-started"` reading `Started <relative time>` with the ISO
|
|
1554
|
+
start in its `title`. The start time comes from the live agent and falls back to run history; a
|
|
1555
|
+
missing timestamp renders no age. The header carries no bare `panel__subtitle` run id and no
|
|
1556
|
+
second status badge. The focus tab survives a reload while the run is active. After the run ends,
|
|
1557
|
+
a restored focus tab shows that run's final status and activity from run history, or a clear
|
|
1558
|
+
no-longer-available state linking to Runs when the run has been pruned.
|
|
1559
|
+
|
|
1560
|
+
The Overview keeps the Start, Pause, Resume, Stop, and Check now controls, the poll and worker
|
|
1561
|
+
facts, and `data-testid="issues-overview"`. It shows the running-agent count as
|
|
1562
|
+
`data-testid="issues-running-agents"`, the pending count, the number of issues in cooldown and the
|
|
1563
|
+
earliest cooldown time as `data-testid="issues-cooldowns"`. The left column holds the Automation
|
|
1564
|
+
panel and, directly after it, the running agents in
|
|
1565
|
+
`data-testid="issues-agent-summary"`: one row per agent carrying `data-run` and `data-issue`,
|
|
1566
|
+
sorted by issue number, with `aria-current="true"` on
|
|
1567
|
+
the selected agent, the issue title, a `needs answer` badge while it asks, and its elapsed time.
|
|
1568
|
+
The row's issue number is a tracker link (`data-testid="issues-agent-row-issue"`, opening the
|
|
1569
|
+
issue URL from the queue in a new tab) next to a control that selects the agent's output.
|
|
1570
|
+
The running-agent panel header carries an aggregate `data-testid="issues-overview-asking"` warning
|
|
1571
|
+
badge (`<n> needs answer` or `<n> need answers`) only while at least one agent waits; the fleet
|
|
1572
|
+
count is not repeated in that header because the Automation panel states it.
|
|
1573
|
+
Each row also carries `data-testid="issues-close-issue"` with `data-issue`; it closes that issue on
|
|
1574
|
+
the tracker as `not planned`, so an issue the operator does not want implemented leaves the next
|
|
1575
|
+
poll without stopping the agent already working it. A refused or failed closure leaves the issue
|
|
1576
|
+
open and reports the reason as an error notification; a successful one reports
|
|
1577
|
+
`Closed issue #<number>.` and disables the control until the queue drops the issue.
|
|
1578
|
+
The right column renders only the selected agent's output in
|
|
1579
|
+
`data-testid="issues-overview-output"` with `data-run`, its stream in
|
|
1580
|
+
`data-testid="issues-overview-activity"`, and its question in
|
|
1581
|
+
`data-testid="issues-overview-question"`. The first agent is selected by default, the row's select
|
|
1582
|
+
control switches the output, and the pane falls back to the first live agent when the selected run
|
|
1583
|
+
ends; the overview list selects instead of opening a tab. The queue keeps every open issue, its
|
|
1584
|
+
`goal` badge, and its handled state, and shows `data-run` and a running marker for an issue with an
|
|
1585
|
+
active agent. The Runs page lists every run with its status and issue,
|
|
1586
|
+
and a run's detail shows only that run's activity. The status bar's issues indicator reports the
|
|
1587
|
+
lifecycle and the running-agent count in its title and still opens Issues mode.
|
|
1588
|
+
|
|
1589
|
+
## Draft goals and proposed goals
|
|
1590
|
+
|
|
1591
|
+
The Issues sidebar carries a fourth entry after Runs: `Draft goals`, with `data-command="open.issues.drafts"`
|
|
1592
|
+
and `data-testid="open-issues-drafts"`. It trails the number of listed drafts, its accessible
|
|
1593
|
+
name names the Issues mode and the `Alt+2` shortcut, and the command palette lists it under the
|
|
1594
|
+
same command id. Opening it renders `data-testid="issues-drafts"`: every regular `.md` file
|
|
1595
|
+
directly under `<REPO_DIR>/.devboard/goal-proposals/`, sorted by filename, each with
|
|
1596
|
+
`data-testid="issues-draft"` and `data-path` holding the repository-relative path, its filename,
|
|
1597
|
+
and its first Markdown heading. An empty directory shows `No drafts exist.`.
|
|
1598
|
+
|
|
1599
|
+
Selecting one opens `data-testid="issues-draft-detail"`: the header carries the draft's file name,
|
|
1600
|
+
the state badge for this draft's run only (absent while the run is `idle`), `issues-draft-start`
|
|
1601
|
+
(`issues.draft.start`) and, while this draft runs, `issues-draft-stop` (`issues.draft.stop`). The
|
|
1602
|
+
body reads the proposal before the `## Captured UI context` marker, then the activity stream in
|
|
1603
|
+
`issues-draft-activity` once a run exists; the captured UI context itself is not shown. A pending
|
|
1604
|
+
elaboration question (`issues-draft-question`) renders above the proposal, with
|
|
1605
|
+
`issues-draft-choice`, `issues-draft-answer-input`, and `issues-draft-answer-submit`. The detail
|
|
1606
|
+
refreshes when the page opens, after the report panel writes a draft through
|
|
1607
|
+
`POST /api/goal-proposals`, after an elaboration finishes or stops, and when a listed draft
|
|
1608
|
+
disappears. The page polls the list every two seconds while it is mounted, so an external
|
|
1609
|
+
deletion is visible without a reload, and the report panel's write broadcast reaches an open page
|
|
1610
|
+
immediately. A browser reload restores the Draft goals page and the selected path; when the file
|
|
1611
|
+
no longer exists, the page opens the list.
|
|
1612
|
+
|
|
1613
|
+
The read path accepts only `.devboard/goal-proposals/<name>.md`: an absolute path, `..`, a nested
|
|
1614
|
+
path, a directory, a missing file, or a symlink whose real path leaves the drafts directory is
|
|
1615
|
+
refused with a visible error, and nothing outside the directory is read. The list never follows
|
|
1616
|
+
symlinks.
|
|
1617
|
+
|
|
1618
|
+
Only one elaboration runs at a time. Starting one spawns exactly one ACP peer with `AGENT_COMMAND`
|
|
1619
|
+
in `REPO_DIR`, opens one session, and sends exactly `/goal-draft <repository-relative path>`. Its
|
|
1620
|
+
activity streams into `issues-draft-activity`, its `ask` question renders as options plus a
|
|
1621
|
+
free-text answer, and an answer goes only to that peer. While it runs, every other draft's start
|
|
1622
|
+
control is disabled with the stated reason; Stop closes the peer, records `stopped`, keeps the
|
|
1623
|
+
draft, and re-enables the controls; a successful or failed run closes its peer before recording
|
|
1624
|
+
`succeeded` or `failed`, so no elaboration leaves an agent process behind; a failed peer keeps
|
|
1625
|
+
the draft, shows the error, and accepts a retry as a new peer. The running peer's pid, argv, and
|
|
1626
|
+
`cwd` are recorded in the server state, so a replacement server ends one its predecessor left
|
|
1627
|
+
running.
|
|
1628
|
+
|
|
1629
|
+
`.devboard/prompts/goal-draft.md` is the elaboration contract and
|
|
1630
|
+
`.devboard/omp/commands/goal-draft.md` its command wrapper, which reads the prompt and passes
|
|
1631
|
+
`$ARGUMENTS` as the draft path; `install.sh` installs the wrapper into the host's `.omp/commands/`.
|
|
1632
|
+
The prompt requires repository research before questions, then an `ask` interview with exactly one
|
|
1633
|
+
question per call, three mutually exclusive options with concrete descriptions and exactly one
|
|
1634
|
+
recommended, and forbids questions about effort, cost, time, token budget, or attempt caps. The
|
|
1635
|
+
finalized spec contains exactly `## Objective`, `## Success criteria`, `## Verification`,
|
|
1636
|
+
`## Boundaries`, and `## Stop conditions`, in that order, with binary success criteria and exact
|
|
1637
|
+
verification commands or actions.
|
|
1638
|
+
|
|
1639
|
+
The spec is written to `<REPO_DIR>/.devboard/goals/<slug>.md`: lowercase kebab-case, at most six
|
|
1640
|
+
words, and an existing filename is never overwritten (`-2`, `-3`, …). The elaboration agent
|
|
1641
|
+
creates no other file except that spec and the draft removal, starts no implementation, and does
|
|
1642
|
+
not enter goal mode. It files the spec's bytes verbatim, as a body file, as an open issue in the
|
|
1643
|
+
checkout's `origin` (never the harness repository unless that is the origin), with a one-line title
|
|
1644
|
+
bounded to 200 characters and the exact `goal` label, creating the label first when it is missing.
|
|
1645
|
+
Every tracker command goes through `.devboard/scripts/forge-auth.ts exec --required write`. After
|
|
1646
|
+
filing it reads the issue back with `gh issue view <number> --json number,url,labels,body`; only
|
|
1647
|
+
when the `goal` label is present and the body equals the spec file may it delete the draft. A
|
|
1648
|
+
failed label creation, issue creation, or verification leaves the draft in place and reports the
|
|
1649
|
+
failure and any created issue URL. A successful completion removes the draft from the list, keeps
|
|
1650
|
+
the spec under `.devboard/goals/`, and the next Issues poll lists the new open issue with its
|
|
1651
|
+
`goal` badge.
|
|
1652
|
+
|
|
1653
|
+
The Open issues list shows every open issue with its `goal` badge and its handled state. Every
|
|
1654
|
+
open, unhandled issue gets its own concurrent peer: non-goal issues are prompted with exactly
|
|
1655
|
+
`/implement-issue <number>`, goal issues with exactly `/implement-goal-issue <number>`. Each has its
|
|
1656
|
+
own run record, activity, and question slot. The snapshot publishes
|
|
1657
|
+
`agents: [{ runId, issueNumber, startedAt, question, acp }]`, so the Agents page, the Overview, and
|
|
1658
|
+
the Runs page address each agent by issue number and run id while several are active, and an
|
|
1659
|
+
answer carries that `runId` so it routes only to its peer.
|
|
1660
|
+
|
|
1661
|
+
`.devboard/extensions/implement-goal-issue.ts` implements the command and
|
|
1662
|
+
`.devboard/omp/extensions/implement-goal-issue.ts` is its host-side re-export copied by
|
|
1663
|
+
`install.sh`. It accepts one issue number and refuses a missing number, a missing issue, an issue
|
|
1664
|
+
without the exact `goal` label, or an existing goal in the session. It creates one worktree and
|
|
1665
|
+
branch under `.worktree/`, activates the `goal` tool, and calls the `goal` create operation with
|
|
1666
|
+
the fetched issue body verbatim as the objective: no distillation, no section filtering, and no
|
|
1667
|
+
appended requirements. It then implements the body, verifies every criterion through the product
|
|
1668
|
+
surface, merges into `main` with conflict integration and retry, pushes `main` to origin, removes
|
|
1669
|
+
the worktree and branch, and closes the issue only after the push, through the same forge-auth
|
|
1670
|
+
wrapper.
|
|
1671
|
+
|
|
1672
|
+
Pause lets every active peer finish, then reports `paused` and starts no new peer or poll.
|
|
1673
|
+
Resume polls immediately. Stop closes every peer, records every active run as `stopped`, leaves
|
|
1674
|
+
every issue unhandled, releases the lock, and writes no cooldown. A successful run records only
|
|
1675
|
+
its issue as handled in the ledger; a failed one records `failed` with its first stderr line and
|
|
1676
|
+
gates only that issue for five minutes, leaving it open and unhandled. An issue closed by its
|
|
1677
|
+
agent leaves the queue on the next poll.
|
|
1678
|
+
|
|
1679
|
+
## Auto Scout
|
|
1680
|
+
|
|
1681
|
+
The Issues sidebar lists `Overview`, `Auto Scout`, `Issues`, `Runs`, and then `Draft goals`
|
|
1682
|
+
in that order. Auto Scout (`data-command="open.issues.scout"`, `data-testid="open-issues-scout"`,
|
|
1683
|
+
palette `Open Auto Scout`) renders `data-testid="issues-scout"`. Opening the page, selecting its
|
|
1684
|
+
tab, or reloading the dashboard never starts a worker and never files an issue: only
|
|
1685
|
+
`data-testid="scout-start"` starts the worker, and that control becomes
|
|
1686
|
+
`data-testid="scout-stop"` while a worker is attached. The page header
|
|
1687
|
+
(`data-testid="issues-scout-header"`) carries the repository origin,
|
|
1688
|
+
`data-testid="scout-lifecycle"` (`stopped`, `idle`, `scanning`, or `failed`), the last-read
|
|
1689
|
+
time, and the single Start/Stop action. A 360px side rail holds the worker
|
|
1690
|
+
facts and the filing limits; the results column holds the filed-issues table and
|
|
1691
|
+
`data-testid="scout-activity"` for the current or newest scan. The page also shows
|
|
1692
|
+
`data-testid="scout-error"` for the last error.
|
|
1693
|
+
|
|
1694
|
+
There is no review path. While a worker runs, a scan whose items score at least 4 files each
|
|
1695
|
+
accepted item directly as a GitHub issue in the host `origin`, in the same scan cycle, with no
|
|
1696
|
+
dashboard command, click, approval, or draft. The page exposes no staged recommendation, no
|
|
1697
|
+
`scout-widget`, `scout-approve`, `scout-draft-goal`, `scout-reject`, or `scout-shortfall` element,
|
|
1698
|
+
and `approveScout`, `draftScoutGoal`, and `rejectScout` are not accepted commands. Stop keeps the
|
|
1699
|
+
filed rows and the limits; the next Start reads GitHub and scans again.
|
|
1700
|
+
|
|
1701
|
+
### Limits
|
|
1702
|
+
|
|
1703
|
+
`data-testid="scout-max-per-hour"` (default 10) and `data-testid="scout-max-pending"` (default 5)
|
|
1704
|
+
accept integers from 1 through 100. `data-testid="scout-save-limits"` persists both; an invalid
|
|
1705
|
+
value renders `data-testid="scout-limits-error"` and sends nothing. A valid save is written
|
|
1706
|
+
atomically to `<STATE_DIR>/scout/<journalKey(REPO_DIR)>/settings.json`, so it survives a page
|
|
1707
|
+
reload and a worker restart, and a running worker receives it as a limits command without
|
|
1708
|
+
restarting. A worker start reads that file first, then the last state's limits, then the defaults.
|
|
1709
|
+
|
|
1710
|
+
`maxPerHour` counts every Auto Scout issue whose creation time is inside a rolling 60-minute
|
|
1711
|
+
window, in any state, including issues adopted from GitHub and issues filed before a restart.
|
|
1712
|
+
`maxPending` counts every open `scout` issue. One scan requests and files at most
|
|
1713
|
+
`min(maxPerHour - windowCount, maxPending - openCount)` items. A zero batch starts no scan; the
|
|
1714
|
+
next attempt is the earlier of the next 60-second read (a close can free a pending slot) and the
|
|
1715
|
+
moment the oldest hourly entry expires, so the worker never retries in a tight loop. When an issue
|
|
1716
|
+
closes, the 60-second read frees the slot and the next scan starts without any manual action. A
|
|
1717
|
+
scan that files fewer items than requested is a normal shortfall, recorded in scan activity, never
|
|
1718
|
+
a failure. Immediately before filing, the worker reads every `scout` issue again; a failed read
|
|
1719
|
+
files nothing, keeps the last known rows, and blocks filing until a read succeeds.
|
|
1720
|
+
|
|
1721
|
+
### Significance
|
|
1722
|
+
|
|
1723
|
+
Every recommendation carries an integer `significance` from 1 to 5 and a non-empty
|
|
1724
|
+
`significanceReason`. The scan prompt defines the rubric: 4 or 5 requires a named, observable
|
|
1725
|
+
impact — a reproducible defect, a real failure mode, or a measured structural cost; polishing
|
|
1726
|
+
code that is already correct is at most 2. Items scoring below 4 are dropped before filing and
|
|
1727
|
+
recorded in scan activity. A missing or out-of-range score, or a missing reason, fails the whole
|
|
1728
|
+
scan and files nothing unless the correction turn fixes it. Each filed issue body carries
|
|
1729
|
+
`## Significance` with `<n>/5` and the reason.
|
|
1730
|
+
|
|
1731
|
+
A filed issue carries the exact `scout` label (created first when absent), the recommendation's
|
|
1732
|
+
title, and a body with category, summary, impact, significance, evidence, proposed change, files,
|
|
1733
|
+
and acceptance criteria; the worker resolves forge write access through `lib/forge-auth.ts`. The
|
|
1734
|
+
Issues worker picks it up on its next poll exactly like any other open issue, and its pickup and
|
|
1735
|
+
implementation behavior is unchanged.
|
|
1736
|
+
|
|
1737
|
+
A scan never files a title whose normalized form (lowercased, whitespace-collapsed, trimmed)
|
|
1738
|
+
matches any existing Auto Scout issue in any state, any open issue title, or any draft goal
|
|
1739
|
+
heading. A duplicate is dropped, recorded in activity, and consumes no cap slot. The scan request
|
|
1740
|
+
names the normalized `knownTitles` and the draft headings, so the agent avoids them too.
|
|
1741
|
+
|
|
1742
|
+
### Issue table
|
|
1743
|
+
|
|
1744
|
+
`data-testid="scout-issues"` renders one `data-testid="scout-issue"` row per Auto Scout issue,
|
|
1745
|
+
newest `createdAt` first, carrying `data-number`, `data-state`, and `data-version`. Each row
|
|
1746
|
+
renders `data-testid="scout-issue-title"` as an anchor to the issue URL,
|
|
1747
|
+
`data-testid="scout-issue-created"` with a local creation time (the raw ISO value stays in the
|
|
1748
|
+
cell's `title`), `data-testid="scout-issue-type"`,
|
|
1749
|
+
`data-testid="scout-issue-state"`, and `data-testid="scout-issue-version"`. The type is the
|
|
1750
|
+
recommendation's category, or the issue body's `## Category` value for an adopted issue, or `—`.
|
|
1751
|
+
With no rows, `data-testid="scout-issues-empty"` renders a centered stack: a decorative inbox
|
|
1752
|
+
mark, `No issues filed yet`, and one line stating that Auto Scout files significant findings after
|
|
1753
|
+
each scan. The empty state adds no control of its own; Start/Stop stays in the page header and the
|
|
1754
|
+
row count stays in the panel header. The sidebar `data-testid="scout-count"` shows the number of
|
|
1755
|
+
rows whose state is `pending`.
|
|
1756
|
+
|
|
1757
|
+
An open issue is `pending`. A closed issue is `implemented` regardless of `stateReason`; a
|
|
1758
|
+
reopened issue returns to `pending` on the next read. For an implemented issue the worker resolves
|
|
1759
|
+
the commit that closed it — the timeline's closing commit when present, otherwise the last 7–40
|
|
1760
|
+
character hex SHA in the newest comment that contains one — to one unambiguous full commit through
|
|
1761
|
+
the checkout or GitHub. It then selects the earliest published GitHub Release tag whose history
|
|
1762
|
+
contains that commit, ordered by `publishedAt` and breaking ties by tag name. The row shows `—`
|
|
1763
|
+
when no commit resolves or no published release contains it, and a release published later fills a
|
|
1764
|
+
blank version on the next read.
|
|
1765
|
+
|
|
1766
|
+
### Periodic check
|
|
1767
|
+
|
|
1768
|
+
While started, the worker reads every `scout` issue and every published release every 60 seconds,
|
|
1769
|
+
and immediately on Start; the table, states, and versions reflect a change within one interval. One
|
|
1770
|
+
periodic read, including every `gh` child it starts, is bounded to 30 seconds; a read that does not
|
|
1771
|
+
finish records a sync error and releases the worker, so the next interval reads again. A failed
|
|
1772
|
+
read keeps the last known rows, shows the reason in `data-testid="scout-sync-error"`, and keeps the
|
|
1773
|
+
last success time in `data-testid="scout-sync-at"`; the reason clears on the next successful read.
|
|
1774
|
+
Filing stays blocked until a read succeeds. An injected GitHub client owns its own command bounds;
|
|
1775
|
+
the worker still races the whole read against the deadline, but it cannot cancel work that client
|
|
1776
|
+
never settles.
|
|
1777
|
+
|
|
1778
|
+
### Worker lifecycle
|
|
1779
|
+
|
|
1780
|
+
Start proves one set of facts before spawning anything: a canonical checkout root that is not a
|
|
1781
|
+
linked worktree, a GitHub `origin`, `STATE_DIR` inside `REPO_DIR`, `omp` and `gh` on `PATH` with
|
|
1782
|
+
push permission, and the `/scout-recommendations` command installed at
|
|
1783
|
+
`<REPO_DIR>/.omp/commands/`, `~/.omp/commands/`, or `~/.omp/agent/commands/`. A refusal names the
|
|
1784
|
+
missing fact and writes no scan. The worker is spawned detached from the loaded harness package
|
|
1785
|
+
with `REPO_DIR` as its working directory and holds `<REPO_DIR>/.devboard/.tmp/scout/lock`; a live
|
|
1786
|
+
owner is never replaced, a different `STATE_DIR` is refused, a dead owner is reclaimed, and a
|
|
1787
|
+
second dashboard with the same repository and state directory attaches. Stop ends the peer,
|
|
1788
|
+
records `enabled: false` and `lifecycle: "stopped"`, keeps every row and the limits, releases the
|
|
1789
|
+
lock, and exits; a worker that outlives the command is signalled only after the process table
|
|
1790
|
+
proves it. A worker on another harness version is left running with a notice, and Stop then Start
|
|
1791
|
+
is the only upgrade path.
|
|
1792
|
+
|
|
1793
|
+
State lives under `<STATE_DIR>/scout/<journalKey(REPO_DIR)>/state.json`, written atomically and
|
|
1794
|
+
carrying the worker record, lifecycle, limits, every Auto Scout issue row, the last read, the
|
|
1795
|
+
current scan, next attempt, last error, and notice; a damaged state file is renamed aside. A
|
|
1796
|
+
state produced by the previous Auto Scout version loads with its worker and scan facts intact,
|
|
1797
|
+
discards every unapproved staged recommendation without filing anything, applies the default
|
|
1798
|
+
limits, and publishes a notice naming how many recommendations were discarded; no pre-upgrade
|
|
1799
|
+
recommendation is ever filed. The dashboard migrates that file in memory too, so an upgraded
|
|
1800
|
+
dashboard reads the lifecycle of a worker it did not start, and Stop acts on the live lock owner
|
|
1801
|
+
even when no state file is readable.
|
|
1802
|
+
|
|
1803
|
+
Scan directories live under `<REPO_DIR>/.devboard/.tmp/scout/scans/<scanId>/` and hold
|
|
1804
|
+
`request.json`, `recommendations.json`, `scan.json`, and `activity.json`, which is bounded at the
|
|
1805
|
+
first of 1,000 entries or 1 MiB with the trimmed marker. One scan runs at a time and sends exactly
|
|
1806
|
+
one ACP prompt from `AGENT_COMMAND` (default `omp acp`) with `cwd = REPO_DIR`:
|
|
1807
|
+
`/scout-recommendations <absolute scan directory>`. The prompt is
|
|
1808
|
+
`prompts/scout-recommendations.md`, wrapped by `omp/commands/scout-recommendations.md`; the
|
|
1809
|
+
version-2 request carries the repository path, the requested batch, categories, excluded roots,
|
|
1810
|
+
normalized known titles, draft-goal headings, and the significance threshold, and the agent writes
|
|
1811
|
+
only `recommendations.json`. A permission request is auto-approved, a question is answered with
|
|
1812
|
+
its recommended option (or its first), and both sides reach the activity. A scan that exceeds 15
|
|
1813
|
+
minutes is closed as failed.
|
|
1814
|
+
|
|
1815
|
+
Every result is validated before filing: at most the requested count; a known category; a one-line
|
|
1816
|
+
title of at most 200 characters; a summary, proposed change, significance from 1 through 5,
|
|
1817
|
+
non-empty significance reason, and at least one acceptance criterion; at least one evidence entry
|
|
1818
|
+
whose repository-relative path resolves to an existing regular file inside `REPO_DIR` and outside
|
|
1819
|
+
`.git/`, `node_modules/`, `dist/`, `.worktree/`, and `.tmp/`, with an in-range `N` or `N-M` span;
|
|
1820
|
+
and a non-empty file list whose entries all appear in the evidence. A rejected answer is sent back
|
|
1821
|
+
to the same agent session once, with the rejection text and the root-relative path rule, while the
|
|
1822
|
+
scan's own deadline still bounds the turn; a repaired file is validated again and filed normally.
|
|
1823
|
+
A malformed item, duplicate id, path escape, over-count, missing score, or out-of-range score that
|
|
1824
|
+
survives the correction turn fails the whole answer atomically, naming the offending item, so
|
|
1825
|
+
nothing is filed. An item's id is the first 16 hex characters of SHA-256 over its category, sorted
|
|
1826
|
+
evidence paths, and normalized title, so the same finding keeps its id. Items below significance
|
|
1827
|
+
4 and duplicate titles are dropped before filing and recorded in the activity. The worker never
|
|
1828
|
+
edits source, tests, configuration, a goal, a draft, a branch, or a pull request.
|
|
1829
|
+
|
|
1830
|
+
## Report, remove, or redesign a UI element
|
|
1831
|
+
|
|
1832
|
+
When the server has a Gitea address for harness-owned reporting, every page carries three UI issue
|
|
1833
|
+
controls in the activity bar's application actions, beside commands, notifications, and theme:
|
|
1834
|
+
`Report UI issue`, `Remove element`, and `Redesign element`. Without `DEVBOARD_GITEA_URL` they are
|
|
1835
|
+
not rendered and their palette commands are disabled, so no page offers a filing control that
|
|
1836
|
+
cannot work.
|
|
1837
|
+
Each is a 28px icon control with an accessible name and `aria-pressed="true"` while its action is
|
|
1838
|
+
armed; the outcome toast and its error stay out of element picking. `Esc` cancels a mode that is
|
|
1839
|
+
not filing, and `app.report.cancel` cancels any active mode from the palette. While one request is
|
|
1840
|
+
in flight every control is disabled and the active one is labelled `Filing the UI issue`, so one
|
|
1841
|
+
click or one double-click files at most one issue.
|
|
1842
|
+
|
|
1843
|
+
`Report UI issue` enters element-selection mode: the element under the pointer is outlined, and a
|
|
1844
|
+
click opens a report panel anchored beside it, without filing anything yet. The panel shows the
|
|
1845
|
+
selected element's selector path; the report text is free-form. `File issue` posts the report and
|
|
1846
|
+
the captured context to `POST /api/feedback`, the server files the issue through the configured
|
|
1847
|
+
Gitea API, and the panel closes back into report selection mode so another
|
|
1848
|
+
element can be picked without leaving and re-entering the mode. A successful filing shows a
|
|
1849
|
+
`Report filed.` toast with an `Open issue` link to the created issue. `Esc`, the control again, or
|
|
1850
|
+
`Cancel` leaves the mode, a failed report stays in the panel with its error, and a click in
|
|
1851
|
+
selection mode never activates the element. The toast survives leaving the mode: a success
|
|
1852
|
+
dismisses itself after eight seconds, a failure stays until dismissed or replaced.
|
|
1853
|
+
|
|
1854
|
+
The panel also answers questions. `Ask`, or `Enter` in the composer (`Shift+Enter` inserts a
|
|
1855
|
+
newline), sends the typed question and the captured element context — route, URL, selector,
|
|
1856
|
+
viewport, visible text, DOM excerpt, and capture time — over the existing WebSocket to a
|
|
1857
|
+
background ACP agent started from `AGENT_COMMAND`. The agent runs in the host repository root,
|
|
1858
|
+
and its answer streams into a compact transcript in the same panel as plain text, with preserved
|
|
1859
|
+
line breaks and no Markdown interpretation. Every prompt tells it the conversation is `read-only`:
|
|
1860
|
+
never edit files, run mutating commands, file issues, commit, or push. It also tells it to answer
|
|
1861
|
+
in at most `three short sentences` and about 400 characters in plain text. That is a prompt-level
|
|
1862
|
+
rule, not a sandbox.
|
|
1863
|
+
|
|
1864
|
+
One conversation exists `per selected element`, and only while its panel is open. Escape, `×`,
|
|
1865
|
+
`Cancel`, a new pick, a successful filing, or a lost connection closes it: the server kills the
|
|
1866
|
+
agent and the transcript is discarded. Reopening a panel starts a new conversation, selecting a
|
|
1867
|
+
different element never resumes the previous one, nothing is written to `localStorage` or the
|
|
1868
|
+
state directory, and a page reload loses it. While a turn is pending the composer and both action
|
|
1869
|
+
buttons are disabled, `Ask` reads `Asking…`, and `Cancel` stays enabled. A failed start, an exited
|
|
1870
|
+
agent, or a rejected prompt shows the error in the panel, keeps the question and earlier answers,
|
|
1871
|
+
and accepts the next question. `Ask` never calls `POST /api/feedback` and creates no issue.
|
|
1872
|
+
|
|
1873
|
+
Filing or proposing from a panel that has answered questions carries the visible Ask conversation
|
|
1874
|
+
into the issue or draft as a `## Ask conversation` block between the request and
|
|
1875
|
+
`## Captured UI context`, labelled `You:` and `Agent:` per turn. The conversation is bounded to 40
|
|
1876
|
+
turns, 4000 characters per turn, and 12000 characters in total; when it exceeds that, the newest
|
|
1877
|
+
whole turns are kept and the oldest dropped. A panel without a conversation produces no block, and
|
|
1878
|
+
a conversation is never resumed from storage.
|
|
1879
|
+
|
|
1880
|
+
`Goal` turns the composer text into a raw proposed goal instead of an issue. With non-whitespace
|
|
1881
|
+
text it posts `report`, the Ask conversation when the panel has one, and the same captured element
|
|
1882
|
+
context to `POST /api/goal-proposals`; the server writes one Markdown draft under
|
|
1883
|
+
`<REPO_DIR>/.devboard/goal-proposals/` and answers
|
|
1884
|
+
`201 { "path": ".devboard/goal-proposals/<file>.md" }`. The directory is deliberately separate
|
|
1885
|
+
from the final goal specs in `.devboard/goals/`: a draft is unprocessed input, and this action
|
|
1886
|
+
never writes a goal document, `GOAL.md`, or an ADR, never files an issue, and never runs `gh`.
|
|
1887
|
+
The file name is chosen by one bounded naming turn: an ACP peer starts with `AGENT_COMMAND` in
|
|
1888
|
+
`REPO_DIR`, receives the operator report marked as data with a read-only instruction, and must
|
|
1889
|
+
answer with only a lowercase kebab-case slug, at most six hyphen-separated words, `[a-z0-9-]`, and
|
|
1890
|
+
at most 64 characters. A start failure, an exited peer, a question, a 15-second timeout, or an
|
|
1891
|
+
unusable answer falls back to the report's first non-empty line, lowercased, with every run of
|
|
1892
|
+
non-alphanumeric characters replaced by `-`, leading and trailing `-` removed, and at most six
|
|
1893
|
+
hyphen-separated words, falling back to `ui-goal`; an existing name earns `-2`, `-3`, … through an
|
|
1894
|
+
exclusive create, so the first draft is never overwritten. The body opens with
|
|
1895
|
+
`# Proposed goal: <first line, whitespace collapsed, at most 200 characters>`, then the operator's
|
|
1896
|
+
text verbatim, the `## Ask conversation` block when the panel has one, and the
|
|
1897
|
+
`## Captured UI context` block an issue report carries. It carries no notice line or other prose
|
|
1898
|
+
the operator did not write. A successful click closes the panel,
|
|
1899
|
+
stays in report selection mode with an empty composer, and shows the
|
|
1900
|
+
`Goal proposed: <path>` toast as plain text with no link; a failed write keeps the panel open
|
|
1901
|
+
with the server's error and leaves no draft. The drafts are ignored by git.
|
|
1902
|
+
|
|
1903
|
+
`Remove element` and `Redesign element` are one-click shortcuts. Selecting one arms it and shows a
|
|
1904
|
+
shortcut-specific hint; the next click on any element files the issue immediately, with no panel
|
|
1905
|
+
and no text field. Filing does not use client-supplied report text. A successful filing shows a
|
|
1906
|
+
`Removal issue filed.` or `Redesign issue filed.` toast with an `Open issue` link to the created
|
|
1907
|
+
issue and keeps the mode armed, so the next element is one click away; a failure shows a
|
|
1908
|
+
`Removal issue failed: <message>` or `Redesign issue failed: <message>` alert toast, creates no
|
|
1909
|
+
issue, and keeps the mode armed with the controls re-enabled. A success toast dismisses itself
|
|
1910
|
+
after eight seconds; a failure stays until dismissed or replaced. Either survives `Esc`, the
|
|
1911
|
+
active control, or `app.report.cancel`, because leaving the mode is not a reason to hide whether
|
|
1912
|
+
the issue was created. The active control, `Esc`, or `app.report.cancel` returns to idle.
|
|
1913
|
+
|
|
1914
|
+
Both shortcuts use a fixed title and body, so the implementation agent starts from the captured
|
|
1915
|
+
context rather than operator prose. The title is `[ui] Remove element: <selector>` or
|
|
1916
|
+
`[ui] Redesign element: <selector>`, with the selector's whitespace collapsed and the whole title
|
|
1917
|
+
clamped to 200 characters; an empty selector leaves `[ui] Remove element`. A removal body starts
|
|
1918
|
+
with `## Removal request` and the instruction `Remove the selected UI element from the
|
|
1919
|
+
interface.`. A redesign body starts with `## Redesign request` and the instruction `Check the
|
|
1920
|
+
selected UI element and redesign it because it does not look good. Use the global OMP UI/UX
|
|
1921
|
+
standard (the ui-ux skill) and this repository's DESIGN.md and design/tokens.json as the normative
|
|
1922
|
+
design system.` Both bodies end with the same `## Captured UI context` block: route, URL, selected
|
|
1923
|
+
element, viewport, capture time, visible text, and DOM excerpt.
|
|
1924
|
+
|
|
1925
|
+
The report title is `[ui] ` plus the first line of the report, at most 200 characters; its body is
|
|
1926
|
+
the verbatim report followed by the same captured context, so the ticket stays authoritative
|
|
1927
|
+
without a model in the loop.
|
|
1928
|
+
|
|
1929
|
+
Every request carries an `action` of `report`, `remove`, or `redesign`; an absent `action` means
|
|
1930
|
+
`report`, the only action that requires report text, and any other value is refused with HTTP 400
|
|
1931
|
+
before the Gitea request. Harness-owned reporting uses only the Gitea API: `DEVBOARD_GITEA_URL`
|
|
1932
|
+
names the instance, `GITEA_TOKEN` (or `DEVBOARD_GITEA_TOKEN`) authenticates it, and
|
|
1933
|
+
`DEVBOARD_RELEASE_REPO` selects the repository (default `dsdenes/devboard`), whatever host project
|
|
1934
|
+
the interface is serving. Nothing is created in the host's own tracker, and no `github.com`
|
|
1935
|
+
repository is used for filing. Without `DEVBOARD_GITEA_URL` the endpoint answers HTTP 503 naming
|
|
1936
|
+
that variable and performs no request. A refused or failing request is reported in the filing
|
|
1937
|
+
toast and creates nothing. The endpoint accepts only same-origin `POST` requests, bounds the body
|
|
1938
|
+
to 64 KB, and gives the request 60 seconds.
|
|
1939
|
+
|
|
1940
|
+
## Container
|
|
1941
|
+
|
|
1942
|
+
Build the container from the `.devboard` context at the repository root:
|
|
1943
|
+
|
|
1944
|
+
```bash
|
|
1945
|
+
docker build -t devboard-adr-ui .devboard
|
|
1946
|
+
docker run --rm -d --name devboard-adr-ui -p 127.0.0.1:8080:8080 \
|
|
1947
|
+
-v "$PWD:/repo" devboard-adr-ui
|
|
1948
|
+
```
|
|
1949
|
+
|
|
1950
|
+
The container reads `/repo/adrs` and listens on port `8080`. The repository mount is writable because `Save goal` replaces `/repo/GOAL.md`; mount it read-only only if goal saving is not needed. Docker publishes the port on the host's loopback interface only. The container process binds `0.0.0.0` because Docker forwards published traffic to the container interface; the local-only access boundary is enforced by the `127.0.0.1` host publish.
|
|
1951
|
+
|
|
1952
|
+
## WebSocket contract
|
|
1953
|
+
|
|
1954
|
+
The client sends:
|
|
1955
|
+
|
|
1956
|
+
```json
|
|
1957
|
+
{ "type": "listRecords" }
|
|
1958
|
+
{ "type": "getGoal" }
|
|
1959
|
+
{ "type": "getProject" }
|
|
1960
|
+
{ "type": "getVersion" }
|
|
1961
|
+
{ "type": "getIssues" }
|
|
1962
|
+
{ "type": "getIssuesRun", "runId": "2026-10-27T10-00-00-000Z" }
|
|
1963
|
+
{ "type": "startIssues" }
|
|
1964
|
+
{ "type": "pauseIssues" }
|
|
1965
|
+
{ "type": "resumeIssues" }
|
|
1966
|
+
{ "type": "stopIssues" }
|
|
1967
|
+
{ "type": "checkIssues" }
|
|
1968
|
+
{ "type": "answerIssues", "id": 7, "value": "all", "runId": "2026-10-27T10-00-02-000Z" }
|
|
1969
|
+
{ "type": "closeIssue", "number": 240 }
|
|
1970
|
+
{ "type": "getGoalDrafts" }
|
|
1971
|
+
{ "type": "getGoalDraft", "path": ".devboard/goal-proposals/example.md" }
|
|
1972
|
+
{ "type": "startGoalDraft", "path": ".devboard/goal-proposals/example.md" }
|
|
1973
|
+
{ "type": "stopGoalDraft" }
|
|
1974
|
+
{ "type": "answerGoalDraft", "id": 4, "value": "The whole goal" }
|
|
1975
|
+
|
|
1976
|
+
{ "type": "getScout" }
|
|
1977
|
+
{ "type": "startScout" }
|
|
1978
|
+
{ "type": "stopScout" }
|
|
1979
|
+
{ "type": "setScoutLimits", "maxPerHour": 10, "maxPending": 5 }
|
|
1980
|
+
{ "type": "restartServer" }
|
|
1981
|
+
{ "type": "restartServer", "whenIdle": true }
|
|
1982
|
+
{ "type": "saveGoal", "text": "# Goal\n\nThe complete goal text." }
|
|
1983
|
+
{ "type": "startRun" }
|
|
1984
|
+
{ "type": "startRun", "file": "0002-example.md" }
|
|
1985
|
+
{ "type": "startLifecycle" }
|
|
1986
|
+
{ "type": "pauseLifecycle" }
|
|
1987
|
+
{ "type": "resumeLifecycle" }
|
|
1988
|
+
{ "type": "reviseGoal", "request": "Tighten the goal" }
|
|
1989
|
+
{ "type": "reviseGoal" }
|
|
1990
|
+
{ "type": "createGoal" }
|
|
1991
|
+
{ "type": "prepareSkills" }
|
|
1992
|
+
{ "type": "getSkills" }
|
|
1993
|
+
{ "type": "deleteSkill", "name": "test-strategy" }
|
|
1994
|
+
{ "type": "resetImplementation" }
|
|
1995
|
+
{ "type": "answer", "id": 1, "value": "Gamma" }
|
|
1996
|
+
{ "type": "ask", "conversationId": "…", "question": "What does this label mean?", "context": { … } }
|
|
1997
|
+
{ "type": "closeAsk", "conversationId": "…" }
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
- `getGoal` reads the goal document from disk and answers with its complete contents; an
|
|
2001
|
+
absent document answers with empty text, which shows `Create goal` and keeps the lifecycle
|
|
2002
|
+
start disabled.
|
|
2003
|
+
- `getProject` answers with the page heading label. `path` is `REPO_DIR` relative to the user's
|
|
2004
|
+
home directory when the repository lies inside it, and the absolute path otherwise.
|
|
2005
|
+
- `getVersion` answers with `versionState`: `running` is the harness package version the
|
|
2006
|
+
backend loaded, `installed` is the version its prefix holds now, `latest` is the newest
|
|
2007
|
+
release an update check has seen, and `stale` is true only when `running` and `installed` are
|
|
2008
|
+
readable and differ. A dashboard that serves the canonical source checkout reads neither side
|
|
2009
|
+
from the checked-in manifest: it reports `0.1.<commit-count>` over the full history, the same
|
|
2010
|
+
version a release packs, so an unpacked checkout never claims `0.1.0`. A client that connects,
|
|
2011
|
+
and the page every few seconds, asks for it; a stale answer makes the reload control appear
|
|
2012
|
+
with both versions in its title.
|
|
2013
|
+
- `getIssues` answers with `issuesState`, the worker snapshot plus `present` and `attached`:
|
|
2014
|
+
lifecycle, enabled, paused, repository, poll times, queue entries with `labels`, `handled`,
|
|
2015
|
+
`runId`, and `agentRunId`, run summaries, the running `agents` (run id, issue number, startedAt,
|
|
2016
|
+
question, ACP child), the per-issue `cooldowns` map, errors, and the harness version the worker
|
|
2017
|
+
loaded. A missing state and no live lock answer `present: false`, `attached: false`, and
|
|
2018
|
+
`lifecycle: "stopped"`. A live worker from another harness version adds a non-blocking `notice`
|
|
2019
|
+
naming both versions. A connect also replays one `issuesActivity` with `replay: true` for every
|
|
2020
|
+
running agent; `getIssuesRun` answers `issuesRun` with one run's metadata and activity. The
|
|
2021
|
+
client never polls: state and activity arrive as events, so switching modes and reconnecting
|
|
2022
|
+
send no frame.
|
|
2023
|
+
- `answerIssues` carries `{ runId, id, value }`; the answer reaches only the agent with that run
|
|
2024
|
+
id, and a missing or unknown run id is refused with an `error` that reaches no peer.
|
|
2025
|
+
- `closeIssue` carries `{ number }` and closes that issue on the checkout's GitHub origin as
|
|
2026
|
+
`not planned` with a write-capable account. It answers `issuesClosed` with `{ number, ok }` and,
|
|
2027
|
+
on failure, the reason in `message`; a missing number answers `error`. A running agent is not
|
|
2028
|
+
interrupted, and the queue drops the issue on the worker's next poll.
|
|
2029
|
+
- `getGoalDrafts` answers `issuesDrafts` with the sorted list of `{ path, name, heading }` and
|
|
2030
|
+
`issuesDraftState` with the current elaboration. `getGoalDraft` answers `issuesDraftDetail` with
|
|
2031
|
+
the draft's exact bytes, or `issuesDraftError` when the path is refused or missing.
|
|
2032
|
+
`startGoalDraft` starts the one elaboration peer and broadcasts `issuesDraftState` and
|
|
2033
|
+
`issuesDrafts`; `stopGoalDraft` closes it, records `stopped`, and keeps the draft.
|
|
2034
|
+
`answerGoalDraft` routes one answer to the elaboration question. Activity is sent as
|
|
2035
|
+
`issuesDraftState` snapshots while the peer runs.
|
|
2036
|
+
|
|
2037
|
+
lifecycle, enabled, paused, repository, poll times, queue, run summaries, active run, question,
|
|
2038
|
+
next attempt, errors, and the harness version the worker loaded. A missing state and no live
|
|
2039
|
+
lock answer `present: false`, `attached: false`, and `lifecycle: "stopped"`. A live worker from
|
|
2040
|
+
another harness version adds a non-blocking `notice` naming both versions. A connect also
|
|
2041
|
+
replays the active run's whole activity as one `issuesActivity` with `replay: true`;
|
|
2042
|
+
`getIssuesRun` answers `issuesRun` with one run's metadata and activity. The client never
|
|
2043
|
+
polls: state and activity arrive as events, so switching modes and reconnecting send no frame.
|
|
2044
|
+
- `getScout` answers with `scoutState` and the newest scan's `scoutActivity`, on connect and on
|
|
2045
|
+
demand: the worker record, lifecycle, enabled and attached flags, the persisted limits, every
|
|
2046
|
+
issue row, the last GitHub read, the current scan, next attempt, errors, and notice. A connect
|
|
2047
|
+
replays a present state and the newest scan's whole activity before any reply to the client's
|
|
2048
|
+
own commands. `startScout` runs the preflight and spawns or attaches; `stopScout` writes one
|
|
2049
|
+
stop command; `setScoutLimits` carries `{ maxPerHour, maxPending }`, refuses a value outside
|
|
2050
|
+
1–100, writes `settings.json`, and writes one limits command when a worker is attached. There is
|
|
2051
|
+
no `approveScout`, `draftScoutGoal`, or `rejectScout`. The rows are pushed to every connected
|
|
2052
|
+
dashboard, so a filing or a close in one updates another without a reload.
|
|
2053
|
+
- `restartServer` broadcasts `serverRestarting`, then hands the process to its launcher, which
|
|
2054
|
+
stops the listener, starts the installed harness package, and exits. It is refused with an
|
|
2055
|
+
`error` when no launcher is configured or while a restart is already in progress. A client
|
|
2056
|
+
that clicked the reload control waits for the replacement to answer and reloads the page even
|
|
2057
|
+
if the acknowledgement was lost when the old process exited. With `"whenIdle": true`, a
|
|
2058
|
+
dashboard with active work answers `upgradePending` instead and restarts itself as soon as
|
|
2059
|
+
that work ends; an idle dashboard restarts immediately.
|
|
2060
|
+
- `saveGoal` replaces the goal document with `text` exactly as submitted and answers with the same text. A write failure leaves the file unchanged and answers with an `error`. It is accepted while the lifecycle is paused and refused while the lifecycle is advancing.
|
|
2061
|
+
- `startRun` without `file` starts the sequence with the lowest-numbered pending record.
|
|
2062
|
+
- `startRun` with `file` starts that record first only when it is pending; the sequence then
|
|
2063
|
+
continues with the lowest-numbered remaining pending records.
|
|
2064
|
+
- Every accepted start resolves one record at a time until no pending record remains or a run
|
|
2065
|
+
ends without resolving its record. A still-pending or removed active record ends the
|
|
2066
|
+
sequence.
|
|
2067
|
+
- A manual start is accepted while a lifecycle run is active. When the lifecycle is advancing,
|
|
2068
|
+
the server first pauses it at its interrupted stage and then starts the sequence; a start that
|
|
2069
|
+
cannot run leaves the lifecycle advancing. While the lifecycle is paused, the manual sequence
|
|
2070
|
+
runs beside the suspended lifecycle lanes, which keep their tabs, activity, and questions.
|
|
2071
|
+
Resolving the record the lifecycle itself has in progress is refused. When the sequence ends
|
|
2072
|
+
the lanes stay paused, with their tabs intact, until the lifecycle is resumed. `runStarted` events for a manual record carry `"manual": true`, and a client that
|
|
2073
|
+
connects during the sequence receives that conversation.
|
|
2074
|
+
- A start is refused with an `error` event while another manual sequence is running (including
|
|
2075
|
+
between records), while a goal revision, goal creation, or skills preparation is active, when
|
|
2076
|
+
no record is pending, or when the named record is not pending.
|
|
2077
|
+
- `answer` supplies one value for the pending elicitation; the question is cleared after the first answer.
|
|
2078
|
+
- `ask` sends one question and the captured element context for a conversation id. The first turn
|
|
2079
|
+
starts one ACP peer from `AGENT_COMMAND` in `REPO_DIR`; a follow-up reuses that peer and session,
|
|
2080
|
+
and a replacement peer receives the earlier turns. Only the asking socket receives the
|
|
2081
|
+
conversation's `askChunk`, `askDone`, and `askError` events, each carrying its conversation id.
|
|
2082
|
+
A question must be 1–4000 trimmed characters with a present, bounded context, one turn runs at a
|
|
2083
|
+
time per conversation, and events for a closed conversation are dropped. `closeAsk` closes the
|
|
2084
|
+
conversation and kills its peer; closing the socket closes every conversation it owned. See
|
|
2085
|
+
Report, remove, or redesign a UI element.
|
|
2086
|
+
- `reviseGoal` starts a guided goal revision with one ACP peer. `request` is optional free-form guidance passed to the agent; without it, the agent establishes the direction through its interview questions. The agent writes the goal document itself. Activity and questions use the existing `activity` and `question` events, and `answer` replies to a revision question exactly as it does to a run question. It is accepted while the lifecycle is paused, where the suspended lanes keep their tabs.
|
|
2087
|
+
- A revision is refused with an `error` event while the lifecycle is advancing, a run or run
|
|
2088
|
+
sequence is active, a manual ADR sequence is active, or another revision is active. While a revision is active, `startRun`, `startLifecycle`, `resumeLifecycle`, and another `reviseGoal` are each refused with an `error`, and no agent is spawned.
|
|
2089
|
+
- `createGoal` starts the guided goal creation with one ACP peer whose prompt is exactly
|
|
2090
|
+
`/goal-outcome`, the installed skill command that reads the harness prompt and interviews the
|
|
2091
|
+
developer. The server accepts it in every state: it replaces a goal document that already has
|
|
2092
|
+
content, and it preempts an in-flight creation or revision and stops an advancing ADR run,
|
|
2093
|
+
manual ADR sequence, or lifecycle before the new peer starts. It reads
|
|
2094
|
+
`<REPO_DIR>/GOAL.md` before and after the peer, while `getGoal` and `saveGoal` keep using
|
|
2095
|
+
`GOAL_FILE`. It is accepted while a lifecycle run is paused, where the suspended lanes keep
|
|
2096
|
+
their tabs, and it stops a manual sequence running beside those lanes. Activity and
|
|
2097
|
+
questions use the existing `activity` and `question` events, and `answer` replies to a creation question
|
|
2098
|
+
exactly as it does to a revision question; a second answer for the same question id is refused
|
|
2099
|
+
with an `error`. When the peer ends, the server reads `<REPO_DIR>/GOAL.md` again: text that
|
|
2100
|
+
contains at least one non-whitespace character completes the creation, while an absent, empty,
|
|
2101
|
+
or whitespace-only document is a failure that sends an `error` followed by a failed
|
|
2102
|
+
`goalCreationStopped`.
|
|
2103
|
+
- A creation is refused with an `error` event, without spawning a peer, only when
|
|
2104
|
+
`<REPO_DIR>/GOAL.md` exists but cannot be read. Otherwise a `createGoal` request always wins:
|
|
2105
|
+
the server cancels the active creation or revision, reports it stopped with `completed: false`,
|
|
2106
|
+
stops an advancing run or lifecycle and reports the idle `goal` stage, then starts the new peer.
|
|
2107
|
+
While a creation is active, `startRun`, `startLifecycle`, `resumeLifecycle`, and `reviseGoal`
|
|
2108
|
+
are each refused with an `error`, and no agent is spawned; a second `createGoal` restarts the
|
|
2109
|
+
creation instead. The creation runs in the server, so closing the browser does not stop it, and
|
|
2110
|
+
a client that connects during it receives `goalCreationStarted`, the recorded activity, and the
|
|
2111
|
+
pending question before its own command replies.
|
|
2112
|
+
- `prepareSkills` starts one ACP peer whose prompt is exactly `/prepare-skills`, the installed
|
|
2113
|
+
command that reads `.devboard/prompts/prepare-skills.md`. The server accepts it only while
|
|
2114
|
+
`<REPO_DIR>/GOAL.md` contains at least one non-whitespace character and no run, run sequence,
|
|
2115
|
+
lifecycle run (paused or advancing), goal creation, goal revision, or other preparation is
|
|
2116
|
+
active; otherwise it answers with an `error` and spawns no peer. The server snapshots
|
|
2117
|
+
`<REPO_DIR>/.omp/skills/`, deletes every skill carrying `metadata.devboard-managed: "true"`,
|
|
2118
|
+
and broadcasts `skillsPreparationStarted`. Activity and questions use the existing `activity`
|
|
2119
|
+
and `question` events, and `answer` replies to a preparation question like any other. When the
|
|
2120
|
+
peer exits, the server validates the managed set, compares every installed skill for
|
|
2121
|
+
incompatible directives, and checks that no unmanaged skill changed; success broadcasts
|
|
2122
|
+
`skillsPreparationStopped` with `completed: true`, the skill list, and an empty contradiction
|
|
2123
|
+
list, while any failure broadcasts an `error`, restores the snapshot, and broadcasts the same
|
|
2124
|
+
event with `completed: false`. `getSkills` answers with the current managed set read from disk
|
|
2125
|
+
and sends `skillsState`; the client issues it on every connection, so the panel is rebuilt after
|
|
2126
|
+
a reload or restart instead of starting a run. While preparation is active, `startRun`,
|
|
2127
|
+
`startLifecycle`, `resumeLifecycle`, `reviseGoal`, `saveGoal`, `createGoal`, and a second
|
|
2128
|
+
`prepareSkills` are each refused with an `error` and spawn no peer. `deleteSkill` (with `name`)
|
|
2129
|
+
removes exactly one installed managed skill of the prepare family from `<REPO_DIR>/.omp/skills/`,
|
|
2130
|
+
is accepted only while that directory is installed and no run, lifecycle, goal creation,
|
|
2131
|
+
revision, or preparation is active, and answers by broadcasting a fresh `skillsState`; it
|
|
2132
|
+
refuses an unmanaged skill, a Discover-family skill, and a name that is not in the managed set.
|
|
2133
|
+
- `resetImplementation` deletes and commits the implementation as described above. The server
|
|
2134
|
+
broadcasts `implementationResetStarted` when it accepts the command, performs the reset
|
|
2135
|
+
itself, and broadcasts `implementationResetStopped` with `completed: true` and the 40-hex
|
|
2136
|
+
`commit` only after `git push origin main` succeeds; a failed push includes the local commit
|
|
2137
|
+
with `completed: false`, and a failure before or during the commit omits `commit`. Every
|
|
2138
|
+
failure is preceded by one `error` carrying git's stderr where git failed. While a reset
|
|
2139
|
+
runs, `saveGoal`, `startRun`, `startLifecycle`, `pauseLifecycle`, `resumeLifecycle`,
|
|
2140
|
+
`reviseGoal`, `createGoal`, `prepareSkills`, `answer`, and a second `resetImplementation`
|
|
2141
|
+
are each refused with an `error`; `listRecords`, `getGoal`, `getProject`, `getIssues`, `getIssuesRun`,
|
|
2142
|
+
and `getSkills` still answer. A client that connects during a reset receives `implementationResetStarted` before its own
|
|
2143
|
+
command replies. On success the server follows the stopped event, in order, with
|
|
2144
|
+
`lifecycleState` at `goal` with pass `1`, `work: null`, and `paused: false`, `records` with
|
|
2145
|
+
an empty list and `next: null`, and `goal` with the unchanged text (empty when the document
|
|
2146
|
+
is absent).
|
|
2147
|
+
- `startLifecycle` starts the backend-owned parallel pipeline. It is refused with an `error`
|
|
2148
|
+
while a run, a run sequence, a manual ADR sequence, a goal revision, a goal creation, or
|
|
2149
|
+
another lifecycle run is active; when the goal document is absent, unreadable, or contains only
|
|
2150
|
+
whitespace; when `<REPO_DIR>/.omp/skills/` cannot be read; and when no managed skill is
|
|
2151
|
+
installed there. A refused start spawns no agent and reports no `lifecycleState`. An accepted
|
|
2152
|
+
start reports `lifecycleState` at `epic` with `work: null`, the pass already held, and
|
|
2153
|
+
`paused: false` before its first tracker query and before any agent starts. While the lifecycle
|
|
2154
|
+
run is active, `startLifecycle` is also refused. `startRun` is accepted and pauses every lane
|
|
2155
|
+
as described above, and the lifecycle resumes when the manual sequence ends.
|
|
2156
|
+
`reviseGoal` and `saveGoal` are refused only while the lifecycle is
|
|
2157
|
+
advancing.
|
|
2158
|
+
- `stopLifecycle` ends an active run for real. It closes every live lane peer, reports the idle
|
|
2159
|
+
`lifecycleState` at `goal` with `work: null`, `paused: false`, and the pass already reached,
|
|
2160
|
+
and publishes a lane snapshot with every stopped lane `failed` and `runningLanes: 0`. It
|
|
2161
|
+
leaves nothing suspended. It is refused with an `error` when no lifecycle run is active. A
|
|
2162
|
+
stopped run's loop finishes unwinding before a later `startLifecycle` is accepted, so the two
|
|
2163
|
+
runs never publish concurrently.
|
|
2164
|
+
- The accepted run reads every open epic and starts one implementation lane per epic at once,
|
|
2165
|
+
with no fixed cap. Each lane sends `/implement-next-epic <ref>` to its own ACP peer; a lane
|
|
2166
|
+
never selects a different epic. If no epic is open, the run sends one `/epic-next` peer.
|
|
2167
|
+
Generation is zero-or-more: it may file several pairwise-independent epics in one pass, and
|
|
2168
|
+
every epic it files starts its own lane immediately. Closing an epic increments `pass`, marks
|
|
2169
|
+
that lane `closed`, and requests a generation pass immediately, even while other lanes keep
|
|
2170
|
+
implementing; a request during an active pass coalesces into at most one follow-up, and two
|
|
2171
|
+
generation peers never run at once.
|
|
2172
|
+
- A lane that ends with its epic still open is marked `retrying` and its lane sends an `error`;
|
|
2173
|
+
it is retried after a five-second backoff and immediately when `main` advances. A lane whose
|
|
2174
|
+
peer prints a line beginning `DEVBOARD-BLOCKED:` is `parked`; it is retried only when `main`
|
|
2175
|
+
advances. Neither state ever creates a second concurrent lane for the same epic, and neither
|
|
2176
|
+
stops the other lanes. With no lane active, a generation pass that files
|
|
2177
|
+
nothing, and only parked epics remaining, the run sends an `error` naming every parked epic,
|
|
2178
|
+
reports the idle `goal` stage, and leaves those epics open; a new run clears the parked set.
|
|
2179
|
+
- When no epic remains, a generation pass files nothing, and no ADR is still pending, the
|
|
2180
|
+
lifecycle resolves each pending record in ascending file-name order and then reports
|
|
2181
|
+
`finished`. A pass that leaves the same pending record set as the previous one without an epic
|
|
2182
|
+
sends an `error` and stops. Every stop before `finished` reports the idle `goal` stage with
|
|
2183
|
+
`work: null`, `paused: false`, and the pass reached. A tracker or record read failure also
|
|
2184
|
+
stops the run. A forge can expose a peer's tracker write a moment after the peer exits, so the
|
|
2185
|
+
server re-reads the tracker for a bounded settle window per lane before trusting an answer that
|
|
2186
|
+
still shows the pre-peer state; pause holds the loop between reads, and an exhausted window
|
|
2187
|
+
accepts the last answer.
|
|
2188
|
+
- `pauseLifecycle` suspends every running lane with `SIGSTOP`, marks each lane `paused`, and holds
|
|
2189
|
+
the scheduler before the next peer start or stage transition. It reports `lifecycleState` with
|
|
2190
|
+
the interrupted `stage`, `pass`, and `work` and `paused: true`, then a lane snapshot with
|
|
2191
|
+
`runningLanes: 0`; a pause during the first lookup reports `epic` with `work: null`. No tracker
|
|
2192
|
+
result use, stage transition, retry, or new ACP peer happens until a resume. Pending lane
|
|
2193
|
+
questions stay in their tabs, and `answer` is accepted and buffered for the suspended child.
|
|
2194
|
+
Selecting a manual ADR start while the lifecycle advances pauses every lane the same way and
|
|
2195
|
+
resumes it once the sequence ends. An explicit pause command stays suspended until
|
|
2196
|
+
`resumeLifecycle`; the interface's single lifecycle button offers only start and stop, not
|
|
2197
|
+
pause.
|
|
2198
|
+
- `resumeLifecycle` reports the same `stage`, `pass`, and `work` with `paused: false`, continues
|
|
2199
|
+
every suspended lane with `SIGCONT`, and releases the held loop. It is refused while a goal
|
|
2200
|
+
revision or a manual ADR sequence is active, leaving the lifecycle paused until that peer ends.
|
|
2201
|
+
Pause and resume are each refused with an `error` when no lifecycle run is active, when pausing
|
|
2202
|
+
an already-paused run, and when resuming a run that is not paused. The paused flag is part of
|
|
2203
|
+
the durable state, so a run paused before a restart stays paused and spawns no peer until
|
|
2204
|
+
`resumeLifecycle`; it is discarded when the run ends.
|
|
2205
|
+
- Each lifecycle lane is one `omp acp` process. `runStarted`, `activity`, `question`, and
|
|
2206
|
+
`runStopped` for a lane carry its `lane` id; lanes also report their epic or record identity,
|
|
2207
|
+
attempt number, status, recorded activity, and pending question in `laneState`. Untagged
|
|
2208
|
+
`runStarted`, `activity`, `question`, and `runStopped` events remain the foreground run,
|
|
2209
|
+
revision, creation, and skills preparation. Lifecycle state is durable: the lifecycle, its
|
|
2210
|
+
pass, every lane tab with its activity and question, and the epic bookkeeping are written to
|
|
2211
|
+
the harness state directory keyed by `REPO_DIR` and restored when the backend starts. A client
|
|
2212
|
+
that connects while no run is active receives the idle `goal` state when the retained pass is
|
|
2213
|
+
above 1.
|
|
2214
|
+
|
|
2215
|
+
The server answers with events:
|
|
2216
|
+
|
|
2217
|
+
```json
|
|
2218
|
+
{ "type": "lifecycleState", "stage": "epic", "pass": 1, "work": null, "paused": false }
|
|
2219
|
+
{ "type": "lifecycleState", "stage": "adr", "pass": 1, "work": { "kind": "adr", "id": "0002-example.md" }, "paused": false }
|
|
2220
|
+
{ "type": "lifecycleState", "stage": "implementation", "pass": 1, "work": { "kind": "epic", "id": "42", "url": "https://github.com/owner/repo/issues/42" }, "paused": false }
|
|
2221
|
+
{ "type": "lifecycleState", "stage": "implementation", "pass": 1, "work": { "kind": "epic", "id": "42", "url": "https://github.com/owner/repo/issues/42" }, "paused": true }
|
|
2222
|
+
{ "type": "lifecycleState", "stage": "epic", "pass": 2, "work": null, "paused": false }
|
|
2223
|
+
{ "type": "lifecycleState", "stage": "goal", "pass": 2, "work": null, "paused": false }
|
|
2224
|
+
{ "type": "laneState", "openEpics": 3, "runningLanes": 3, "lanes": [ { "id": "lane-2", "kind": "implementation", "epic": { "number": 42, "title": "Add export", "url": "https://github.com/owner/repo/issues/42" }, "record": null, "attempt": 1, "status": "running", "activity": [ "Working on epic 42" ], "question": null }, { "id": "lane-3", "kind": "adr", "epic": null, "record": { "file": "0002-example.md", "title": "Choose a runtime" }, "attempt": 1, "status": "paused", "activity": [ "Inspecting 0002-example.md" ], "question": { "id": 1, "message": "Choose an answer", "options": [] } } ] }
|
|
2225
|
+
{ "type": "runStarted", "lane": "lane-2", "kind": "implementation", "attempt": 1, "file": "", "title": "Add export", "action": "implementing" }
|
|
2226
|
+
{ "type": "activity", "lane": "lane-2", "text": "Working on epic 42" }
|
|
2227
|
+
{ "type": "question", "lane": "lane-2", "id": 1, "message": "Choose an answer", "options": [] }
|
|
2228
|
+
{ "type": "runStopped", "lane": "lane-2", "file": "", "title": "Add export", "resolved": true, "status": "closed" }
|
|
2229
|
+
{ "type": "records", "records": [ { "file": "0001-example.md", "title": "Example", "status": "Accepted", "pending": false, "summary": "Use the simplest runtime." } ], "next": null }
|
|
2230
|
+
{ "type": "project", "path": "projects/devboard" }
|
|
2231
|
+
{ "type": "versionState", "running": "0.1.200", "installed": "0.1.201", "latest": "0.1.201", "stale": true }
|
|
2232
|
+
{ "type": "serverRestarting" }
|
|
2233
|
+
{ "type": "upgradePending" }
|
|
2234
|
+
{ "type": "goal", "text": "# Goal\n\nThe complete goal text." }
|
|
2235
|
+
{ "type": "goalRevisionStarted" }
|
|
2236
|
+
{ "type": "goalRevisionStopped", "completed": true, "text": "# Revised goal\n\nThe agent rewrote this." }
|
|
2237
|
+
{ "type": "goalRevisionStopped", "completed": false }
|
|
2238
|
+
{ "type": "goalCreationStarted" }
|
|
2239
|
+
{ "type": "goalCreationStopped", "completed": true, "text": "# Created goal\n\nThe agent wrote this." }
|
|
2240
|
+
{ "type": "goalCreationStopped", "completed": false }
|
|
2241
|
+
{ "type": "skillsPreparationStarted" }
|
|
2242
|
+
{ "type": "skillsPreparationStopped", "completed": true, "skills": [ { "name": "dependency-direction", "description": "Choose dependency direction." } ], "contradictions": [] }
|
|
2243
|
+
{ "type": "skillsPreparationStopped", "completed": false }
|
|
2244
|
+
{ "type": "skillsState", "skills": [], "contradictions": [] }
|
|
2245
|
+
{ "type": "implementationResetStarted" }
|
|
2246
|
+
{ "type": "implementationResetStopped", "completed": true, "commit": "0123456789abcdef0123456789abcdef01234567" }
|
|
2247
|
+
{ "type": "implementationResetStopped", "completed": false, "commit": "0123456789abcdef0123456789abcdef01234567" }
|
|
2248
|
+
{ "type": "implementationResetStopped", "completed": false }
|
|
2249
|
+
{ "type": "runStarted", "file": "0001-example.md", "title": "Example", "action": "resolving" }
|
|
2250
|
+
{ "type": "runStarted", "file": "", "title": "Improve watch mode", "action": "implementing" }
|
|
2251
|
+
{ "type": "runStarted", "file": "", "title": "Epic generation", "action": "generating" }
|
|
2252
|
+
{ "type": "runStarted", "file": "0002-example.md", "title": "Manual example", "action": "resolving", "manual": true }
|
|
2253
|
+
{ "type": "activity", "text": "Inspecting 0001-example.md" }
|
|
2254
|
+
{ "type": "activity", "text": " and its status line", "append": true }
|
|
2255
|
+
{
|
|
2256
|
+
"type": "question",
|
|
2257
|
+
"id": 1,
|
|
2258
|
+
"message": "Choose an answer",
|
|
2259
|
+
"options": [
|
|
2260
|
+
{ "label": "Alpha", "description": "Alpha leads to the first outcome.", "recommended": true },
|
|
2261
|
+
{ "label": "Beta", "description": "Beta leads to the second outcome.", "recommended": false }
|
|
2262
|
+
]
|
|
2263
|
+
}
|
|
2264
|
+
{ "type": "runStopped", "file": "0001-example.md", "title": "Example", "resolved": true, "status": "Accepted" }
|
|
2265
|
+
{ "type": "error", "message": "No pending ADRs to start." }
|
|
2266
|
+
```
|
|
2267
|
+
|
|
2268
|
+
- A `records` event carries every record in file-name order. `summary` is the first paragraph
|
|
2269
|
+
of the record's `Decision` section, shortened at a word boundary; it is empty when the record
|
|
2270
|
+
has no such section. `next` names the lowest-numbered pending record, or null.
|
|
2271
|
+
- `runStarted` carries `action`: `resolving` for an ADR peer, `implementing` when the lifecycle
|
|
2272
|
+
peer implements an epic, and `generating` when it creates one. The client labels the run from
|
|
2273
|
+
it, so an implementation is never described as a resolution.
|
|
2274
|
+
- `activity` events carry one line of the agent's output. An event with `"append": true`
|
|
2275
|
+
continues the previous line: the client appends `text` to it instead of starting a new one.
|
|
2276
|
+
Streamed ACP text updates are deltas of one assistant message (identified by its
|
|
2277
|
+
`messageId`) and are joined this way, so the developer reads whole messages rather than
|
|
2278
|
+
chunks; a different message id, a change from thought to reply, a tool call, or a plan
|
|
2279
|
+
starts a new line. The server stores the joined lines, so a client that connects during a
|
|
2280
|
+
run receives each line once, without the `append` flag. Text updates that carry no message
|
|
2281
|
+
id are complete lines and always start a new one. The interface renders the last line as the
|
|
2282
|
+
current action and lists earlier lines under a collapsed history.
|
|
2283
|
+
- A manual record `runStarted` carries `"manual": true`; a lifecycle stage's event omits the
|
|
2284
|
+
field. The flag lets the client keep its start controls disabled for the manual sequence while
|
|
2285
|
+
a paused lifecycle remains resumable. A client that connects during a manual sequence receives
|
|
2286
|
+
that `runStarted`, the recorded activity, and the pending question, ahead of the suspended
|
|
2287
|
+
lifecycle conversation.
|
|
2288
|
+
- A `question` event carries one option per choice. Each option has a `label`, an optional
|
|
2289
|
+
`description` stating the consequence of choosing it, and `recommended`, true for the answer
|
|
2290
|
+
the agent suggests. When the peer's schema marks none, the server marks the first option as
|
|
2291
|
+
recommended, so a choice list never lacks a suggestion. `answer` names the selected label as
|
|
2292
|
+
`value`.
|
|
2293
|
+
- A record's title and status come from its YAML frontmatter `title` and `status` fields.
|
|
2294
|
+
- A record is pending exactly when its status is `draft` or `proposed`; every other status is
|
|
2295
|
+
resolved.
|
|
2296
|
+
- Records are ordered by file name, and `next` is the first pending record in that order (the lowest-numbered pending file).
|
|
2297
|
+
- `lifecycleState` reports `stage` (`goal`, `epic`, `adr`, `implementation`, or `finished`),
|
|
2298
|
+
the `pass` number, `work` (`null`, an ADR filename, or an epic issue number), and `paused`.
|
|
2299
|
+
An epic `work` also carries the issue's `url` from the tracker when the origin reported one,
|
|
2300
|
+
which the page renders as a link. A `lifecycleState` is sent when a lifecycle run is accepted,
|
|
2301
|
+
on every transition and pause change, and to
|
|
2302
|
+
each client that connects or reconnects before any other event, ahead of that client's own
|
|
2303
|
+
command replies: while a lifecycle run is active, the current state; when no run is active
|
|
2304
|
+
and a state has been reported, the last reported state, including `finished` with its final
|
|
2305
|
+
pass or the idle `goal` state of an aborted run with the pass it reached. An accepted run
|
|
2306
|
+
is reported at `epic` with `work: null` before the origin-tracker query, and an open epic
|
|
2307
|
+
follows as a transition to `implementation`. A client that connects while paused receives
|
|
2308
|
+
`paused: true`, followed by the lane snapshot with every lane's recorded activity and pending
|
|
2309
|
+
question. Stage `goal` is the idle state before a lifecycle run starts and after one
|
|
2310
|
+
stops before `finished`; a backend on which no lifecycle run has ever started sends no
|
|
2311
|
+
`lifecycleState` on connection. The retained state is durable, so a replacement process
|
|
2312
|
+
replays the same `lifecycleState`, lanes, activity, questions, and pass.
|
|
2313
|
+
- `goalRevisionStarted` marks the beginning of a revision. `goalRevisionStopped` marks its end: `completed` is `true` only when the peer ended without error and the goal file could be read, in which case `text` carries the complete file contents. A failed revision has `completed: false`, omits `text`, and is accompanied by an `error` event.
|
|
2314
|
+
- A revision emits no `runStarted` or `runStopped` events of its own and does not read or write `adrs/`. When it ends while a lifecycle run is paused, the suspended lane tabs stay as they were; the server does not replay them because they were never removed.
|
|
2315
|
+
- `goalCreationStarted` marks the beginning of a guided goal creation. `goalCreationStopped`
|
|
2316
|
+
marks its end: `completed` is `true` only when the peer ended without error and
|
|
2317
|
+
`<REPO_DIR>/GOAL.md` could be read with at least one non-whitespace character, in which case
|
|
2318
|
+
`text` carries the complete file contents. A failed creation has `completed: false`, omits
|
|
2319
|
+
`text`, and is accompanied by an `error` event; that includes a peer that ends with an absent,
|
|
2320
|
+
empty, or whitespace-only goal document. A creation emits no `runStarted` or `runStopped`
|
|
2321
|
+
events and does not read or write `adrs/`. When it ends while a lifecycle run is paused, the
|
|
2322
|
+
suspended lane tabs stay as they were. A client that connects or reconnects during a creation
|
|
2323
|
+
receives `goalCreationStarted`, every recorded `activity` line, and the pending `question` if
|
|
2324
|
+
one exists, before any other event beyond the lifecycle state and lane snapshot.
|
|
2325
|
+
- A client that connects or reconnects during a revision receives `goalRevisionStarted`, every `activity` line recorded so far, and the pending `question` if one exists, before any other event beyond the lifecycle state and lane snapshot.
|
|
2326
|
+
- `skillsPreparationStarted` marks the beginning of a skills preparation, and
|
|
2327
|
+
`skillsPreparationStopped` marks its end. Success is `completed: true` with `skills` (each
|
|
2328
|
+
entry carries `name` and `description`, and nothing else) and `contradictions: []`; failure is
|
|
2329
|
+
`completed: false` without either field, and follows an `error`. A run that ends with no
|
|
2330
|
+
managed skill is a failure, like every other validation failure. A failed run restores
|
|
2331
|
+
`<REPO_DIR>/.omp/skills/` to its pre-run bytes, so the panel and the disk still show the
|
|
2332
|
+
previous managed set. Preparation emits no `runStarted`, `runStopped`, or `records` events and
|
|
2333
|
+
does not read or write `adrs/`. `skillsState` reports the managed skills read from disk and
|
|
2334
|
+
the current contradiction list; `getSkills` requests it, and a client that connects or
|
|
2335
|
+
reconnects during preparation first receives `skillsPreparationStarted`, every recorded
|
|
2336
|
+
`activity` line, and the pending `question` if one exists. The panel lists managed skills only
|
|
2337
|
+
and shows a contradiction list only when the review found one; the review itself covers every
|
|
2338
|
+
skill directory, managed or not.
|
|
2339
|
+
- The server reads ADR files and never writes them; the agent changes the status line. The server reads the tracker and never writes issues. The goal document is written verbatim by `saveGoal` and by the revision and creation agents, and the server reads it to answer `getGoal` and to report `goalRevisionStopped` or `goalCreationStopped`; a creation always writes `<REPO_DIR>/GOAL.md` even when `GOAL_FILE` names another file.
|
|
2340
|
+
- Skills preparation writes only under `<REPO_DIR>/.omp/skills/` and only through its ACP peer;
|
|
2341
|
+
the server deletes managed skill directories before the run and restores the snapshot after a
|
|
2342
|
+
failure. `runStopped` reports whether the active record is now resolved; a `records` event
|
|
2343
|
+
follows each `runStopped` before the next `runStarted`. One foreground agent — a run, manual
|
|
2344
|
+
sequence, goal revision, goal creation, or skills preparation — works at a time, in server
|
|
2345
|
+
memory, beside any number of lifecycle lanes. A paused lifecycle's suspended lanes coexist
|
|
2346
|
+
with a manual sequence or revision, and resume waits until that peer ends. Stopping the server terminates its child
|
|
2347
|
+
process and the loop.
|
|
2348
|
+
|
|
2349
|
+
## Tests
|
|
2350
|
+
|
|
2351
|
+
```bash
|
|
2352
|
+
cd .devboard
|
|
2353
|
+
bun test
|
|
2354
|
+
bun run verify:interface
|
|
2355
|
+
bun run verify:install
|
|
2356
|
+
bun run verify:update
|
|
2357
|
+
bun run verify:ui
|
|
2358
|
+
bun run verify:issues
|
|
2359
|
+
bun run verify:scout
|
|
2360
|
+
```
|
|
19
2361
|
|
|
20
|
-
`
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
dashboard.
|
|
2362
|
+
`verify:interface` drives the IDE shell against a seeded fixture host in headless Chrome and
|
|
2363
|
+
writes its screenshots under `.tmp/interface`. The other `verify:` scripts build fixture releases
|
|
2364
|
+
under `.devboard/.tmp`, install them into throwaway hosts, and assert the launcher, update, and
|
|
2365
|
+
reload contracts end to end. They never touch a real host.
|
|
25
2366
|
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
2367
|
+
`bun run verify:issues` is the deterministic end-to-end gate. It starts a source dashboard
|
|
2368
|
+
against a throwaway checkout with stub `gh` and ACP commands, starts the worker, and proves four
|
|
2369
|
+
simultaneous peers started in one poll with exact distinct prompts, per-agent run directories and
|
|
2370
|
+
activity files, per-agent questions answered by run id, an issue opened mid-run, one issue's
|
|
2371
|
+
cooldown leaving the others running, Check now retrying the gated issue, pause draining every peer,
|
|
2372
|
+
Stop closing every peer without a cooldown, connect-time replay of every agent, server-restart
|
|
2373
|
+
reattachment, per-repository isolation, same-repository attach, refusal with another `STATE_DIR`,
|
|
2374
|
+
legacy-lock refusal, that no `/implement-all-issues` prompt is ever sent, and that no systemd unit
|
|
2375
|
+
or timer is written and no `systemctl`/`loginctl` stub is invoked. It writes only under
|
|
2376
|
+
`.devboard/.tmp`.
|
|
2377
|
+
`bun run verify:scout` drives the built dashboard in headless Chrome against a fixture checkout,
|
|
2378
|
+
a stub ACP peer, and a stub `gh`: Start files significant items with no click, a duplicate and a
|
|
2379
|
+
score below 4 are dropped and named in the activity, the issue table shows the title anchor, the
|
|
2380
|
+
creation time, the category, the state, and the shipped version, both limits save and survive a
|
|
2381
|
+
reload, an invalid save shows its error and sends nothing, a close flips the row to implemented,
|
|
2382
|
+
a release published later fills its blank version, a failed read shows `error` with its reason
|
|
2383
|
+
and the last success time while keeping the rows, no review widget or command remains, and Stop
|
|
2384
|
+
leaves the table and a released lock. It writes only under `.devboard/.tmp`.
|
|
31
2385
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
2386
|
+
`server/scout.test.ts` covers recommendation identity and the body's `## Category`, result
|
|
2387
|
+
validation (over-count, malformed item, multi-line title, path escape, excluded tree, duplicate
|
|
2388
|
+
id, out-of-range span, uncited file, shortfall, missing score, out-of-range score, missing
|
|
2389
|
+
reason), the version-2 request, the shipped-version rules (earliest release, tie by tag,
|
|
2390
|
+
timeline commit, newest comment SHA, open issue, no release), settings and state migration,
|
|
2391
|
+
the bridge's limits and absent decision path, the Stop/limits command surface, the Start preflight
|
|
2392
|
+
refusals, and the activity delta.
|
|
2393
|
+
`tests/scout.test.ts` covers filing without a command, the significance score and issue body,
|
|
2394
|
+
duplicates against closed issues, open issues, and drafts, both caps across a restart, the
|
|
2395
|
+
effective batch, zero capacity, a close freeing a slot, limits applying live and surviving a
|
|
2396
|
+
restart, state mapping and a reopen, a later release filling a blank version, a failed periodic
|
|
2397
|
+
read, a periodic read that never settles failing at its deadline and releasing the worker, the
|
|
2398
|
+
default client's bounded `gh` calls, a failed pre-filing read, migration from a version-1 board, a
|
|
2399
|
+
missing score failing the scan atomically, a failed filing, Stop keeping the rows, the live lock,
|
|
2400
|
+
a replacement closing an interrupted scan, bridge replay without a spawn, another `STATE_DIR`
|
|
2401
|
+
refused, a harness-version notice, the issue body and label creation, and the scan prompt's rubric.
|
|
2402
|
+
`tests/issues.test.ts` covers the fleet (four mixed issues, twelve peers with no cap, duplicate
|
|
2403
|
+
prevention, an issue opened mid-run, per-issue success and ledger entries, one failure cooling only
|
|
2404
|
+
its issue), Check now clearing every cooldown, pause draining every peer, Stop closing every peer
|
|
2405
|
+
without a cooldown, per-agent questions and answer routing, two orphan children on a fresh start,
|
|
2406
|
+
version 1 state loading, reopened-issue eligibility, history pruning that keeps every running
|
|
2407
|
+
directory, preflight for both wrappers, repo-scoped state and lock isolation, atomic and damaged
|
|
2408
|
+
state, ledger migration, the activity bounds, and the conditional tracker. `tests/cleanup.test.ts`
|
|
2409
|
+
proves the watchdog preserves repo-scoped issues state, the repo-anchored lock, and Flow session
|
|
2410
|
+
records that Flow restore still reads, while sweeping legacy orchestrator run files. Every fixture
|
|
2411
|
+
stays under `.devboard/.tmp`.
|
|
2412
|
+
`tests/launcher.test.ts` covers the single public package: the manifest, the help output, the
|
|
2413
|
+
tarball's required and forbidden paths, the fallback asset name, and template identity. `lib/inventory.test.ts` covers the publish report. `lib/release.test.ts`
|
|
2414
|
+
covers installing a release when the process working directory has been deleted.
|
|
2415
|
+
`server/updates.test.ts` covers the startup check, the interval, backoff, single-flight, and
|
|
2416
|
+
failure journaling.
|
|
2417
|
+
`tests/install.test.ts` covers the wrapper script: a fresh install, an idempotent rerun that
|
|
2418
|
+
keeps unrelated files, a host without `.devboard/`, a differing file refused without `--force`,
|
|
2419
|
+
`--force` replacing it, and the default host path. `server/run.test.ts` drives the interface
|
|
2420
|
+
against the stub ACP agent, covering sequential
|
|
2421
|
+
resolution in file-name order, selected first records, refusal of a second start during a run
|
|
2422
|
+
and between runs, activity and question/answer relay, the unresolved-record stop, the
|
|
2423
|
+
removed-record stop, disconnect continuation, records refresh, and an idle restart deferred
|
|
2424
|
+
until the active run ends. `server/app.test.ts`
|
|
2425
|
+
covers `getGoal`/`saveGoal`, including verbatim writes, failure responses, and the absence of
|
|
2426
|
+
agent startup. `server/goal-revision.test.ts` covers guided goal revision: request and
|
|
2427
|
+
no-request starts, activity and question relay, the rewritten goal reported verbatim, refusal
|
|
2428
|
+
during lifecycle and record runs (including between records), refusals during a revision,
|
|
2429
|
+
reconnect replay, the unreadable-goal failure, and the absence of run events or ADR writes.
|
|
2430
|
+
`server/goal-creation.test.ts` covers guided goal creation: the `/goal-outcome` prompt, no
|
|
2431
|
+
spawn on connect, activity and question relay, the created goal reported verbatim from
|
|
2432
|
+
`<REPO_DIR>/GOAL.md`, the unreadable goal before and after the peer, an absent goal answered as
|
|
2433
|
+
empty, an empty or whitespace-only result reported as a failed creation that leaves the
|
|
2434
|
+
document unchanged and creation available, every refusal, the
|
|
2435
|
+
single-agent refusals while creating, reconnect replay before the late client's own replies,
|
|
2436
|
+
acceptance while the lifecycle is paused with the paused lane tabs retained, completion without
|
|
2437
|
+
reading or writing `adrs/`, and the custom `GOAL_FILE` split.
|
|
2438
|
+
`server/lifecycle.test.ts` covers the `lifecycleState` sequence, the running `epic` state
|
|
2439
|
+
reported before the first tracker lookup and to every client, stopping a running lifecycle and
|
|
2440
|
+
its in-flight lanes with the idle state that follows, the single `/epic-next`
|
|
2441
|
+
peer, ADR-order resolution, implementation of an already-open epic, return to epic generation
|
|
2442
|
+
on the next pass, the settle window that waits out a lagging tracker after epic generation and
|
|
2443
|
+
after implementation and that finishes when the window is exhausted, a lane that stays open
|
|
2444
|
+
marked `retrying` without stopping the run, retry after the backoff, the parked-only stop, the
|
|
2445
|
+
finished transition when a pass yields no epic and no pending ADR, restart after that
|
|
2446
|
+
transition, the lifecycle and goal-save refusals during implementation plus an empty ADR start,
|
|
2447
|
+
replay of the implementation state and activity to a reconnecting client and
|
|
2448
|
+
of the finished state to a client that connects after the run ends, the absence of a
|
|
2449
|
+
`lifecycleState` before any run, the new run's stage after restarting from `finished`, and the
|
|
2450
|
+
goal precondition: an absent, whitespace-only, or just-cleared goal refuses the start with one
|
|
2451
|
+
`error` and no agent, while a non-blank goal starts the loop. It also covers each abort: a
|
|
2452
|
+
same-open-epic stop, epic generation leaving the same pending records, and a throwing tracker
|
|
2453
|
+
query each end with the idle `goal` state, `work: null`, `paused: false`, and the aborting
|
|
2454
|
+
pass; a later client receives that state first, goal saving answers with the saved text, and
|
|
2455
|
+
a new lifecycle start is accepted and runs without a stale refusal.
|
|
2456
|
+
`server/skills-preparation.test.ts` covers skills preparation: the `prepareSkills` command and
|
|
2457
|
+
its `/prepare-skills` prompt, the project-agnostic prompt rules, one peer, activity and question
|
|
2458
|
+
relay, the non-whitespace goal precondition, every refusal while preparation runs and while each
|
|
2459
|
+
other activity runs, reconnect replay and disk replay after a restart, skill layout/name and
|
|
2460
|
+
description validation, the failure when no managed skill is prepared, managed-only replacement
|
|
2461
|
+
with an unmanaged skill preserved byte for byte, the all-skill contradiction pass, the
|
|
2462
|
+
unmanaged-conflict and name-collision errors, invalid output and peer failure restoring the
|
|
2463
|
+
previous managed set, and the absence of `GOAL.md` or `adrs/` writes.
|
|
2464
|
+
`server/lifecycle-pause.test.ts` covers pausing during an implementation and resuming the same
|
|
2465
|
+
peer, pausing with a pending question and carrying its answer across the pause, pausing between
|
|
2466
|
+
stages while a tracker query is outstanding, pausing the initial tracker lookup and resuming
|
|
2467
|
+
into its result, paused lane replay with recorded activity and question, goal saving while
|
|
2468
|
+
paused, manual ADR resolution while paused and while advancing (which pauses first), automatic
|
|
2469
|
+
resume when the manual sequence ends, stopping a paused run for good, refusal of
|
|
2470
|
+
the lifecycle's own in-progress record and of resume during the manual sequence, retention of
|
|
2471
|
+
the paused lane tabs when the sequence ends, guided goal revision while paused including replay
|
|
2472
|
+
of the paused lanes and every refusal path with unchanged state.
|
|
2473
|
+
`server/parallel-lifecycle.test.ts` covers the parallel pipeline: one overlapping implementation
|
|
2474
|
+
peer per open epic, five lanes with no cap, stopping every running lane at once and returning to
|
|
2475
|
+
the idle stage, generation filing several epics that start their own
|
|
2476
|
+
lanes immediately, refill on close while other lanes keep implementing, coalesced refill
|
|
2477
|
+
triggers, per-epic assignment, retry after backoff, immediate retry when `main` advances, a
|
|
2478
|
+
parked lane isolated from the others and retried on `main`, the parked-only error and idle
|
|
2479
|
+
`goal` stage, reconnect replay of counts, tabs, activity, and a lane question, pause and resume
|
|
2480
|
+
across lanes, reset across lanes, finish when nothing remains, and five-epic tracker reads
|
|
2481
|
+
through the existing GitHub and GitLab query shapes.
|
|
2482
|
+
`server/reset-implementation.test.ts` drives the reset against temporary hosts with local bare
|
|
2483
|
+
remotes: the command and started/stopped events, tracked/untracked/ignored deletion, keep-set
|
|
2484
|
+
bytes, the `.worktree` sentinel, symlink removal without following, the exact commit subject and
|
|
2485
|
+
remote tree, staging exclusions without a `.gitignore`, empty-change idempotence, an absent,
|
|
2486
|
+
empty, or whitespace-only goal, a differently named `GOAL_FILE`, a bypassed failing pre-commit
|
|
2487
|
+
hook, refusal off `main`, without `origin`, outside the top level, with an unborn `HEAD`, and
|
|
2488
|
+
during an active run, a rejecting pre-receive hook retaining the local commit, a commit failure
|
|
2489
|
+
retaining the staged deletion, mutating-command refusal during a reset, replay to a late client,
|
|
2490
|
+
and the post-reset empty records, goal text, `getGoal`, and `saveGoal` behavior.
|
|
2491
|
+
`server/journal.test.ts` covers the dashboard journal: a real stub-agent run writes `activity`,
|
|
2492
|
+
`question`, `run-start`, and `run-stop` records with `ts`, `repo`, `port`, and `pid`; a write
|
|
2493
|
+
failure is reported without interrupting the run; rotation keeps every record; and pruning
|
|
2494
|
+
applies the seven-day limit and the size bound without deleting the live window.
|
|
2495
|
+
`tests/watchdog.test.ts` covers discovery (de-duplication, harness resolution, self-exclusion,
|
|
2496
|
+
legacy logs), the inclusive ten-minute window, omp-log attribution and self-exclusion, the exact
|
|
2497
|
+
evidence mapping (prose never produces a finding), host-independent fingerprints, filing and
|
|
2498
|
+
commenting against a stub `gh`, pending retries after a failed call, duplicate windows, the
|
|
2499
|
+
version 1 ledger migration, and the absence of any commit, merge, push, or publish.
|
|
2500
|
+
`tests/watchdog-api.test.ts` covers the read-only endpoint, the newest-cycle projection, the
|
|
2501
|
+
cycle history's newest-first order, 100-directory cap, `latest.json` fallback, and malformed-report
|
|
2502
|
+
skip, issue row order and fields, open/closed state mapping, timeline and comment shipped-version
|
|
2503
|
+
resolution with the earliest-release rule, the `ok`/`error`/`stale` transitions against a stubbed
|
|
2504
|
+
GitHub reader and clock, the request-never-waits contract, and byte-identical state. All new
|
|
2505
|
+
fixtures live under `.devboard/.tmp`; no test uses the network or a real model.
|