@bongos/core 1.19.877 → 1.19.879

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.bongos-core.json +54 -84
  2. package/docs/copy-inventory.md +195 -283
  3. package/docs/copy-registry.json +254 -1141
  4. package/docs/file-map.md +2 -2
  5. package/docs/module-api-changelog.md +5 -1
  6. package/docs/recipes/autobongos-windows-host.md +26 -1
  7. package/modules/autonomy/digest.js +344 -0
  8. package/modules/hall-ui/module.json +1 -1
  9. package/modules/hall-ui/public/builders.js +1 -13
  10. package/modules/hall-ui/public/dom-utils.js +4 -4
  11. package/modules/hall-ui/public/hall-render.js +2 -18
  12. package/modules/hall-ui/public/index.html +0 -3
  13. package/modules/hall-ui/public/panel.css +8 -9
  14. package/modules/hall-ui/public/settings.css +0 -5
  15. package/modules/hall-ui/public/settings.html +0 -29
  16. package/modules/hall-ui/public/settings.js +1 -26
  17. package/modules/hall-ui/public/shell.js +0 -1
  18. package/modules/hall-ui/public/style.css +0 -44
  19. package/modules/hall-ui/public/watch.html +0 -7
  20. package/modules/hall-ui/public/watch.js +1 -37
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/scripts/gds/autobongos-digest.js +123 -0
  24. package/scripts/gds/autobongos-run.js +7 -0
  25. package/scripts/gds/autobongos-service.ps1 +7 -1
  26. package/scripts/gds/provision-core-upgrade.js +23 -3
  27. package/src/bongos/serve-internal.js +11 -11
  28. package/src/module-api.js +1 -1
  29. package/tests/autonomy_digest.mjs +302 -0
  30. package/tests/core_upgrade_runner.mjs +43 -1
  31. package/tests/government_page_gates.mjs +1 -1
  32. package/tests/hall_landing_boot.mjs +1 -1
  33. package/tests/hall_nav.mjs +1 -1
  34. package/tests/hall_oversight_world.mjs +5 -5
  35. package/tests/hall_page_gate_map.mjs +11 -2
  36. package/tests/hall_settings_world.mjs +21 -18
  37. package/modules/hall-ui/public/box-panel.js +0 -456
  38. package/modules/hall-ui/public/harbor.css +0 -9
  39. package/modules/hall-ui/public/harbor.html +0 -69
  40. package/modules/hall-ui/public/harbor.js +0 -367
  41. package/modules/hall-ui/public/harbor.states.json +0 -77
  42. package/modules/hall-ui/public/pair.css +0 -23
  43. package/modules/hall-ui/public/pair.html +0 -58
  44. package/modules/hall-ui/public/pair.js +0 -132
  45. package/modules/hall-ui/public/pair.states.json +0 -14
package/docs/file-map.md CHANGED
@@ -135,7 +135,7 @@
135
135
  │ ├── sessions/ ← CORE-DOMAIN carve (BV1.R82, default-on): the session_records corpus + data plane (upload, cross-builder search/deep-dive), the BFG read-the-room scorer, session-pulse/stale-claim helpers. Registers no port — reached via HTTP POST to its own /sessions route; resolves the economy `reward` port for the cost-plus token reward. CLAUDE.md, module.json, routes/sessions.js, bfg.js, pulse.js.
136
136
  │ ├── autonomy/ ← CORE-DOMAIN carve (BV1.R83, default-on): the MAS autonomy surfaces — the $0 deterministic autonomy gate (System 3, inert until armed), the conductor/architect CLIs, and the in-process routine timers (incl. the weekly peer-vote-tally poller). Registers no port; resolves the lifecycle `tallyPeerVotes` doorway. CLAUDE.md, module.json, routes/autonomy.js, autonomy.js.
137
137
  │ ├── status-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R84, default-on): the public status dashboard at status.<apex>, moved from public-status/. NO routes/db/port — declares contributes.webSurfaces:[{host:"status.",dir:"public"}], served by the loader web-surface seam (moduleWebSurfaces → serve-internal). CLAUDE.md, module.json, public/{index.html,status.js,style.css,cursors/,snapshots/}.
138
- │ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Four ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages), `oversight.css` (the OVERSIGHT family — government, watch, harbor, gate, task 1003469: the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface) and `panel.css` (the PANEL family — settings, profile, pair, task 1003308: the row, the switch, the field with its label above, the chip control, the four voices and the sign-in gate; the hall's my-projects page was DELETED in the same task — "my projects" is the hub's user-level page, and the hall keeps no link or redirect; the family's fourth page `not-ready.html` was deleted by task 1003450, along with the emptied `requireNonXenosPage` gate that was the only thing redirecting to it). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `settings.css` the rail layout, the group head, the token ladder and the voices grid, `profile.css` the identity band and the shelf, `pair.css` the device row, `people.css` the directory's identity cell (task 1003470 made the legacy door `/scouting` ship the same body and script as `/people`; task 1003440 then deleted the door — `scouting.html` and `scouting.states.json` are gone and the URL 301s to `/people`).
138
+ │ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,gate,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Four ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages), `oversight.css` (the OVERSIGHT family — government, watch, gate, task 1003469 (harbor was its fourth until task 1003890 removed the dev box): the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface) and `panel.css` (the PANEL family — settings, profile, task 1003308 (pair was its third until task 1003890 removed the dev box): the row, the switch, the field with its label above, the chip control, the four voices and the sign-in gate; the hall's my-projects page was DELETED in the same task — "my projects" is the hub's user-level page, and the hall keeps no link or redirect; the family's fourth page `not-ready.html` was deleted by task 1003450, along with the emptied `requireNonXenosPage` gate that was the only thing redirecting to it). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `settings.css` the rail layout, the group head, the token ladder and the voices grid, `profile.css` the identity band and the shelf, `people.css` the directory's identity cell (task 1003470 made the legacy door `/scouting` ship the same body and script as `/people`; task 1003440 then deleted the door — `scouting.html` and `scouting.states.json` are gone and the URL 301s to `/people`).
139
139
  │ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
140
140
  │ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. R03 (task 1003114, ADR 0233) added the PROPOSAL: an artist writes the replacement wording and it becomes a Bongos task carrying a fenced copy-proposal patch — no row, NO second migration (a proposals table is the forbidden column with extra steps), and scripts/gds/copy-apply.js lands it as a real diff under a claim. CLAUDE.md, module.json, registry.js, flags.js, proposals.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms,copy_proposals}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
141
141
  │ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans, then the detector), tells.js (task 1003329 / ADR 0223: the anti-pattern detector as two tiers — floors FAIL, craft tells WARN, kit-ignore waivers per file; the rendered twins live in lib.js factsInPage), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. minimalist-ui/, industrial-brutalist-ui/, gpt-taste/, stitch-design-taste/ (task 1003327 / ADR 0230) are the four looks, composition grammars with no colours of their own: the palette is the instance's pack and the texture is a named entry in styles/; a substrate is an archetype that still ships both modes, gpt-taste's selection is derived from the route rather than rolled, and stitch-design-taste writes a DESIGN.md-shaped contract document whose thirteen are transcribed from the pack. image-to-code/, imagegen-frontend-web/, imagegen-frontend-mobile/, brandkit/ (task 1003328 / ADR 0222) are the image family under the hero-only rule: plates and hero objects only, the pair (JPEG dark plate + WebP alpha cutout) with a <file>.json sidecar beside each, through the instance's art pipeline key; the grove specimen's styles/grove/assets/ is the worked example. style/ (task 1003475 / ADR 0231) is the module's first FIRST-PARTY skill (no upstream, so a `## First party` section instead of `## Rebuilt from`): the /style session in which an owner authors their own look in four bounded steps, rendering the nearest library entries before asking, drafting inside the contract, retuning by measurement, and handing over the one-step adoption without ever writing config/branding.json. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove, blueprint (the first cool look, authored through /style at task 1003475); STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
@@ -265,7 +265,7 @@
265
265
  │ ├── terrain.js ← (Phase D + ADR 0012, CJS): TERRAIN + OBJECT enums + BLOCKING_TERRAINS + BLOCKING_OBJECTS (server twin of public/game/world/terrain.js)
266
266
  │ └── map.js ← (Phase D + ADR 0012): two-grid INITIAL_MAP {ground, objects}; isBlocked checks both layers (mirrors public/game/world/{terrain,map}.js)
267
267
  │ (public-status/ moved to modules/status-ui/public/ in BV1.R84 — see the modules/ block above)
268
- │ (public-builders/ moved to modules/hall-ui/public/ in BV1.R85 — the authenticated builders' hall; see the modules/ block above. It keeps its DEDICATED serving block in src/bongos/serve-internal.js: the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection — partitioned out of the generic web-surface loop. index.html=the hall dashboard; work/board/roadmap/goals/watch/harbor/gate/pair/settings/ranks/sessions/primer/diagrams/atlas pages; ideas.{html,js,css}=the Full Idea composer (task 1002928 / R152 of goal 1000062 — the five R141 fields with a live R142 completeness meter, both fetched from the server so the page carries no copy of the rules; drafts in localStorage so navigation costs nothing. Task 1002930 / R154 added the SPARK QUEUE above it and a second mode: `?develop=<id>` reuses the same form to develop a spark — the spark's words shown above the fields, its title read-only, and the file routed to POST /inbox/:id/develop instead of POST /inbox, under its own per-idea draft key); idea.html/idea.js=the idea-detail PAGE (/idea/:id, task 1002929 / R153 — the SECOND two-segment hall page after /task/:id: grade, the five answers in template order, the score, and both attributions. The GRADE decides what the page contains — a quick idea gets no five-field skeleton. task-detail.js keeps the #/idea/N overlay because ~320 of its lines are the Metic+ triage panel; the overlay now links here); profile.html/profile.js=the public, shareable builder profile page (/profile?id=N — rank, works, credits, karma, achievements; task 1467); profile-connections.js=section 6 of that page, the hub-only connections block (task 1002296 / R20 of goal 1000045 — the viewer-relative 1st/2nd/3rd/out-of-network label, the mutuals it was computed from, the connect button, and on your own profile both request queues plus your connections ledger. Loaded BEFORE profile.js, which calls window.OTBConnections.render behind a guard, so a page that omits the script simply has no section; the section reveals only when the hub's /connections routes answer, exactly as the cross-project record does. ADR 0212 records why it is on this page rather than the not-yet-built /u/&lt;handle&gt;, and why it addresses a builder by github_login and then uses the canonical handle the API echoes back); profile-canonical.js=the canonical-profile wiring for that page (task 1002804 / P08, ADR 0214 — on a FEDERATED instance the rel=canonical plus the visible "see the cross-project record" link-out ADR 0171 D1 asks for; on the HUB the numeric ?id= form redirects to the handle URL instead, because a page cannot both redirect to a URL and declare it canonical. All three read ONE resolver, modules/platform-identity/canonical-profile.js, which answers null while /u/&lt;handle&gt; does not exist — so everything is dark today and one boolean turns it all on together); task.html/task.js=the task-detail page (/task/:id, task 751 — replaced the old #/task/N overlay); builders.js=the controller; dom-utils.js/hall-widgets.js=the widget+settings-panel registries.)
268
+ │ (public-builders/ moved to modules/hall-ui/public/ in BV1.R85 — the authenticated builders' hall; see the modules/ block above. It keeps its DEDICATED serving block in src/bongos/serve-internal.js: the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection — partitioned out of the generic web-surface loop. index.html=the hall dashboard; work/board/roadmap/goals/watch/gate/settings/ranks/sessions/primer/diagrams/atlas pages (harbor + pair left with the dev box, task 1003890); ideas.{html,js,css}=the Full Idea composer (task 1002928 / R152 of goal 1000062 — the five R141 fields with a live R142 completeness meter, both fetched from the server so the page carries no copy of the rules; drafts in localStorage so navigation costs nothing. Task 1002930 / R154 added the SPARK QUEUE above it and a second mode: `?develop=<id>` reuses the same form to develop a spark — the spark's words shown above the fields, its title read-only, and the file routed to POST /inbox/:id/develop instead of POST /inbox, under its own per-idea draft key); idea.html/idea.js=the idea-detail PAGE (/idea/:id, task 1002929 / R153 — the SECOND two-segment hall page after /task/:id: grade, the five answers in template order, the score, and both attributions. The GRADE decides what the page contains — a quick idea gets no five-field skeleton. task-detail.js keeps the #/idea/N overlay because ~320 of its lines are the Metic+ triage panel; the overlay now links here); profile.html/profile.js=the public, shareable builder profile page (/profile?id=N — rank, works, credits, karma, achievements; task 1467); profile-connections.js=section 6 of that page, the hub-only connections block (task 1002296 / R20 of goal 1000045 — the viewer-relative 1st/2nd/3rd/out-of-network label, the mutuals it was computed from, the connect button, and on your own profile both request queues plus your connections ledger. Loaded BEFORE profile.js, which calls window.OTBConnections.render behind a guard, so a page that omits the script simply has no section; the section reveals only when the hub's /connections routes answer, exactly as the cross-project record does. ADR 0212 records why it is on this page rather than the not-yet-built /u/&lt;handle&gt;, and why it addresses a builder by github_login and then uses the canonical handle the API echoes back); profile-canonical.js=the canonical-profile wiring for that page (task 1002804 / P08, ADR 0214 — on a FEDERATED instance the rel=canonical plus the visible "see the cross-project record" link-out ADR 0171 D1 asks for; on the HUB the numeric ?id= form redirects to the handle URL instead, because a page cannot both redirect to a URL and declare it canonical. All three read ONE resolver, modules/platform-identity/canonical-profile.js, which answers null while /u/&lt;handle&gt; does not exist — so everything is dark today and one boolean turns it all on together); task.html/task.js=the task-detail page (/task/:id, task 751 — replaced the old #/task/N overlay); builders.js=the controller; dom-utils.js/hall-widgets.js=the widget+settings-panel registries.)
269
269
  │
270
270
  ├── infra/ ← ON-BOX scripts (task 1002699): the four reporter/heartbeat crons every dev box runs from its /workspace clone — box-heartbeat.sh (idle-sweep activity signal), box-report-version.sh (HEAD → POST /box/version), box-report-host-keys.sh (SSH host keys → POST /box/host-keys), box-report-terminal.sh (ttyd URL+credential → POST /box/terminal). Delivered by the workspace clone itself (modules/dev-box/box-access.js sparse scope includes infra/); crons self-installed by box-source-fetch.sh's per-tick bring-up. API base resolves GDS_BASE env → session-file api_base → skip (no hardcoded host in the neutral core).
271
271
  ├── devbox-app/ ← OTB Dev Box — Electron menu-bar/tray switch for the builder's box (#904, ADR 0045; internal). SELF-CONTAINED: own package.json (never feeds the droplet's npm ci); built by .github/workflows/build-devbox-app.yml; published to the public releases repo per config/devbox-app.json; release recipe docs/recipes/devbox-app-release.md
@@ -2227,6 +2227,10 @@ is load-bearing: the script throws rather than guess if it is missing, and
2227
2227
  1.19.876 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2228
2228
  landed since 1.19.875 with no explicit bump. run 35420916304. (task 1002620)
2229
2229
  1.19.877 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2230
- landed since 1.19.876 with no explicit bump. run 35438291185. (task 1002620)
2230
+ landed since 1.19.876 with no explicit bump. run 35438492966. (task 1002620)
2231
+ 1.19.878 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2232
+ landed since 1.19.877 with no explicit bump. run 35439004814. (task 1002620)
2233
+ 1.19.879 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2234
+ landed since 1.19.878 with no explicit bump. run 35439529685. (task 1002620)
2231
2235
  ---------------------------------------------------------------------------
2232
2236
  ```
@@ -101,7 +101,32 @@ A runner can be **Allowed to work** and **Not checking in** at the same time:
101
101
  that is a machine that is off, unplugged or wedged, and it is the state worth
102
102
  noticing.
103
103
 
104
- The full log lives on the box at `%LOCALAPPDATA%\cloudbongos\autobongos.log`.
104
+ ### Reading the night
105
+
106
+ The log answers "what happened" only if you are willing to read a log, and the
107
+ whole premise is that you are not at the keyboard. One command gives you the
108
+ night in five answers instead:
109
+
110
+ ```powershell
111
+ powershell -ExecutionPolicy Bypass -File scripts\gds\autobongos-service.ps1 digest
112
+ ```
113
+
114
+ or from any checkout: `node scripts/gds/autobongos-digest.js` (add `--since 24h`
115
+ for a rolling window, `--all` for everything, `--json` for the structured shape).
116
+
117
+ It answers: **what shipped**, **what is on hold and why**, **what each worker
118
+ decided that its task description did not determine**, **what it cost**, and
119
+ **where both limit windows stand**.
120
+
121
+ The third one is the one to actually read. A worker will not reliably stop and
122
+ ask — given an under-specified task it makes the call and writes it into the
123
+ file, which is what a good engineer does. So the decisions happen whether or not
124
+ anyone sees them, and every line in that section is a sentence the task
125
+ description should have contained. The task with the most entries is the thinnest
126
+ spec you wrote, and the digest names it.
127
+
128
+ The full log still lives on the box at `%LOCALAPPDATA%\cloudbongos\autobongos.log`
129
+ if you want the raw rows.
105
130
 
106
131
  ## 5. Stopping it
107
132
 
@@ -0,0 +1,344 @@
1
+ 'use strict';
2
+
3
+ // modules/autonomy/digest.js — the night, readable afterwards (task 1003906,
4
+ // ADR 0307). Pure: rows in, a digest out. No I/O, no clock of its own.
5
+ //
6
+ // THE OWNER IS NOT AT THE KEYBOARD. That is the whole premise, and it means the
7
+ // run log — an append-only JSONL nobody will ever open — is not an answer. One
8
+ // page has to answer five questions, and it has to answer them without anyone
9
+ // reading a log: what shipped, what is on hold and why, what each worker decided
10
+ // that its task description did not determine, what it cost, and where the two
11
+ // limit windows stand.
12
+ //
13
+ // THE THIRD QUESTION IS THE ONE THAT COMPOUNDS, and it is worth saying why it is
14
+ // here at all. A worker will NOT reliably stop to ask: tested on an ambiguous
15
+ // task, it made four genuine judgement calls and wrote them straight into the
16
+ // file, which is what a competent engineer does with an under-specified brief.
17
+ // So the decisions happen either way. What the digest changes is whether anyone
18
+ // finds out. And every entry is also a sentence the task description SHOULD have
19
+ // contained — which makes this list a spec-quality signal, not just an audit
20
+ // trail: the goals whose decisions pile up are the goals whose specs are thin,
21
+ // and fixing those is what lowers tomorrow's halt count.
22
+ //
23
+ // WHAT COUNTS AS SHIPPED IS THE LEDGER'S ANSWER, never the worker's. A `worked`
24
+ // row with `verified: false` is a task that ran and did not ship, and putting it
25
+ // under "shipped" because a session ended would reintroduce the exact confusion
26
+ // task 1003903 exists to end.
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Helpers
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const isRow = (r) => r && typeof r === 'object' && typeof r.event === 'string';
33
+
34
+ // num — a number, or null for "not a number I can use".
35
+ //
36
+ // THE GUARD ON null IS LOAD-BEARING, and it is not defensive noise:
37
+ // `Number(null)` is 0 and 0 is finite, so the obvious one-liner turns every
38
+ // absent cost into a free worker and every absent duration into an instant one.
39
+ // The same trap cost a bug in modules/autonomy/cadence.js's fingerprint, where an
40
+ // unread claimable count became the literal string "null" for the same reason.
41
+ // Empty string behaves identically. Both are spelled out rather than trusted to
42
+ // Number().
43
+ const num = (v) => {
44
+ if (v === null || v === undefined || v === '') return null;
45
+ const n = Number(v);
46
+ return Number.isFinite(n) ? n : null;
47
+ };
48
+
49
+ function parseAt(row) {
50
+ const t = Date.parse(row && row.at);
51
+ return Number.isFinite(t) ? t : null;
52
+ }
53
+
54
+ // humanDuration — minutes and hours, the way a person says them.
55
+ function humanDuration(s) {
56
+ const n = num(s);
57
+ if (n === null || n < 0) return null;
58
+ if (n < 90) return `${Math.round(n)}s`;
59
+ if (n < 5400) return `${Math.round(n / 60)}m`;
60
+ return `${(n / 3600).toFixed(1)}h`;
61
+ }
62
+
63
+ function usd(n) {
64
+ const v = num(n);
65
+ return v === null ? null : `$${v.toFixed(2)}`;
66
+ }
67
+
68
+ // windowsFromGauge — question five, from the gauge file's own shape.
69
+ //
70
+ // Reports what it can SEE and says so when it cannot. A digest that silently
71
+ // omits a window reads as "nothing to report", and the one morning that matters
72
+ // is the morning the gauge stopped being written.
73
+ // STALENESS IS GAUGE.JS'S RULE, NOT A SECOND ONE. modules/autonomy/gauge.js holds
74
+ // a window UNKNOWN when its `observed_at` is older than staleAfterS (30 minutes
75
+ // by default), and the runner HOLDS on unknown. A digest that applied a laxer
76
+ // test would print "41% used, resets in 2h" about a window the runner had already
77
+ // decided it could not vouch for — two surfaces disagreeing about one file, with
78
+ // the reader believing the one that is wrong. Same threshold, same conclusion.
79
+ // (Grader finding, task 1003906.)
80
+ function windowsFromGauge(gauge, nowEpochS, { staleAfterS = null } = {}) {
81
+ const limit = staleAfterS === null ? require('./gauge.js').DEFAULT_STALE_AFTER_S : staleAfterS;
82
+ const writtenAt = gauge && typeof gauge === 'object' ? num(gauge.written_at) : null;
83
+ const one = (key, label) => {
84
+ const w = gauge && typeof gauge === 'object' ? gauge[key] : null;
85
+ const used = w ? num(w.used_percentage) : null;
86
+ const resets = w ? num(w.resets_at) : null;
87
+ // Per-window observed_at, falling back to the file's written_at — exactly the
88
+ // shape gauge.js reads, because one window can go unreported while the other
89
+ // is carried forward.
90
+ const observedAt = w ? (num(w.observed_at) ?? writtenAt) : null;
91
+ if (used === null) return { key, label, known: false, detail: 'not reported' };
92
+
93
+ const ageS = observedAt !== null && Number.isFinite(nowEpochS) ? nowEpochS - observedAt : null;
94
+ if (ageS !== null && ageS > limit) {
95
+ return {
96
+ key, label, known: false, stale: true, usedPct: Math.round(used), resetsAt: resets, ageS,
97
+ detail: `last read ${Math.round(used)}% used ${humanDuration(ageS)} ago — older than the runner trusts, so it is holding`,
98
+ };
99
+ }
100
+ const ahead = resets !== null && Number.isFinite(nowEpochS) ? resets - nowEpochS : null;
101
+
102
+ // A RESET TIME IN THE PAST MEANS THE READING IS STALE, not that the window is
103
+ // about to turn over. The percentage was true of a window that has since
104
+ // rolled, so printing it as a live figure is worse than printing nothing —
105
+ // "41% used, resets in 0s" invites a decision about headroom that no longer
106
+ // exists. Say the reading is old and let the reader go look.
107
+ if (ahead !== null && ahead <= 0) {
108
+ return {
109
+ key, label, known: false, stale: true, usedPct: Math.round(used), resetsAt: resets, resetsInS: 0,
110
+ detail: `last read ${Math.round(used)}% used, but that window has since reset — the gauge is stale`,
111
+ };
112
+ }
113
+ return {
114
+ key, label, known: true, usedPct: Math.round(used),
115
+ resetsAt: resets, resetsInS: ahead,
116
+ detail: `${Math.round(used)}% used${ahead !== null ? `, resets in ${humanDuration(ahead)}` : ''}`,
117
+ };
118
+ };
119
+ return [one('five_hour', 'Five-hour window'), one('seven_day', 'Seven-day window')];
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // The digest
124
+ // ---------------------------------------------------------------------------
125
+
126
+ // buildDigest — every question answered from the rows, or explicitly unanswered.
127
+ //
128
+ // `rows` is the run log for the period, oldest first. `gauge` is the raw gauge
129
+ // file (the same shape modules/autonomy/gauge.js reads). Unrecognised rows are
130
+ // COUNTED rather than dropped: a digest that silently ignores what it does not
131
+ // understand is how a new failure mode stays invisible for a week.
132
+ function buildDigest(rows = [], { gauge = null, nowEpochS = null, sinceMs = null, untilMs = null, staleAfterS = null } = {}) {
133
+ const all = (Array.isArray(rows) ? rows : []).filter(isRow);
134
+ const worked = all.filter((r) => r.event === 'worked');
135
+
136
+ // 1. WHAT SHIPPED — the ledger's answer.
137
+ const shipped = worked.filter((r) => r.verified === true).map((r) => ({
138
+ taskId: String(r.task_id ?? ''), title: r.title || null, goalId: r.goal_id ?? null,
139
+ durationS: num(r.duration_s), costUsd: num(r.cost_usd),
140
+ }));
141
+
142
+ // 2. WHAT IS ON HOLD, AND WHY. Four different shapes of "did not ship", kept
143
+ // apart because the owner's next action differs for each.
144
+ const ranButDidNotShip = worked.filter((r) => r.verified !== true).map((r) => ({
145
+ taskId: String(r.task_id ?? ''), title: r.title || null,
146
+ shape: r.ledger_shape || 'unknown',
147
+ // THE LEDGER'S REASON FIRST, and the worker's only as a fallback. `row.reason`
148
+ // is verdict.reason — the WORKER's account of itself — and quoting that in the
149
+ // "did not ship" section is the precise inversion this whole goal exists to
150
+ // undo: a worker that stopped dead needing a decision reports success. The
151
+ // ledger's explanation ("the grade passed and the land never finished") is the
152
+ // one a person can act on. Older rows predate ledger_reason, so the fallback
153
+ // stays rather than leaving them blank.
154
+ reason: r.ledger_reason || r.reason || null,
155
+ remedy: r.ledger_remedy || null,
156
+ workerSaid: r.reason || null,
157
+ blockerId: r.blocker_id ?? null,
158
+ parked: !!r.task_parked,
159
+ claimReleased: !!r.claim_released,
160
+ }));
161
+
162
+ const fenced = all.filter((r) => r.event === 'fenced').map((r) => ({
163
+ code: r.code || 'unknown', reason: r.reason || null, taskId: r.task_id ? String(r.task_id) : null,
164
+ }));
165
+
166
+ // An UNWORKABLE QUEUE is not breakage, and the digest must not present it as
167
+ // such: the machine is healthy and there is nothing it may work on.
168
+ const unworkable = [];
169
+ for (const r of all) {
170
+ if (r.event !== 'nothing_claimable') continue;
171
+ for (const sk of (Array.isArray(r.skipped) ? r.skipped : [])) {
172
+ unworkable.push({ taskId: String(sk.id ?? ''), goalId: sk.goal_id ?? null, reasons: sk.reasons || [] });
173
+ }
174
+ }
175
+
176
+ const held = all.filter((r) => r.event === 'hold').map((r) => ({ reason: r.reason || null, sleepUntil: r.sleep_until ?? null }));
177
+
178
+ // 3. WHAT EACH WORKER DECIDED that its spec did not determine — grouped by task,
179
+ // because the SHAPE of this list is the signal. One task with six decisions is a
180
+ // thin spec; six tasks with one each is a normal night.
181
+ const decisions = [];
182
+ for (const r of worked) {
183
+ const list = Array.isArray(r.undetermined_decisions) ? r.undetermined_decisions : [];
184
+ if (!list.length) continue;
185
+ decisions.push({
186
+ taskId: String(r.task_id ?? ''), title: r.title || null, goalId: r.goal_id ?? null,
187
+ shipped: r.verified === true,
188
+ decisions: list.map((d) => String(d)),
189
+ });
190
+ }
191
+ const decisionCount = decisions.reduce((n, d) => n + d.decisions.length, 0);
192
+ // The thinnest spec of the night, named. A list is browsable; a name is
193
+ // actionable, and this one is the sentence the description should have carried.
194
+ const thinnestSpec = decisions.slice().sort((a, b) => b.decisions.length - a.decisions.length)[0] || null;
195
+
196
+ // 4. WHAT IT COST. Nulls are EXCLUDED and counted, never coerced to zero — a
197
+ // worker whose cost was not reported must not make the night look cheaper.
198
+ const costs = worked.map((r) => num(r.cost_usd));
199
+ const knownCosts = costs.filter((c) => c !== null);
200
+ const cost = {
201
+ totalUsd: knownCosts.reduce((a, b) => a + b, 0),
202
+ reported: knownCosts.length,
203
+ unreported: costs.length - knownCosts.length,
204
+ workerRuns: worked.length,
205
+ totalDurationS: worked.reduce((a, r) => a + (num(r.duration_s) || 0), 0),
206
+ };
207
+
208
+ // 5. THE TWO LIMIT WINDOWS.
209
+ const windows = windowsFromGauge(gauge, nowEpochS, { staleAfterS });
210
+
211
+ // How the runner spent the night, so "nothing shipped" can be told apart from
212
+ // "nothing ran".
213
+ const KNOWN = new Set(['worked', 'fenced', 'hold', 'nothing_claimable', 'dry_run',
214
+ 'pick_failed', 'worktree_failed', 'claim_failed', 'verify_halt', 'cadence', 'boot_reconcile']);
215
+ const byEvent = {};
216
+ let unrecognised = 0;
217
+ for (const r of all) {
218
+ byEvent[r.event] = (byEvent[r.event] || 0) + 1;
219
+ if (!KNOWN.has(r.event)) unrecognised += 1;
220
+ }
221
+
222
+ const cadenceRows = all.filter((r) => r.event === 'cadence' && r.mode);
223
+ const endedProbing = cadenceRows.length ? cadenceRows[cadenceRows.length - 1].mode === 'probing' : false;
224
+
225
+ // reduce, not Math.min(...times): `--all` on a long-lived log can hold more
226
+ // timestamps than the engine will accept as call arguments, and a reporting CLI
227
+ // that throws on a big log fails exactly when the log is most worth reading.
228
+ const times = all.map(parseAt).filter((t) => t !== null);
229
+ const minAt = times.length ? times.reduce((a, b) => (b < a ? b : a)) : null;
230
+ const maxAt = times.length ? times.reduce((a, b) => (b > a ? b : a)) : null;
231
+ return {
232
+ period: {
233
+ fromMs: sinceMs ?? minAt,
234
+ toMs: untilMs ?? maxAt,
235
+ rows: all.length,
236
+ },
237
+ shipped,
238
+ onHold: { ranButDidNotShip, fenced, unworkable, held },
239
+ decisions, decisionCount, thinnestSpec,
240
+ cost,
241
+ windows,
242
+ runner: { byEvent, unrecognised, endedProbing, bootReconciles: byEvent.boot_reconcile || 0 },
243
+ };
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Rendering
248
+ // ---------------------------------------------------------------------------
249
+
250
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
251
+
252
+ // renderDigest — the five questions, in the order they are asked, as plain text a
253
+ // person reads over coffee. Deliberately not a table: the widths are unknowable
254
+ // and a wrapped table is worse than prose.
255
+ //
256
+ // EVERY SECTION PRINTS, even when empty. A digest whose quiet sections vanish
257
+ // teaches its reader that absence means "fine", and then the one morning a
258
+ // section is missing because the DATA is missing reads exactly the same.
259
+ function renderDigest(d, { title = 'Autobongos — overnight' } = {}) {
260
+ const L = [];
261
+ const when = d.period.fromMs && d.period.toMs
262
+ ? `${new Date(d.period.fromMs).toISOString().slice(0, 16).replace('T', ' ')} → ${new Date(d.period.toMs).toISOString().slice(11, 16)} UTC`
263
+ : 'no runs recorded';
264
+ L.push(title, '='.repeat(title.length), when, '');
265
+
266
+ // 1
267
+ L.push(`SHIPPED — ${plural(d.shipped.length, 'task', 'tasks')}`);
268
+ if (!d.shipped.length) L.push(' nothing shipped.');
269
+ for (const s of d.shipped) {
270
+ L.push(` ${s.taskId} ${s.title || '(untitled)'}`);
271
+ const bits = [humanDuration(s.durationS), usd(s.costUsd)].filter(Boolean);
272
+ if (bits.length) L.push(` ${bits.join(' · ')}`);
273
+ }
274
+ L.push('');
275
+
276
+ // 2
277
+ const holdTotal = d.onHold.ranButDidNotShip.length + d.onHold.fenced.length + d.onHold.unworkable.length + d.onHold.held.length;
278
+ L.push(`ON HOLD — ${plural(holdTotal, 'thing', 'things')}`);
279
+ if (!holdTotal) L.push(' nothing is waiting on you.');
280
+ for (const r of d.onHold.ranButDidNotShip) {
281
+ L.push(` ${r.taskId} ran but did not ship — ${r.shape}`);
282
+ if (r.reason) L.push(` ${String(r.reason).slice(0, 160)}`);
283
+ if (r.remedy) L.push(` next: ${String(r.remedy).slice(0, 160)}`);
284
+ // Printed only when it ADDS something. A worker that agreed with the ledger
285
+ // tells the reader nothing twice; one that claimed success against a ledger
286
+ // that says otherwise is the single most useful line on the page.
287
+ if (r.workerSaid && r.workerSaid !== r.reason) L.push(` the worker said: ${String(r.workerSaid).slice(0, 160)}`);
288
+ if (r.blockerId) L.push(` blocker ${r.blockerId}${r.parked ? ' (task parked, out of the queue)' : ''}`);
289
+ }
290
+ // Fenced refusals are collapsed by code: twenty identical lines say nothing the
291
+ // first one did not.
292
+ const byCode = {};
293
+ for (const f of d.onHold.fenced) (byCode[f.code] = byCode[f.code] || []).push(f);
294
+ for (const [code, list] of Object.entries(byCode)) {
295
+ L.push(` fence: ${code} ×${list.length}`);
296
+ if (list[0].reason) L.push(` ${String(list[0].reason).slice(0, 160)}`);
297
+ }
298
+ if (d.onHold.unworkable.length) {
299
+ L.push(` queue: ${plural(d.onHold.unworkable.length, 'task', 'tasks')} in the allowlist, every one halted`);
300
+ L.push(' (this is not a runner failure — it has nothing it may work on)');
301
+ for (const u of d.onHold.unworkable.slice(0, 5)) L.push(` ${u.taskId}: ${(u.reasons || []).join(', ') || 'halted'}`);
302
+ }
303
+ if (d.onHold.held.length) L.push(` capacity: held ${plural(d.onHold.held.length, 'time', 'times')} — ${d.onHold.held[0].reason || 'no headroom'}`);
304
+ L.push('');
305
+
306
+ // 3 — the one that compounds.
307
+ L.push(`DECIDED FOR YOU — ${plural(d.decisionCount, 'decision', 'decisions')} across ${plural(d.decisions.length, 'task', 'tasks')}`);
308
+ if (!d.decisionCount) {
309
+ L.push(' no worker had to fill a gap in its spec.');
310
+ } else {
311
+ L.push(' Each line is a sentence the task description should have contained.');
312
+ for (const t of d.decisions) {
313
+ L.push(` ${t.taskId} ${t.title || '(untitled)'}${t.shipped ? '' : ' [did not ship]'}`);
314
+ for (const one of t.decisions) L.push(` · ${one}`);
315
+ }
316
+ if (d.thinnestSpec && d.thinnestSpec.decisions.length > 1) {
317
+ L.push('');
318
+ L.push(` Thinnest spec: ${d.thinnestSpec.taskId} (${d.thinnestSpec.decisions.length} decisions).`);
319
+ L.push(' Tightening that description is what lowers tomorrow’s halts.');
320
+ }
321
+ }
322
+ L.push('');
323
+
324
+ // 4
325
+ L.push('COST');
326
+ L.push(` ${usd(d.cost.totalUsd)} across ${plural(d.cost.workerRuns, 'worker run', 'worker runs')}` +
327
+ (d.cost.unreported ? ` (${d.cost.unreported} did not report a cost)` : ''));
328
+ if (d.cost.totalDurationS) L.push(` ${humanDuration(d.cost.totalDurationS)} of worker time`);
329
+ L.push('');
330
+
331
+ // 5
332
+ L.push('LIMITS');
333
+ for (const w of d.windows) L.push(` ${w.label.padEnd(18)} ${w.detail}`);
334
+ L.push('');
335
+
336
+ // The footer answers "did it run at all", which none of the five questions do.
337
+ const events = Object.entries(d.runner.byEvent).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} ${n}`).join(' · ');
338
+ L.push(`RUNNER ${events || 'no rows'}`);
339
+ if (d.runner.endedProbing) L.push(' Ended the night PROBING — it hit repeated failures and slowed itself down.');
340
+ if (d.runner.unrecognised) L.push(` ${plural(d.runner.unrecognised, 'row', 'rows')} of a kind this digest does not know about.`);
341
+ return L.join('\n');
342
+ }
343
+
344
+ module.exports = { buildDigest, renderDigest, windowsFromGauge, humanDuration };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "key": "hall-ui",
3
3
  "title": "Builders' Hall",
4
- "description": "The authenticated builders' hall served at builders.<apex> (builders.example.com) — the roster, work board, standing/karma, leaderboard, achievements, primer, system maps, settings, dev-box pair page, and the Archon-only watch/harbor/gate pages, all polling /api/bongos/* and the read-only /api/bongos/public/* surface. The largest WEB-SURFACE core-domain module (~13k LOC), carved out of the GDS core in BV1.R85 (ADR 0093 §1; 02-module-map §6). default-on: a Cloud Bongos instance ships its hall like it ships its status page. Registers no port and contributes no routes — it declares a webSurface (host prefix + static dir). Unlike the generic status surface, the hall keeps its dedicated serving block in serve-internal.js (the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection) — the webSurfaces declaration is the discovery + scope-map + window.__MODULES__ record; serve-internal serves it from this module's public/ dir with that richer machinery. The me/public/leaderboard read endpoints it renders stay SHARED core in routes/{me,public,builders}.js (the status surface + the off-box mirror read them too).",
4
+ "description": "The authenticated builders' hall served at builders.<apex> (builders.example.com) — the roster, work board, standing/karma, leaderboard, achievements, primer, system maps, settings, and the Archon-only watch/gate pages, all polling /api/bongos/* and the read-only /api/bongos/public/* surface. The largest WEB-SURFACE core-domain module (~13k LOC), carved out of the GDS core in BV1.R85 (ADR 0093 §1; 02-module-map §6). default-on: a Cloud Bongos instance ships its hall like it ships its status page. Registers no port and contributes no routes — it declares a webSurface (host prefix + static dir). Unlike the generic status surface, the hall keeps its dedicated serving block in serve-internal.js (the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection) — the webSurfaces declaration is the discovery + scope-map + window.__MODULES__ record; serve-internal serves it from this module's public/ dir with that richer machinery. The me/public/leaderboard read endpoints it renders stay SHARED core in routes/{me,public,builders}.js (the status surface + the off-box mirror read them too).",
5
5
  "coreVersion": "^1.11.0",
6
6
  "default": true,
7
7
  "maintenance": { "status": "core-maintained" },
@@ -408,15 +408,6 @@
408
408
  }
409
409
 
410
410
 
411
- // #777/#783/#807: the dev-box management UI now lives in the shared OTBBoxPanel
412
- // module (public-builders/box-panel.js) so it can be reused verbatim on the
413
- // Settings page. Here it runs in browser-only mode (showSsh:false) — a newcomer
414
- // sees only the online terminal, never raw SSH details. We hold ONE instance;
415
- // it owns its own self-rescheduling poll + terminal-window handle. Lazily
416
- // created so it survives the very first renderWelcome regardless of which
417
- // <script> finished loading first; a no-op shim keeps callers safe if the
418
- // module somehow failed to load.
419
-
420
411
  // ---- onboarding checklist (V3.R75 #294; reshaped #777) ------------------
421
412
  // Renders the "Path into the City" checklist for the CURRENT logged-in
422
413
  // builder. Hidden once graduated (current_stage === 'graduated') so it
@@ -1284,8 +1275,6 @@
1284
1275
 
1285
1276
  // #777: setup-config toggle in the welcome panel. Flips which config pane
1286
1277
  // (Desktop app vs Claude Online) is shown — pure client-side, no round-trip.
1287
- // The dev-box buttons + credential-copy inside the online pane are handled by
1288
- // the OTBBoxPanel module's own scoped listener (#807), not here.
1289
1278
  $('#welcome-body')?.addEventListener('click', (e) => {
1290
1279
  const tab = e.target.closest('.setup__tab[data-config]');
1291
1280
  if (!tab) return;
@@ -1305,7 +1294,6 @@
1305
1294
  if (!scroll) return;
1306
1295
  const minimized = scroll.classList.toggle('scroll--welcome--minimized');
1307
1296
  if (btn) btn.textContent = minimized ? '+' : '−';
1308
- if (minimized) onboardingBox().stop(); else onboardingBox().refresh();
1309
1297
  });
1310
1298
 
1311
1299
  // V3.R25 (#245): delegated click handler for the peer-acclaim vote buttons.
@@ -1425,7 +1413,7 @@
1425
1413
  // reads what this page holds NOW rather than what it held at boot. Everything
1426
1414
  // else is stable for the life of the page. This call sits AFTER every
1427
1415
  // declaration it passes — moving it earlier is a temporal-dead-zone crash.
1428
- const { SETUP_CONFIG_KEY, acclaimEvidenceHtml, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey } = window.OTBHallRender({
1416
+ const { SETUP_CONFIG_KEY, acclaimEvidenceHtml, analyticsState, lbState, mountHallWidgets, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey } = window.OTBHallRender({
1429
1417
  API, RANK_ORDER, api, brandWorld, curLabel, emptyRow, fmtDate, mins, normalizeRank, pct, postJSON, rankLabel, rankWeight, signClasses, toast, usd,
1430
1418
  get instanceRepo() { return instanceRepo; },
1431
1419
  get lastClaimableCount() { return lastClaimableCount; },
@@ -6,8 +6,8 @@
6
6
  // NOT a consumer: modules/status-ui/public/ (the off-box status mirror) stays
7
7
  // self-contained by design — do not wire this there. Network helpers
8
8
  // (getJSON/postJSON/patchJSON) and renderRankBadge are deliberately NOT here:
9
- // the former carry auth-redirect drift that differs per page (and box-panel
10
- // catches its redirect by message), the latter is single-use and coupled to
9
+ // the former carry auth-redirect drift that differs per page, the latter is
10
+ // single-use and coupled to
11
11
  // the rank-ladder constant (a D1 concern). See
12
12
  // docs/audits/2026-06-14-repo-cleanup-audit.md §2A/§2B (frontend-utils).
13
13
  //
@@ -212,8 +212,8 @@
212
212
  // Module→core settings-panel contribution seam (BV1.R42, ADR 0083) — the
213
213
  // browser twin of the server's 'profile.fields' seam (src/module-seams.js). A
214
214
  // module that has left core registers a Settings panel from its own client
215
- // script (loaded before settings.js, the box-panel.js pattern); settings.js
216
- // then renders every registered panel instead of hardcoding box/discord/art.
215
+ // script (loaded before settings.js); settings.js then renders every
216
+ // registered panel instead of hardcoding discord/art.
217
217
  // Empty until a module registers → the page renders exactly what it does today.
218
218
  // A panel: { id, title?, order?, mount(el) } — mount() is handed a container
219
219
  // element to render into. order sorts ascending (default 0).
@@ -854,9 +854,7 @@
854
854
  <div class="setup__pane setup__pane--online">
855
855
  <ol class="setup__steps">
856
856
  <li><strong>Create your <a href="https://claude.com" target="_blank" rel="noopener noreferrer">Claude</a> account.</strong></li>
857
- <li><strong>Open your online environment.</strong> This is the environment you will build in — no SSH, nothing to install.
858
- <div class="setup__box" data-box-panel></div>
859
- </li>
857
+ <li><strong>Open your online environment.</strong> This is the environment you will build in — no SSH, nothing to install.</li>
860
858
  <li><strong>Sign in when Claude prompts you.</strong> Claude starts automatically when you open the terminal.
861
859
  <span class="setup__hint">To paste the sign-in code: <strong>Ctrl+Shift+V</strong> or right-click — plain Ctrl+V will not paste here. Claude takes over the terminal with its own interface once you're signed in. This guide stays open in its own tab. If Claude didn't start automatically, type <code>claude</code> and press Enter.</span></li>
862
860
  <li><strong>Then drive it however you like:</strong>
@@ -912,20 +910,6 @@
912
910
  // Remember the stage we rendered for, so the dismiss handler can record it.
913
911
  scroll.dataset.stageIdx = String(currentIdx);
914
912
  scroll.hidden = false;
915
- // #783/#807: mount the shared dev-box panel into the online pane and paint
916
- // it from live server state. Browser-only onboarding hides SSH details
917
- // (showSsh:false) — a newcomer sees only the online terminal. The panel runs
918
- // its own poll while anything is in flight (survives tab flips / re-renders /
919
- // reloads / background approvals); mount re-binds to the freshly-rendered DOM.
920
- const boxMount = body.querySelector('[data-box-panel]');
921
- if (boxMount) onboardingBox().mount(boxMount);
922
- }
923
- let _onboardingBox = null;
924
- function onboardingBox() {
925
- if (!_onboardingBox && window.OTBBoxPanel) {
926
- _onboardingBox = window.OTBBoxPanel.create({ api: API, showSsh: false, toast });
927
- }
928
- return _onboardingBox || { mount() {}, refresh() {}, stop() {} };
929
913
  }
930
914
  function renderOnboarding(onboarding, discord) {
931
915
  const scroll = $('#onboarding-scroll');
@@ -1351,6 +1335,6 @@
1351
1335
  return idx === -1 ? null : { place: idx + 1, total: ranked.length };
1352
1336
  }
1353
1337
 
1354
- return { SETUP_CONFIG_KEY, acclaimEvidenceHtml, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey };
1338
+ return { SETUP_CONFIG_KEY, acclaimEvidenceHtml, analyticsState, lbState, mountHallWidgets, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey };
1355
1339
  };
1356
1340
  })();
@@ -105,9 +105,6 @@
105
105
  <script src="/builders/shell.js?v=2026-07-02-shell1" defer></script>
106
106
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
107
107
  <script src="/builders/palette.js" defer></script>
108
- <!-- #807: shared dev-box management panel — loaded BEFORE builders.js so the
109
- OTBBoxPanel global is defined when the welcome panel mounts it. -->
110
- <script src="/builders/box-panel.js?v=2026-06-14-domutils" defer></script>
111
108
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
112
109
  Loaded BEFORE hall-widgets.js, whose skeletons call OTBKit.pagerBarHtml(). -->
113
110
  <script src="/builders/hall-visual.js?v=2026-09-16-visual1" defer></script>
@@ -48,9 +48,8 @@
48
48
  .settings-card__hint { font-size: 13px; color: var(--ink-faint); }
49
49
  .panel-status,
50
50
  .settings-status,
51
- .pair-status { margin: var(--sp-xs) 0 0; min-height: 1.2em; font-size: 13.5px; color: var(--ink-soft); }
52
- .panel-status--ok, .settings-card__ok, .pair-status--ok { color: var(--ok); }
53
- .panel-status--danger, .pair-status--err { color: var(--danger); }
51
+ .panel-status--ok, .settings-card__ok { color: var(--ok); }
52
+ .panel-status--danger { color: var(--danger); }
54
53
  .panel-status:empty, .settings-status:empty { margin-top: 0; min-height: 0; }
55
54
  /* a prose link in any of the four voices is a control and owes the 24px floor */
56
55
  .panel-lede a, .panel-note a, .panel-hint a, .panel-status a, .settings-card__hint a, .section-note a {
@@ -114,7 +113,7 @@
114
113
  accent door — the panel's one primary act — and takes --accent-on, the dark
115
114
  ink for the salmon; white on it fails AA and DESIGN.md bans it. */
116
115
  .pbtn,
117
- .settings-btn, .sessions-row__end, .event-sound-preview, .pair-approve {
116
+ .settings-btn, .sessions-row__end, .event-sound-preview {
118
117
  display: inline-flex;
119
118
  align-items: center;
120
119
  justify-content: center;
@@ -135,14 +134,14 @@
135
134
  transition: background 200ms cubic-bezier(.2,.7,.3,1), border-color 200ms cubic-bezier(.2,.7,.3,1), color 200ms cubic-bezier(.2,.7,.3,1);
136
135
  }
137
136
  .pbtn:hover:not(:disabled), .settings-btn:hover:not(:disabled), .sessions-row__end:hover:not(:disabled),
138
- .event-sound-preview:hover:not(:disabled), .pair-approve:hover:not(:disabled) { background: var(--glass-strong); color: var(--ink); text-decoration: none; }
139
- .pbtn:disabled, .settings-btn:disabled, .sessions-row__end:disabled, .pair-approve:disabled,
137
+ .event-sound-preview:hover:not(:disabled) { background: var(--glass-strong); color: var(--ink); text-decoration: none; }
138
+ .pbtn:disabled, .settings-btn:disabled, .sessions-row__end:disabled,
140
139
  .pbtn[aria-disabled="true"], .settings-btn[aria-disabled="true"] { color: var(--ink-faint); background: var(--glass); cursor: default; }
141
140
  .pbtn--quiet, .settings-btn--quiet, .sessions-row__end, .event-sound-preview { background: transparent; }
142
141
  .pbtn--save,
143
- .craft-save, .pair-approve { background: var(--accent); border-color: var(--accent); color: var(--accent-on); font-weight: 500; }
144
- .pbtn--save:hover:not(:disabled), .craft-save:hover:not(:disabled), .pair-approve:hover:not(:disabled) { background: var(--accent-lift); border-color: var(--accent-lift); color: var(--accent-on); }
145
- .craft-save:disabled, .pbtn--save:disabled, .pair-approve:disabled { background: var(--glass); border-color: var(--glass-edge); color: var(--ink-faint); }
142
+ .craft-save { background: var(--accent); border-color: var(--accent); color: var(--accent-on); font-weight: 500; }
143
+ .pbtn--save:hover:not(:disabled), .craft-save:hover:not(:disabled) { background: var(--accent-lift); border-color: var(--accent-lift); color: var(--accent-on); }
144
+ .craft-save:disabled, .pbtn--save:disabled { background: var(--glass); border-color: var(--glass-edge); color: var(--ink-faint); }
146
145
  /* the shell's `.craft-save` was a bordered box; it is the family's save button
147
146
  now, so it carries the pill's own box model too */
148
147
  button.craft-save { display: inline-flex; align-items: center; justify-content: center; min-height: 38px; min-width: 38px; padding: 0 15px; border-radius: var(--radius-pill); font: inherit; font-size: 14px; cursor: pointer; }