@bongos/core 1.19.1041 → 1.19.1043

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 (87) hide show
  1. package/.bongos-core.json +127 -87
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +6 -0
  4. package/clients/bongos-client/index.cjs +6 -0
  5. package/clients/bongos-client/index.d.ts +8 -1
  6. package/clients/bongos-client/index.mjs +6 -0
  7. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +14 -0
  8. package/docs/adr/0339-a-projects-owner-upgrades-it-and-a-hosted-project-hands-off-to-the-hub-door.md +1 -0
  9. package/docs/adr/0340-every-hall-takes-its-update-from-settings-and-deploy-is-the-platform-halls-page.md +64 -0
  10. package/docs/adr/README.md +1 -0
  11. package/docs/api/openapi.json +109 -7
  12. package/docs/api-reference.md +9 -2
  13. package/docs/copy-inventory.md +79 -59
  14. package/docs/copy-registry.json +248 -68
  15. package/docs/file-map.md +1 -1
  16. package/docs/module-api-changelog.md +12 -0
  17. package/docs/modules-contract.md +4 -1
  18. package/modules/hall-ui/public/atlas.html +1 -1
  19. package/modules/hall-ui/public/blockers.html +1 -1
  20. package/modules/hall-ui/public/board-room.html +1 -1
  21. package/modules/hall-ui/public/collab.html +1 -1
  22. package/modules/hall-ui/public/copy-desk.html +1 -1
  23. package/modules/hall-ui/public/deploy.html +1 -1
  24. package/modules/hall-ui/public/diagrams.html +1 -1
  25. package/modules/hall-ui/public/drachmae.html +1 -1
  26. package/modules/hall-ui/public/fleet.html +1 -1
  27. package/modules/hall-ui/public/gate.html +1 -1
  28. package/modules/hall-ui/public/goals.html +1 -1
  29. package/modules/hall-ui/public/government.html +1 -1
  30. package/modules/hall-ui/public/idea.html +1 -1
  31. package/modules/hall-ui/public/ideas.html +1 -1
  32. package/modules/hall-ui/public/index.html +1 -1
  33. package/modules/hall-ui/public/modules.html +1 -1
  34. package/modules/hall-ui/public/palette.js +1 -0
  35. package/modules/hall-ui/public/people.html +1 -1
  36. package/modules/hall-ui/public/primer.html +1 -1
  37. package/modules/hall-ui/public/profile.html +1 -1
  38. package/modules/hall-ui/public/project-settings.html +1 -1
  39. package/modules/hall-ui/public/ranks.html +1 -1
  40. package/modules/hall-ui/public/roadmap.html +1 -1
  41. package/modules/hall-ui/public/roster.html +1 -1
  42. package/modules/hall-ui/public/sessions.html +1 -1
  43. package/modules/hall-ui/public/settings-software-update.js +194 -0
  44. package/modules/hall-ui/public/settings.css +39 -0
  45. package/modules/hall-ui/public/settings.html +24 -2
  46. package/modules/hall-ui/public/settings.js +5 -0
  47. package/modules/hall-ui/public/settings.states.json +2 -1
  48. package/modules/hall-ui/public/shell.js +25 -39
  49. package/modules/hall-ui/public/studio.html +1 -1
  50. package/modules/hall-ui/public/style.css +4 -0
  51. package/modules/hall-ui/public/task.css +6 -0
  52. package/modules/hall-ui/public/task.html +2 -2
  53. package/modules/hall-ui/public/task.js +17 -0
  54. package/modules/hall-ui/public/watch.html +1 -1
  55. package/modules/hall-ui/public/work.html +1 -1
  56. package/modules/hall-ui/records/panel-family.md +9 -0
  57. package/modules/hall-ui/records/record-family.md +2 -0
  58. package/modules/npm-release/module.json +8 -2
  59. package/modules/npm-release/public/task-widget.js +79 -0
  60. package/modules/npm-release/public/work.js +8 -3
  61. package/modules/npm-release/routes/task-where.js +48 -0
  62. package/modules/npm-release/work.js +78 -11
  63. package/package-lock.json +2 -2
  64. package/package.json +1 -1
  65. package/release-notes.json +16 -0
  66. package/scripts/gds/exec-path-guard.js +1 -1
  67. package/scripts/gds/fitness.js +7 -0
  68. package/scripts/gds/main-audit.js +204 -7
  69. package/scripts/gds/seam-wiring-guard.js +4 -2
  70. package/scripts/hall-preview/server.js +14 -1
  71. package/{modules/npm-release/registry.js → src/bongos/package-registry.js} +14 -7
  72. package/src/bongos/routes/core-update.js +65 -3
  73. package/src/bongos/serve-internal.js +35 -4
  74. package/src/bongos/software-update.js +127 -0
  75. package/src/module-api.js +10 -1
  76. package/src/module-loader/loader.js +13 -3
  77. package/tests/core_upgrade_owner_door.mjs +27 -18
  78. package/tests/hall_record_world.mjs +23 -1
  79. package/tests/helpers.mjs +92 -0
  80. package/tests/main_audit_attestation.mjs +238 -0
  81. package/tests/module_api.mjs +1 -0
  82. package/tests/module_loader.mjs +27 -0
  83. package/tests/nav_permission_atoms.mjs +1 -1
  84. package/tests/npm_release_page.mjs +11 -1
  85. package/tests/npm_release_where.mjs +323 -0
  86. package/tests/npm_release_work.mjs +9 -94
  87. package/tests/software_update.mjs +449 -0
package/docs/file-map.md CHANGED
@@ -137,7 +137,7 @@
137
137
  │ ├── 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, records/ (one file per page family — what each family's redesign settled; CLAUDE.md points at them, and a new record is a new file there so concurrent hall tasks stop colliding on one insertion point, task 1003499), 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`), `thinking.css` THE SKY's one sheet (task 1004231 / sky part 3 — `thinking.html` at `/thinking` is the prototype the owner adopted as the design of record, `docs/design/mocks/sky/`, built over `GET /sky` + `GET /sky/mine`; its script is the seven `sky-*.js` files on one shared `window.OTBSky`; it replaced THE ROOM of task 1004098, which survives as the list drawer, and the old `/sky` page, whose URL 301s here; `idea-objects.js` beside it is the shared five-object vocabulary).
138
138
  │ ├── 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).
139
139
  │ ├── 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.
140
- │ ├── npm-release/ ← the deploy page for a project that publishes an npm package from GitHub (task 1002622, default-OFF — on only in the Bongos hall, which publishes @bongos/core). "Where the work is": every task finished in the last 14 days, placed in the stage it has actually reached (not merged · merged, no version yet · published, not running here · running here, not released · released · in no version), plus the version history — each version's tasks. The version↔task join reads the release-notes.json the package itself carries (scripts/gds/release-notes.js writes it at pack time), streamed out of the NEWEST version's npm tarball in-process (no child process, no sync call) when the running core's own copy stops short. module.json, work.js (the reading + the stage placement), registry.js (packument + tarball stream + a small tar reader, cached), routes/work.js (GET /npm-release/work, gated core.pin.move like the page), public/work.{js,css} (the two sections /deploy lends it: #nr-work, #nr-versions). Tests sit at the repo root — tests/npm_release_{work,page}.mjs — because a default-off module's own tests/ is skipped by the unit runner.
140
+ │ ├── npm-release/ ← the deploy page for a project that publishes an npm package from GitHub (task 1002622, default-OFF — on only in the Bongos hall, which publishes @bongos/core). "Where the work is": every task finished in the last 14 days, placed in the stage it has actually reached (not merged · merged, no version yet · published, not running here · running here, not released · released · in no version), plus the version history — each version's tasks. The version↔task join reads the release-notes.json the package itself carries (scripts/gds/release-notes.js writes it at pack time), streamed out of the NEWEST version's npm tarball in-process (no child process, no sync call) when the running core's own copy stops short. module.json, work.js (the reading + the stage placement — the packument, tarball stream and tar reader it uses are the core's src/bongos/package-registry.js, reached through the doorway's readPackageRegistry since task 1004296), routes/work.js (GET /npm-release/work, gated core.pin.move like the page), public/work.{js,css} (the two sections /deploy lends it: #nr-work, #nr-versions). Task 1004301 added the same line on a task's own page: routes/task-where.js (GET /npm-release/task/:id, any signed-in builder — it shows nothing not already public) + public/task-widget.js, injected into task.html by the loader's uiSections seam (hallWidgetScripts file task-widget.js) to fill #task-contrib on otb:task-shown. Tests sit at the repo root — tests/npm_release_{work,page}.mjs — because a default-off module's own tests/ is skipped by the unit runner.
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).
142
142
  │ ├── character-anim/ ← consistent-character generative animation (ADR 0088) — graded painterly frame sets + manifest, the otb-character-review skill. CLAUDE.md, module.json.
143
143
  │ ├── provisioning/ ← stands up + operates a Cloud Bongos instance's own infra (server/DB/domain/DNS/TLS) via the API (ADR 0111); web-tier request+state only, no cloud tokens — a separate control-plane runner drains the intents. Distinct from the `hosting` module (ADR 0092), which brokers compute workloads. CLAUDE.md, module.json.
@@ -2561,5 +2561,17 @@ is load-bearing: the script throws rather than guess if it is missing, and
2561
2561
  landed since 1.19.1039 with no explicit bump. run 36251192410. (task 1002620)
2562
2562
  1.19.1041 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2563
2563
  landed since 1.19.1040 with no explicit bump. run 36253865251. (task 1002620)
2564
+ (next carrier) — readPackageRegistry re-exported (task 1004296, goal 1000090).
2565
+ What the npm registry says about a package — its versions, their publish
2566
+ dates, the `latest` label, and the release notes the newest version was
2567
+ packed with — moved from a module into the core (src/bongos/
2568
+ package-registry.js), because Settings → Software update needs it in
2569
+ every hall. A module that publishes a package reads the same answer and
2570
+ the same process-local cache through here. Additive — a MINOR by the
2571
+ 1.19.0 precedent, riding the next CI patch as joinabilityMode did.
2572
+ 1.19.1042 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2573
+ landed since 1.19.1041 with no explicit bump. run 36254852933. (task 1002620)
2574
+ 1.19.1043 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2575
+ landed since 1.19.1042 with no explicit bump. run 36258640780. (task 1002620)
2564
2576
  ---------------------------------------------------------------------------
2565
2577
  ```
@@ -98,7 +98,10 @@ Every module must have a `module.json` at its root (`modules/<key>/module.json`)
98
98
  "pollers": ["box-lifecycle"], // background pollers (start/stop lifecycle)
99
99
  "disciplines": ["artist"], // builder disciplines this module adds
100
100
  "skills": ["otb-tile-generate"], // slash-command skills it ships
101
- "uiSections": ["boxes"], // hall/status UI sections gated by window.__MODULES__
101
+ "uiSections": ["boxes"], // hall/status UI sections gated by window.__MODULES__. Ship public/hall-widget.js
102
+ // and it is injected into the hall index; ship public/task-widget.js and it
103
+ // is injected into a task's page before task.js, to fill #task-contrib on
104
+ // `otb:task-shown` (task 1004301). The page itself names no module.
102
105
  "webSurfaces": [{ "host": "status.", "dir": "public" }], // static web surfaces: a Host PREFIX (BV1.R84)…
103
106
  // …or { "apexRoot": true, "dir": "public" } for the
104
107
  // apex root (ADR 0218). One or the other, never both.
@@ -91,7 +91,7 @@
91
91
 
92
92
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
93
93
 
94
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
94
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
95
95
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
96
96
  <script src="/builders/palette.js" defer></script>
97
97
  <script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
@@ -50,7 +50,7 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
53
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
54
54
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
55
55
  <script src="/builders/palette.js" defer></script>
56
56
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -60,7 +60,7 @@
60
60
 
61
61
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
62
62
 
63
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
63
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
64
64
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
65
65
  <script src="/builders/palette.js" defer></script>
66
66
  <script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
@@ -148,7 +148,7 @@
148
148
 
149
149
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
150
150
 
151
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
151
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
152
152
  <script src="/builders/palette.js" defer></script>
153
153
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
154
154
  <script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
@@ -177,7 +177,7 @@
177
177
 
178
178
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
179
179
 
180
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
180
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
181
181
  <script src="/builders/palette.js" defer></script>
182
182
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
183
183
  <script src="/builders/hall-kit.js"></script>
@@ -69,7 +69,7 @@
69
69
 
70
70
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
71
71
 
72
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
72
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
73
73
  <script src="/builders/palette.js" defer></script>
74
74
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
75
75
  <script src="/builders/hall-kit.js"></script>
@@ -97,7 +97,7 @@
97
97
 
98
98
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
99
99
  time so the chrome exists before the deferred page scripts execute. -->
100
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
100
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
101
101
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
102
102
  <script src="/builders/palette.js" defer></script>
103
103
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
@@ -98,7 +98,7 @@
98
98
 
99
99
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
100
100
 
101
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
101
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
102
102
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
103
103
  <script src="/builders/palette.js" defer></script>
104
104
  <script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
@@ -54,7 +54,7 @@
54
54
 
55
55
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
56
56
 
57
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
57
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
58
58
  <script src="/builders/palette.js" defer></script>
59
59
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
60
60
  <script src="/builders/hall-kit.js"></script>
@@ -76,7 +76,7 @@
76
76
  <div id="gate-modal-root"></div>
77
77
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
78
78
 
79
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
79
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
80
80
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
81
81
  <script src="/builders/palette.js" defer></script>
82
82
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -235,7 +235,7 @@
235
235
 
236
236
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
237
237
 
238
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
238
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
239
239
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
240
240
  <script src="/builders/palette.js" defer></script>
241
241
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -134,7 +134,7 @@
134
134
 
135
135
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
136
136
 
137
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
137
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
138
138
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
139
139
  <script src="/builders/palette.js" defer></script>
140
140
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -52,7 +52,7 @@
52
52
 
53
53
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
54
54
 
55
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
55
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
56
56
  <script src="/builders/palette.js" defer></script>
57
57
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
58
58
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
@@ -225,7 +225,7 @@
225
225
 
226
226
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
227
227
 
228
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
228
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
229
229
  <script src="/builders/palette.js" defer></script>
230
230
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
231
231
  <!-- task 1003069 (R08) — the quick-filing form's routed result links a task
@@ -102,7 +102,7 @@
102
102
  <script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
103
103
  <!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
104
104
  BEFORE builders.js so the chrome ids exist when events are wired. -->
105
- <script src="/builders/shell.js?v=2026-09-26-own1" defer></script>
105
+ <script src="/builders/shell.js?v=2026-09-26-swupdate" 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
108
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -80,7 +80,7 @@
80
80
 
81
81
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
82
82
 
83
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
83
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
84
84
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
85
85
  <script src="/builders/palette.js" defer></script>
86
86
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
@@ -62,6 +62,7 @@
62
62
  { label: 'Extensions', href: '/settings#extensions' },
63
63
  { label: 'Preferences', href: '/settings#preferences' },
64
64
  { label: 'Sound', href: '/settings#sound' },
65
+ { label: 'Software update', href: '/settings#update' },
65
66
  ] },
66
67
  ];
67
68
 
@@ -66,7 +66,7 @@
66
66
 
67
67
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
68
68
  chrome exists before the page script runs. -->
69
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
69
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
70
70
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
71
71
  <script src="/builders/palette.js" defer></script>
72
72
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -115,7 +115,7 @@
115
115
 
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
118
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
119
119
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
120
120
  <script src="/builders/palette.js" defer></script>
121
121
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -187,7 +187,7 @@
187
187
 
188
188
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
189
189
  chrome exists before the page script runs. -->
190
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
190
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
191
191
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
192
192
  <script src="/builders/palette.js" defer></script>
193
193
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -58,7 +58,7 @@
58
58
 
59
59
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
60
60
 
61
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
61
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
62
62
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
63
63
  <script src="/builders/palette.js" defer></script>
64
64
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -68,7 +68,7 @@
68
68
 
69
69
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
70
70
  chrome exists before the page script runs. -->
71
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
71
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
72
72
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
73
73
  <script src="/builders/palette.js" defer></script>
74
74
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -41,7 +41,7 @@
41
41
 
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
44
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
45
45
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
46
46
  <script src="/builders/palette.js" defer></script>
47
47
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -50,7 +50,7 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
53
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
54
54
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
55
55
  <script src="/builders/palette.js" defer></script>
56
56
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -99,7 +99,7 @@
99
99
 
100
100
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
101
101
  chrome exists before the page script runs. -->
102
- <script src="/builders/shell.js?v=2026-09-26-own1"></script>
102
+ <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
103
103
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
104
104
  <script src="/builders/palette.js" defer></script>
105
105
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -0,0 +1,194 @@
1
+ // modules/hall-ui/public/settings-software-update.js — Settings → Software update
2
+ // (task 1004296).
3
+ //
4
+ // The owner, 2026-09-25: "each builders hall settings has a version description where they
5
+ // can pull the latest release. Just like iOS does." So this panel says three things and
6
+ // offers one: the version this project runs; either "up to date" or the newest release its
7
+ // update channel lets it take; what each version in between brings — and, for the people
8
+ // the upgrade door lets through, an Update button.
9
+ //
10
+ // THE BUTTON LEADS TO THE DOOR; IT IS NOT THE DOOR. Taking an update is the deploy door's
11
+ // two steps (see exactly what moves, then confirm that version) with its health check and
12
+ // auto-rollback (ADR 0293), and on a hosted project that door is on the hub (ADR 0339 D3).
13
+ // A second copy of that flow here would be a second place for its safety rules to drift, so
14
+ // the server names where this project's door is (`door`) and the button goes there.
15
+ //
16
+ // THE PAGE KNOWS NOTHING (deploy.js's rule, kept). Every version here is the server's reading
17
+ // with the time it was taken. "Up to date" is drawn only from an ok reading with nothing
18
+ // offered; a reading that failed says so, and release notes that could not be read are
19
+ // reported as unread, never drawn as a version with no changes.
20
+ //
21
+ // READ WHEN ITS GROUP OPENS, not with the page: an update's notes come out of the newer
22
+ // package's download (src/bongos/package-registry.js), and most visits to Settings are for
23
+ // something else. settings.js announces each group switch as a `settings:group` event.
24
+ //
25
+ // Its own file, like the sessions and interaction panels: settings.js is against the
26
+ // 1500-line ratchet, and this panel shares nothing with it but the toolkit globals.
27
+
28
+ (() => {
29
+ 'use strict';
30
+
31
+ const API = '/api/bongos';
32
+ const GROUP = 'update';
33
+ const { escapeHtml } = window.OTB;
34
+ const byId = (id) => document.getElementById(id);
35
+ const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
36
+
37
+ // Versions drawn open before "Show every version": enough to read the recent work
38
+ // without a project that is a hundred versions behind burying the button.
39
+ const VERSIONS_OPEN = 5;
40
+
41
+ let asked = false;
42
+
43
+ function ago(iso) {
44
+ const then = iso ? new Date(iso).getTime() : NaN;
45
+ if (!Number.isFinite(then)) return null;
46
+ const mins = Math.max(0, Math.round((Date.now() - then) / 60000));
47
+ if (mins < 1) return 'just now';
48
+ if (mins < 60) return `${mins} minute${mins === 1 ? '' : 's'} ago`;
49
+ const hours = Math.round(mins / 60);
50
+ if (hours < 48) return `${hours} hour${hours === 1 ? '' : 's'} ago`;
51
+ const days = Math.round(hours / 24);
52
+ return `${days} day${days === 1 ? '' : 's'} ago`;
53
+ }
54
+
55
+ const mono = (v) => `<span class="su-ver">${escapeHtml(String(v))}</span>`;
56
+ // Voice rule V4 (docs/voice.md): a dash is banned in QUOTED data — and a release-note line
57
+ // is a task's ship summary, handed over by the ledger — so it becomes a comma at render,
58
+ // the way goal-inbox.js and the public feed's cleanTitle() do. Never by editing the data.
59
+ const unDash = (raw) => String(raw == null ? '' : raw).replace(/\s*[—–]\s*/g, ', ').replace(/,\s*,/g, ',').trim();
60
+
61
+ function row(label, desc, ctl) {
62
+ return `<div class="prow">
63
+ <div class="prow__main"><span class="prow__label">${label}</span>${desc ? `<span class="prow__desc">${desc}</span>` : ''}</div>
64
+ ${ctl ? `<div class="prow__ctl">${ctl}</div>` : ''}
65
+ </div>`;
66
+ }
67
+
68
+ // Where the Update button goes, and what it says it will do. Only a plain same-origin path
69
+ // or an http(s) URL is ever put in an href; anything else draws no button.
70
+ function doorHtml(data, offered) {
71
+ const door = data.door || {};
72
+ if (!data.can_update) {
73
+ return '<p class="panel-hint su-door">Only the people allowed to upgrade this project see an Update button here.</p>';
74
+ }
75
+ if (door.kind === 'cli') {
76
+ return '<p class="panel-hint su-door">This project is upgraded on the server that runs it: <code>bongos upgrade</code> there takes this version, with the same health check and rollback.</p>';
77
+ }
78
+ const url = String(door.url || '');
79
+ const sameOrigin = /^\/[a-z0-9/_-]*$/i.test(url);
80
+ if (!sameOrigin && !/^https?:\/\/[^\s"'<>]+$/i.test(url)) return '';
81
+ let where = '';
82
+ if (!sameOrigin) {
83
+ try { where = ` on ${new URL(url).host}`; } catch (e) { return ''; }
84
+ }
85
+ return `<div class="row-end su-door">
86
+ <a class="pbtn pbtn--save" id="su-update" href="${escapeHtml(url)}">Update to ${escapeHtml(offered)}${sameOrigin ? '' : ' ↗'}</a>
87
+ <span class="panel-hint">Opens this project's upgrade${escapeHtml(where)}: you see what would move, confirm it, and it installs, checks itself, and rolls back if the project does not come back healthy.</span>
88
+ </div>`;
89
+ }
90
+
91
+ // One entry: its text, and — only in the hall whose ledger the ids belong to, which is
92
+ // the one that runs the door itself — a link to that task.
93
+ function entryHtml(e, linkTasks) {
94
+ const text = escapeHtml(unDash(e.text));
95
+ return linkTasks && e.id ? `<li><a href="/task/${escapeHtml(e.id)}">${text}</a></li>` : `<li>${text}</li>`;
96
+ }
97
+
98
+ function versionHtml(v, linkTasks, notesRead) {
99
+ const when = ago(v.published_at);
100
+ let body;
101
+ if (!notesRead) body = '<p class="prow__desc">Its notes could not be read.</p>';
102
+ else if (v.entries === null) body = '<p class="prow__desc">No notes were packed for this version.</p>';
103
+ else if (!v.entries.length) body = '<p class="prow__desc">No changes are listed for this version.</p>';
104
+ else body = `<ul class="su-entries">${v.entries.map((e) => entryHtml(e, linkTasks)).join('')}</ul>`;
105
+ return `<li class="su-version">
106
+ <span class="prow__label">${mono(v.version)}${when ? `<span class="prow__tag">published ${escapeHtml(when)}</span>` : ''}</span>
107
+ ${body}
108
+ </li>`;
109
+ }
110
+
111
+ function whatsNewHtml(w, linkTasks) {
112
+ if (!w) return '';
113
+ const n = w.count;
114
+ const head = `<h3 class="su-h" id="su-new-h">What's new in ${n === 1 ? 'this version' : `these ${escapeHtml(String(n))} versions`}</h3>`;
115
+ const warn = w.ok ? '' : `<p class="panel-status panel-status--danger" role="status">The release notes could not be read: ${escapeHtml(w.error || 'unknown error')}. The versions are still listed.</p>`;
116
+ const items = w.versions.map((v, i) => versionHtml(v, linkTasks, w.ok).replace('<li class="su-version">',
117
+ i < VERSIONS_OPEN ? '<li class="su-version">' : '<li class="su-version" hidden>'));
118
+ const hiddenCount = Math.max(0, w.versions.length - VERSIONS_OPEN);
119
+ const more = hiddenCount ? `<button type="button" class="pbtn pbtn--quiet" id="su-more">Show ${escapeHtml(String(hiddenCount))} more version${hiddenCount === 1 ? '' : 's'}</button>` : '';
120
+ const beyond = w.more ? `<p class="panel-hint">…and ${escapeHtml(String(w.more))} older version${w.more === 1 ? '' : 's'}, also in this update.</p>` : '';
121
+ return `${head}${warn}<ol class="su-versions" aria-labelledby="su-new-h">${items.join('')}</ol>${more ? `<div class="row-end">${more}</div>` : ''}${beyond}`;
122
+ }
123
+
124
+ function render(data) {
125
+ const body = byId('su-body');
126
+ const note = byId('su-note');
127
+ if (!body) return;
128
+ const u = (data && data.update) || {};
129
+ const checked = ago(u.checked_at);
130
+ const notes = [];
131
+ if (checked) notes.push(`Checked ${checked}.`);
132
+
133
+ if (u.reading !== 'ok') {
134
+ body.innerHTML = row('This project runs', u.running ? mono(u.running) : 'a version it could not name')
135
+ + `<p class="panel-status panel-status--danger" role="status">Could not check for a newer version: ${escapeHtml(u.error || 'no answer')}.</p>`;
136
+ if (note) note.textContent = notes.join(' ');
137
+ return;
138
+ }
139
+
140
+ let html = row('This project runs', mono(u.running));
141
+ if (!u.offered) {
142
+ html += row('Newest for this project', `${mono(u.running)} — this project is up to date.`, '<span class="su-pill su-pill--ok">Up to date</span>');
143
+ } else {
144
+ const when = ago(u.offered_published_at);
145
+ html += row('Newest for this project', `${mono(u.offered)}${when ? ` — published ${escapeHtml(when)}` : ''}`, '<span class="su-pill">Update ready</span>');
146
+ html += doorHtml(data, u.offered);
147
+ html += whatsNewHtml(u.whats_new, (data.door || {}).kind === 'local');
148
+ }
149
+ // A newer line this project's channel does not take is a fact worth one sentence — it
150
+ // explains why the newest number on the registry is not the one offered.
151
+ if (u.newest && u.newest !== (u.offered || u.running) && String(u.newest).split('.')[1] !== String(u.running).split('.')[1]) {
152
+ notes.push(`${u.newest} is also published; it starts a new line, and this project takes updates within ${String(u.running).split('.').slice(0, 2).join('.')} on its own.`);
153
+ }
154
+ body.innerHTML = html;
155
+ if (note) note.textContent = notes.join(' ');
156
+
157
+ const more = byId('su-more');
158
+ if (more) {
159
+ more.addEventListener('click', () => {
160
+ for (const li of body.querySelectorAll('.su-version[hidden]')) li.hidden = false;
161
+ more.parentNode.remove();
162
+ });
163
+ }
164
+ }
165
+
166
+ async function load() {
167
+ if (asked) return;
168
+ asked = true;
169
+ const body = byId('su-body');
170
+ if (body) body.innerHTML = '<p class="panel-hint">Checking for a newer version…</p>';
171
+ try {
172
+ const res = await api.request('GET', `${API}/software-update`);
173
+ if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
174
+ if (!res.ok || !res.data) throw new Error((res.data && res.data.error && res.data.error.message) || `HTTP ${res.status}`);
175
+ render(res.data);
176
+ } catch (err) {
177
+ // Its own failure, like every section here: this panel says it could not check, and
178
+ // the rest of Settings is untouched. Asking again is allowed.
179
+ asked = false;
180
+ if (body) body.innerHTML = `<p class="panel-status panel-status--danger" role="status">Could not check for updates: ${escapeHtml(err.message || 'unknown error')}.</p>`;
181
+ }
182
+ }
183
+
184
+ function boot() {
185
+ const section = byId('software-update-scroll');
186
+ if (!section) return;
187
+ section.hidden = false;
188
+ const groups = byId('settings-groups');
189
+ if (groups && groups.dataset && groups.dataset.active === GROUP) load();
190
+ document.addEventListener('settings:group', (e) => { if (e && e.detail && e.detail.group === GROUP) load(); });
191
+ }
192
+
193
+ boot();
194
+ })();
@@ -26,6 +26,7 @@
26
26
  .settings-groups[data-active="access"] > [data-group="access"],
27
27
  .settings-groups[data-active="preferences"] > [data-group="preferences"],
28
28
  .settings-groups[data-active="sound"] > [data-group="sound"],
29
+ .settings-groups[data-active="update"] > [data-group="update"],
29
30
  .settings-groups[data-active="extensions"] > [data-group="extensions"] { display: block; }
30
31
  .settings-groups > [data-group][hidden] { display: none !important; }
31
32
  .settings-groups .scroll { margin: 0 0 var(--sp-sm); }
@@ -212,3 +213,41 @@ select.voices__select { min-width: 140px; }
212
213
  font-family: inherit;
213
214
  opacity: 0.92;
214
215
  }
216
+
217
+ /* ---- Software update (task 1004296) -------------------------------------- */
218
+ /* A version is read digit by digit and compared against another, so it takes the mono
219
+ face. The state pill borrows project-settings' shape: a word in its own colour with a
220
+ hairline of the same, so "up to date" and "update ready" differ by more than hue. */
221
+ .su-ver { font-family: var(--font-mono); font-size: 13.5px; color: var(--ink); }
222
+ .su-pill {
223
+ display: inline-block;
224
+ padding: 1px 8px;
225
+ border: var(--line) solid currentColor;
226
+ border-radius: var(--radius-pill);
227
+ font-size: 11.5px;
228
+ font-weight: 600;
229
+ white-space: nowrap;
230
+ color: var(--accent-ink);
231
+ }
232
+ .su-pill--ok { color: var(--ok); }
233
+ .su-door { margin-top: var(--sp-sm); }
234
+ .su-door .panel-hint { flex: 1 1 18rem; min-width: 0; }
235
+ .su-h { margin: var(--sp-md) 0 0; font-size: 14.5px; font-weight: 600; color: var(--ink); }
236
+ .su-versions { list-style: none; margin: var(--sp-xs) 0 0; padding: 0; }
237
+ .su-version { padding: var(--sp-sm) 0; border-top: var(--line) solid var(--rule-soft); }
238
+ .su-version > .prow__label { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px; }
239
+ .su-entries { margin: var(--sp-hair) 0 0; padding-left: 1.15rem; max-width: var(--measure); font-size: 13.5px; color: var(--ink-soft); }
240
+ .su-entries li + li { margin-top: 2px; }
241
+ /* a task title is a link, and a link owes the 24px floor. Block, so a two-line note keeps
242
+ its bullet on its first line; the soft ink with an underline, because on the platform
243
+ every note is a link and a list of accent-coloured sentences reads as a warning. */
244
+ .su-entries a {
245
+ display: block;
246
+ min-height: 24px;
247
+ padding: 2px 0;
248
+ color: var(--ink-soft);
249
+ text-decoration: underline;
250
+ text-decoration-color: color-mix(in srgb, var(--ink-soft) 45%, transparent);
251
+ text-underline-offset: 3px;
252
+ }
253
+ .su-entries a:hover { color: var(--ink); text-decoration-color: currentColor; }