@bongos/core 1.19.1042 → 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.
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 — 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). 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.
@@ -2571,5 +2571,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2571
2571
  1.19.0 precedent, riding the next CI patch as joinabilityMode did.
2572
2572
  1.19.1042 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2573
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)
2574
2576
  ---------------------------------------------------------------------------
2575
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.
@@ -3151,6 +3151,10 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
3151
3151
  min-width: 0;
3152
3152
  max-width: 100%;
3153
3153
  }
3154
+ /* The 24px floor (task 1004301 found it failing): at 13px the base field's padding left a
3155
+ number input 21px tall beside a select that clears it — the task page's "Wait for task"
3156
+ box. Sized to sit level with its select. */
3157
+ .td-field input { min-height: 26px; box-sizing: border-box; }
3154
3158
  .td-form .td-actions, .td-form-hint, .td-form .td-action-note { grid-column: 1 / -1; }
3155
3159
  .td-form .td-actions { margin: 4px 0 0; }
3156
3160
  .td-form-hint { font-size: 12.5px; color: var(--ink-soft); margin: 0; }
@@ -139,3 +139,9 @@
139
139
  .tk-axis:nth-child(3) { border-left: 0; padding-left: 0; }
140
140
  .tk-lrow { grid-template-columns: 1fr; gap: 6px; }
141
141
  }
142
+
143
+ /* The module slot under the rail (task 1004301): a module's task-widget.js may write one
144
+ line of its own here (the npm-release module says which version carries the task).
145
+ Empty unless a module fills it, and then it takes no room at all. */
146
+ .tk-contrib { margin: var(--sp-md) 0 0; }
147
+ .tk-contrib:empty { display: none; }
@@ -69,6 +69,6 @@
69
69
  <script src="/builders/brand-apply.js?v=2026-06-19-r56" defer></script>
70
70
  <script src="/builders/md-reader.js" defer></script>
71
71
  <script src="/builders/hall-visual.js?v=2026-09-16-visual1" defer></script>
72
- <script src="/builders/task.js?v=2026-07-03-taskpage1" defer></script>
72
+ <script src="/builders/task.js?v=2026-09-26-taskwhere" defer></script>
73
73
  </body>
74
74
  </html>
@@ -634,6 +634,11 @@
634
634
  ['Priority', t.priority == null ? null : 'P' + t.priority],
635
635
  ]));
636
636
  parts.push(lifecycleRail(t, creditVal));
637
+ // A slot a MODULE may fill (task 1004301): the module's public/task-widget.js is
638
+ // injected just before this script and writes into it on `otb:task-shown` below. The
639
+ // page itself names no module and draws nothing here — with none enabled it stays
640
+ // empty, and `:empty` in task.css collapses it.
641
+ parts.push('<div class="tk-contrib" id="task-contrib" aria-live="polite"></div>');
637
642
 
638
643
  // Claim history (task 751). The most recent holder leads; since task 1003437
639
644
  // the read also carries the FULL claim ledger (?include=claims), so a task
@@ -806,6 +811,7 @@
806
811
 
807
812
  const body = $('#task-body');
808
813
  if (body) body.innerHTML = parts.join('');
814
+ announceShown(t);
809
815
 
810
816
  const copyBtn = $('#copy-prompt-btn');
811
817
  if (copyBtn) {
@@ -958,6 +964,17 @@
958
964
  // The two honest failures. Both keep the <h1> — the empty and error renders are
959
965
  // renders like any other, and a page whose only heading lives on the happy path
960
966
  // has none in the states a reader is most likely to hit.
967
+ // Tell any module widget which task is on screen (task 1004301). Fired after EVERY
968
+ // render, because render() rebuilds #task-body wholesale and the slot a widget filled a
969
+ // moment ago is gone. The last task shown is also left on window.OTBTaskShown for a
970
+ // widget that loads after the first announcement. Only the id and the status travel:
971
+ // a widget that needs more asks its own module's route, which applies its own gate.
972
+ function announceShown(t) {
973
+ const detail = { id: String(t.id), status: t.status || null };
974
+ window.OTBTaskShown = detail;
975
+ if (typeof CustomEvent === 'function') document.dispatchEvent(new CustomEvent('otb:task-shown', { detail }));
976
+ }
977
+
961
978
  function renderNotFound(id) {
962
979
  const body = $('#task-body');
963
980
  if (body) body.innerHTML = `
@@ -11,3 +11,5 @@
11
11
  **The task record's grade panel, session log and claim ledger (task 1003437).** Two net-new regions the owner approved at the composition round (direction doc §6 decision 4) sit between Shipped and Actions on `task.html`. Four rules carry them: all three ride the ONE task read as opt-in siblings — `GET /tasks/:id?include=last_claim,grade,session_logs,claims`; the two new reads (`getTaskSessionLogs`, `getTaskClaims` in `modules/lifecycle/db-claim-reads.js`) are keyed by `task_id`, newest first, **rank never returned**; absent → **NOTHING** (the ship-visual precedent), except a grade row whose panel was `unavailable`, which is data and says so in one line; and verdicts and outcomes wear the fixed verification vocabulary through `.rec-pill--ok/warn/danger`, **never the accent**. Pinned in `tests/hall_record_world.mjs` (page) and `tests/task_detail_includes.mjs` (route + reads).
12
12
 
13
13
  **The way into and out of a task record (task 1003468, ADR 0229).** Since 1759/1869 a to-do card's BODY toggles selection so a builder can bulk-claim — which is why the owner could not open a card, the only door being a 26px ⤢ glyph nobody found. Now **the title is the door**: `.task__link` on both surfaces that render the card (`work.js`, `goals-render.js`), bought up to the 24px floor with the `.scroll__lede a` recipe; both click handlers let it through; the glyph is gone. **The link carries where it was opened from** — `OTBTaskDetail.href(id)` appends `?from=<path+query+hash>` (a query param so the origin survives a reload and keeps a goal's `#/goal/N`, which a referrer drops) — and `task.js` resolves the exit once (`?from=` → same-origin referrer → the work board), points the crumb **before** the fetch so the way back exists in every state, and refuses anything that is not a path on this host. **Never write a bare `/task/N` link from a list again — call `OTBTaskDetail.href(id)`.** Pinned by §10 of `tests/hall_record_world.mjs` and §7 of `tests/hall_board_world.mjs`.
14
+
15
+ **The task page carries a module slot (task 1004301).** Under the lifecycle rail sits `#task-contrib`, empty and collapsed (`.tk-contrib:empty`) unless a module fills it, and `task.js` announces every render as `otb:task-shown` with `{ id, status }` (also left on `window.OTBTaskShown` for a late widget). The page names no module: the server injects each enabled module's `public/task-widget.js` just before `task.js` (the loader's uiSections seam, `hallWidgetScripts({ file: 'task-widget.js' })`), so both run deferred in order and the listener exists before the first announcement. Render rebuilds `#task-body` wholesale, which is why the announcement is per render, not per load. The first filler is npm-release's line — "In 1.19.1036 · running here · not released to other projects yet", or "Done, but not merged" for a stranded ship. Tests: `tests/hall_record_world.mjs` (slot + announcement), `tests/npm_release_where.mjs` (the widget and the seam).
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "key": "npm-release",
3
3
  "title": "npm release",
4
- "description": "The deploy page for a project that publishes an npm package from its GitHub repo — where every recently shipped task has got to (not merged, merged, in a version, live, released) and each version's list of work, read from the release notes the package itself carries (task 1002622). Built for the Bongos hall, which publishes @bongos/core; off everywhere else.",
4
+ "description": "The deploy page for a project that publishes an npm package from its GitHub repo — where every recently shipped task has got to (not merged, merged, in a version, live, released) and each version's list of work, read from the release notes the package itself carries (task 1002622) — and the same line on a task's own page (task 1004301). Built for the Bongos hall, which publishes @bongos/core; off everywhere else.",
5
5
  "coreVersion": "^1.19.0",
6
6
  "default": false,
7
7
  "author": "Cloud Bongos",
@@ -12,7 +12,13 @@
12
12
  "status": "core-maintained"
13
13
  },
14
14
  "contributes": {
15
- "routes": ["work"]
15
+ "routes": [
16
+ "work",
17
+ "task-where"
18
+ ],
19
+ "uiSections": [
20
+ "task-where"
21
+ ]
16
22
  },
17
23
  "provides": [],
18
24
  "consumes": []
@@ -0,0 +1,79 @@
1
+ // modules/npm-release/public/task-widget.js — the line a task's own page shows about where
2
+ // its code is (task 1004301): "In 1.19.1036 · running here · not released yet", or that a
3
+ // task marked done never reached main — which is how a stranded ship becomes visible to the
4
+ // person who shipped it.
5
+ //
6
+ // HOW IT GETS THERE. The core's task page names no module. It renders an empty slot
7
+ // (#task-contrib) and announces each render as `otb:task-shown`; the server injects this file
8
+ // just before task.js because the module declares a uiSection and ships task-widget.js
9
+ // (src/module-loader/loader.js hallWidgetScripts, file: 'task-widget.js'). Module off: this
10
+ // file is never injected, its asset 404s, and the slot stays empty.
11
+ //
12
+ // THE PAGE KNOWS NOTHING (deploy.js's rule, kept). The stage and version are the server's
13
+ // reading; a partial reading says so; any failure draws no line rather than a wrong one.
14
+
15
+ (() => {
16
+ 'use strict';
17
+
18
+ const API = '/api/bongos';
19
+ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
20
+ const client = window.BongosClient && window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
21
+ const DONE = ['completed', 'confirmed', 'shipped'];
22
+
23
+ // One reading per task per page load: the page re-renders on every action, and each
24
+ // render re-announces, but where the code is does not change between clicks. Bounded —
25
+ // a page shows one task, and the overlay can walk to a few more; nothing needs more than
26
+ // the last handful.
27
+ const cache = new Map();
28
+ const CACHE_MAX = 8;
29
+
30
+ // The line for each stage, in the deploy page's own words (its stage names), shortened to
31
+ // read as one sentence under a task's lifecycle rail.
32
+ const REST = {
33
+ unversioned: () => 'Merged, waiting for the next published version.',
34
+ not_live: (w) => `published, not running here yet${w.offered ? ' · already offered to other projects' : ''}.`,
35
+ live_unreleased: () => 'running here · not released to other projects yet.',
36
+ released: (w) => `${w.live ? 'running here · ' : ''}released to other projects.`,
37
+ // Said with the reason a builder would recognise, the way "Done, but not merged" is: this
38
+ // is most often work that changed no code, and only sometimes a merge that lost its name.
39
+ no_version: () => 'Shipped, though no published version lists it — usually a change that needed no code, like a scope edit or a check, and now and then a merge whose note did not name this task.',
40
+ };
41
+
42
+ function lineHtml(w) {
43
+ if (w.stage === 'not_merged') {
44
+ return '<p class="rec-meta nr-where nr-where--stranded"><b>Done, but not merged.</b> The ledger marks this task done, and its code is not on main yet — a ship that has not landed.</p>';
45
+ }
46
+ const rest = REST[w.stage];
47
+ if (!rest) return '';
48
+ const lead = w.version ? `In <b>${esc(w.version)}</b> · ` : '';
49
+ const partial = w.partial ? ' (a partial reading: the registry or the release notes could not be read)' : '';
50
+ return `<p class="rec-meta nr-where">${lead}${esc(rest(w) + partial)}</p>`;
51
+ }
52
+
53
+ async function reading(id) {
54
+ if (cache.has(id)) return cache.get(id);
55
+ const p = (async () => {
56
+ if (!client) return null;
57
+ const res = await client.request('GET', `${API}/npm-release/task/${encodeURIComponent(id)}`);
58
+ return res && res.status === 200 && res.data ? res.data : null;
59
+ })().catch(() => null);
60
+ cache.set(id, p);
61
+ while (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
62
+ return p;
63
+ }
64
+
65
+ async function show(detail) {
66
+ if (!detail || !/^\d{1,12}$/.test(String(detail.id || ''))) return;
67
+ if (!DONE.includes(detail.status)) return; // nothing to place until it is done
68
+ const w = await reading(String(detail.id));
69
+ // The page may have re-rendered while the read was in flight: write into the slot that
70
+ // exists NOW, and only for the task it is still showing.
71
+ const slot = document.getElementById('task-contrib');
72
+ const shown = window.OTBTaskShown;
73
+ if (!slot || !w || (shown && String(shown.id) !== String(detail.id))) return;
74
+ slot.innerHTML = lineHtml(w);
75
+ }
76
+
77
+ document.addEventListener('otb:task-shown', (e) => { show(e && e.detail); });
78
+ if (window.OTBTaskShown) show(window.OTBTaskShown);
79
+ })();
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ // modules/npm-release/routes/task-where.js — GET /npm-release/task/:id, the one line a
4
+ // task's own page shows about where its code is (task 1004301): which version carries it,
5
+ // whether this hall runs that version, whether it is released — or that a task marked done
6
+ // never reached main. See readTaskWhere in ../work.js for the reading.
7
+ //
8
+ // ANY SIGNED-IN BUILDER, NOT THE DEPLOY PAGE'S ATOM. Every builder can open a task page, so
9
+ // the question the task asked was what this line may show them. It shows nothing that is
10
+ // not already public: the version naming the task is in the package's own release notes on
11
+ // npm, the core this hall runs is what the public GET /version reports, and "released" is
12
+ // the registry's `latest` label. The deploy page stays on `core.pin.move` for what it adds
13
+ // beyond that — the ledger of upgrades, the door. requireBuilder rather than nothing, like
14
+ // the task page's own read, so the line is never a signed-out probe.
15
+ //
16
+ // READ-ONLY.
17
+
18
+ const express = require('express');
19
+ const api = require('../../../src/module-api');
20
+ const { readTaskWhere } = require('../work');
21
+
22
+ const log = api.logger('npm-release');
23
+
24
+ module.exports = function buildNpmReleaseTaskWhereRouter({ read = readTaskWhere } = {}) {
25
+ const router = express.Router();
26
+
27
+ router.get(
28
+ '/npm-release/task/:id',
29
+ api.requireBuilder,
30
+ async (req, res) => {
31
+ const id = String((req.params && req.params.id) || '');
32
+ if (!/^\d{1,12}$/.test(id)) {
33
+ res.status(400).json({ error: { code: 'bad_task_id', message: 'A task id is a number.' } });
34
+ return;
35
+ }
36
+ try {
37
+ const where = await read({ pool: api.pool, id });
38
+ if (!where) { res.status(404).json({ error: { code: 'task_not_found', message: `No task ${id}.` } }); return; }
39
+ res.json(where);
40
+ } catch (e) {
41
+ log.error(`npm-release task-where read failed: ${e && e.message}`);
42
+ res.status(500).json({ error: { code: 'task_where_failed', message: 'Could not read where this task\'s code is right now.' } });
43
+ }
44
+ },
45
+ );
46
+
47
+ return router;
48
+ };
@@ -57,9 +57,16 @@ function configuredPackage(env = process.env) {
57
57
  return v || CORE_PACKAGE;
58
58
  }
59
59
 
60
+ // READ ONCE PER PROCESS. The file ships inside the installed package, so it cannot change
61
+ // while this process runs — an upgrade replaces the package AND restarts the server. The
62
+ // deploy page's reading reached it rarely; a task's own page (task 1004301) reaches it on
63
+ // every view by every builder, and a synchronous disk read has no business on that path.
64
+ let installedNotesMemo;
60
65
  function readInstalledNotes() {
61
- try { return JSON.parse(fs.readFileSync(INSTALLED_NOTES, 'utf8')); }
62
- catch { return null; }
66
+ if (installedNotesMemo !== undefined) return installedNotesMemo;
67
+ try { installedNotesMemo = JSON.parse(fs.readFileSync(INSTALLED_NOTES, 'utf8')); }
68
+ catch { installedNotesMemo = null; }
69
+ return installedNotesMemo;
63
70
  }
64
71
 
65
72
  function boundText(text) {
@@ -194,12 +201,9 @@ function clampInt(v, dflt, max) {
194
201
  * `now`, `store` and `installedNotes` are the seams tests drive; `live` defaults to this
195
202
  * process's own core version when the package is the core.
196
203
  */
197
- async function readWork({
198
- pool, pkg = configuredPackage(), live, days, before, limit,
199
- installedNotes = readInstalledNotes, get, now, store,
200
- } = {}) {
201
- const windowDays = clampInt(days, DEFAULT_DAYS, MAX_DAYS);
202
- const pageLimit = clampInt(limit, DEFAULT_LIMIT, MAX_LIMIT);
204
+ // What versions exist and what each carried — the half of the reading both the deploy page
205
+ // and a task's own page need (task 1004301 split it out of readWork unchanged).
206
+ async function readVersions({ pkg, live, installedNotes, get, now, store }) {
203
207
  const running = live !== undefined ? live : (pkg === CORE_PACKAGE ? api.CORE_VERSION : null);
204
208
  const liveVersion = VERSION_RE.test(String(running || '')) ? running : null;
205
209
 
@@ -225,6 +229,17 @@ async function readWork({
225
229
  const coversTo = versions.length ? versions[versions.length - 1].version : null;
226
230
  const published = registry.ok ? registry.published : {};
227
231
  const released = registry.ok ? registry.latest : null;
232
+ return { liveVersion, registry, versions, source, notesError, coversTo, published, released };
233
+ }
234
+
235
+ async function readWork({
236
+ pool, pkg = configuredPackage(), live, days, before, limit,
237
+ installedNotes = readInstalledNotes, get, now, store,
238
+ } = {}) {
239
+ const windowDays = clampInt(days, DEFAULT_DAYS, MAX_DAYS);
240
+ const pageLimit = clampInt(limit, DEFAULT_LIMIT, MAX_LIMIT);
241
+ const { liveVersion, registry, versions, source, notesError, coversTo, published, released } =
242
+ await readVersions({ pkg, live, installedNotes, get, now, store });
228
243
 
229
244
  const tasks = await recentTasks(pool, windowDays);
230
245
  const stages = placeTasks({ tasks, versions, live: liveVersion, released, coveredAt: coversTo ? published[coversTo] || null : null });
@@ -255,4 +270,50 @@ async function readWork({
255
270
  };
256
271
  }
257
272
 
258
- module.exports = { readWork };
273
+ /**
274
+ * Where ONE task's code is (task 1004301) — the line its own page shows: the same
275
+ * placement the deploy page's list makes, for one task and without the 14-day window.
276
+ *
277
+ * Returns null for an id that is not a task. For a task not yet declared done, `stage` is
278
+ * null: there is no code to place. Otherwise `stage` is one of the deploy page's six, with
279
+ * the version that carries it when there is one.
280
+ *
281
+ * WHAT IT DISCLOSES, because every builder can open a task page: the version that names the
282
+ * task (the package's own release notes, published on npm), whether that version is the one
283
+ * this hall runs (GET /version already says which core runs), and whether it is released
284
+ * (the registry's `latest` label). Nothing here is not already public, which is why its
285
+ * route asks only for a signed-in builder — unlike the deploy page, whose ledger view and
286
+ * upgrade door are the owner's.
287
+ */
288
+ async function readTaskWhere({ pool, id, pkg = configuredPackage(), live, installedNotes = readInstalledNotes, get, now, store } = {}) {
289
+ const taskId = String(id == null ? '' : id);
290
+ if (!/^\d{1,12}$/.test(taskId)) return null;
291
+ const { rows } = await pool.query(
292
+ `SELECT id, status, value_summary, COALESCE(shipped_at, updated_at) AS at FROM tasks WHERE id = $1::bigint`,
293
+ [taskId],
294
+ );
295
+ if (!rows.length) return null;
296
+ const r = rows[0];
297
+ const task = { id: String(r.id), status: r.status, summary: r.value_summary || null, at: r.at instanceof Date ? r.at.toISOString() : r.at };
298
+ const base = { task_id: task.id, package: pkg, status: task.status };
299
+ if (!['completed', 'confirmed', 'shipped'].includes(task.status)) return { ...base, stage: null };
300
+
301
+ const { liveVersion, registry, versions, notesError, coversTo, published, released } =
302
+ await readVersions({ pkg, live, installedNotes, get, now, store });
303
+ const stages = placeTasks({ tasks: [task], versions, live: liveVersion, released, coveredAt: coversTo ? published[coversTo] || null : null });
304
+ const stage = Object.keys(stages).find((k) => stages[k].length) || null;
305
+ const row = stage ? stages[stage][0] : {};
306
+ return {
307
+ ...base,
308
+ stage,
309
+ version: row.version || null,
310
+ offered: !!row.offered,
311
+ live: liveVersion,
312
+ released,
313
+ // A partial reading is said, not hidden: without the notes a shipped task reads as "in no
314
+ // version", which would be a claim the page cannot back.
315
+ partial: !registry.ok || !!notesError,
316
+ };
317
+ }
318
+
319
+ module.exports = { readWork, readTaskWhere };
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1042",
3
+ "version": "1.19.1043",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1042",
9
+ "version": "1.19.1043",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1042",
3
+ "version": "1.19.1043",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -7553,5 +7553,11 @@
7553
7553
  "id": "1001433",
7554
7554
  "text": "The nightly security check of the main code line no longer trusts the name typed on each change, which anyone can fake. It now checks that every change to a sensitive file carries the server's own sign-off from a senior builde"
7555
7555
  }
7556
+ ],
7557
+ "1.19.1043": [
7558
+ {
7559
+ "id": "1004301",
7560
+ "text": "A task's own page now says where its code is — which version carries it, whether it is running on Bongos, whether it is released — and says plainly when a task marked done never actually landed."
7561
+ }
7556
7562
  ]
7557
7563
  }
@@ -103,7 +103,7 @@ const EXEMPTIONS = [
103
103
  + 'absorbed into this check.',
104
104
  },
105
105
  {
106
- at: 'src/module-loader/loader.js:299',
106
+ at: 'src/module-loader/loader.js:309',
107
107
  why: 'The poller twin of the route require at :196 — same DEFAULT_ROOTS bound, same plugin-by-'
108
108
  + 'design rationale, same ADR 0107 / task 1003400 follow-on. Listed on its own line so a '
109
109
  + 'change to either call re-opens only that one.',
@@ -95,11 +95,13 @@ const SEAM_CONSUMERS = {
95
95
  artifact: (dir, entry) => path.join(dir, 'rooms', `${entry}.js`),
96
96
  },
97
97
  uiSections: {
98
- // Per-MODULE, not per-entry: hallWidgetScripts looks for one hall-widget.js
99
- // however many sections the module names.
98
+ // Per-MODULE, not per-entry: hallWidgetScripts looks for one widget file per page
99
+ // however many sections the module names — hall-widget.js for the index, and since
100
+ // task 1004301 task-widget.js for a task's page. Either one wires the declaration.
100
101
  consumer: 'src/module-loader/loader.js hallWidgetScripts',
101
102
  kind: 'file',
102
103
  artifact: (dir) => path.join(dir, 'public', 'hall-widget.js'),
104
+ alternates: (dir) => [path.join(dir, 'public', 'task-widget.js')],
103
105
  },
104
106
  skills: {
105
107
  // moduleSkillSources resolves a declared name against the module's OWN tree or
@@ -77,7 +77,8 @@ let express;
77
77
  try { express = require(path.join(ROOT, 'node_modules', 'express')); }
78
78
  catch (_) { express = require('express'); }
79
79
  const { buildStampedHtml } = require(path.join(ROOT, 'src', 'asset-version.js'));
80
- const { injectBrandingGlobal, mountInternalSurfaces } = require(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'));
80
+ const { injectBrandingGlobal, mountInternalSurfaces, injectModuleHallWidgets } = require(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'));
81
+ const { hallWidgetScripts } = require(path.join(ROOT, 'src', 'module-loader', 'loader.js'));
81
82
  const { clientModules } = require(path.join(ROOT, 'src', 'modules.js'));
82
83
  const { overrideKey, resolveLiveIdentity } = require('./live-identity');
83
84
 
@@ -128,6 +129,9 @@ const FIXTURE_FALLBACKS = [
128
129
  // without it /deploy?project=… only ever rendered its "could not read" state, which
129
130
  // is the trap this directory's README calls out by name.
130
131
  [/^provisioning\/instances\/\d+\/core-upgrade$/, 'provisioning__instances__core-upgrade'],
132
+ // the npm-release module's line on a task's page (task 1004301): one reading serves any
133
+ // id, as the core-upgrade preview above does — the page shows one task.
134
+ [/^npm-release\/task\/\d+$/, 'npm-release__task'],
131
135
  ];
132
136
 
133
137
  // A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
@@ -283,6 +287,15 @@ for (const key of MODULES_OFF) {
283
287
  for (const key of MODULES_ON) {
284
288
  (mods.enabled = mods.enabled || {})[key] = true;
285
289
  }
290
+ // Module page widgets, injected at boot exactly as serve-internal.js does for prod: the
291
+ // hall index's hall-widget.js and a task page's task-widget.js (task 1004301), for the
292
+ // modules THIS harness runs with. Without it a module's line on a page could never be
293
+ // previewed — the page would only ever render its empty slot.
294
+ {
295
+ const isOn = (k) => !!(mods.enabled && mods.enabled[k]);
296
+ if (stamped['index.html']) stamped['index.html'] = injectModuleHallWidgets(stamped['index.html'], hallWidgetScripts({ isEnabled: isOn }));
297
+ if (stamped['task.html']) stamped['task.html'] = injectModuleHallWidgets(stamped['task.html'], hallWidgetScripts({ isEnabled: isOn, file: 'task-widget.js' }), 'task');
298
+ }
286
299
 
287
300
  function sendPage(res, name) {
288
301
  const html = stamped[name];
@@ -443,14 +443,17 @@ function clientBundleScriptTag() {
443
443
  // module's widget is never injected (it vanishes); contributeHallWidget is the second
444
444
  // gate (it no-ops a widget whose module is off). `scripts` is the loader projection
445
445
  // [{ key, src }]; default reads the live enabled set. Pure + exported for the test.
446
- function injectModuleHallWidgets(html, scripts = hallWidgetScripts({ isEnabled: isModuleEnabled })) {
446
+ function injectModuleHallWidgets(html, scripts = hallWidgetScripts({ isEnabled: isModuleEnabled }), consumer = 'builders') {
447
447
  if (!Array.isArray(scripts) || scripts.length === 0) return html;
448
448
  const tags = scripts
449
449
  .map((s) => `<script src="${s.src}" defer></script>`)
450
450
  .join('');
451
- // Anchor on builders.js's <script> regardless of its ?v= stamp so the module
452
- // widgets always land immediately before the consumer that mounts them.
453
- const anchor = html.match(/<script src="\/builders\/builders\.js[^"]*"[^>]*><\/script>/);
451
+ // Anchor on the consuming page script's <script> (builders.js on the index, task.js on
452
+ // a task's page — task 1004301) regardless of its ?v= stamp, so the module widgets
453
+ // always land immediately before the consumer: both are `defer`, so they run in
454
+ // document order, and a widget's listener exists before the page first announces.
455
+ const name = /^[a-z-]+$/.test(consumer) ? consumer : 'builders';
456
+ const anchor = html.match(new RegExp(`<script src="\\/builders\\/${name}\\.js[^"]*"[^>]*><\\/script>`));
454
457
  if (anchor) return html.replace(anchor[0], tags + anchor[0]);
455
458
  return html.includes('</body>') ? html.replace('</body>', `${tags}</body>`) : html + tags;
456
459
  }
@@ -1070,6 +1073,17 @@ function mountInternalSurfaces(app) {
1070
1073
  if (buildersHtml['index.html']) {
1071
1074
  buildersHtml['index.html'] = injectModuleHallWidgets(buildersHtml['index.html']);
1072
1075
  }
1076
+ // task 1004301: the same seam for a task's own page. A module that ships
1077
+ // public/task-widget.js is loaded just before task.js, and fills the slot task.js
1078
+ // renders and announces (`otb:task-shown`). The page names no module; with none
1079
+ // enabled, nothing is injected and the slot stays empty.
1080
+ if (buildersHtml['task.html']) {
1081
+ buildersHtml['task.html'] = injectModuleHallWidgets(
1082
+ buildersHtml['task.html'],
1083
+ hallWidgetScripts({ isEnabled: isModuleEnabled, file: 'task-widget.js' }),
1084
+ 'task',
1085
+ );
1086
+ }
1073
1087
 
1074
1088
  // Serve enabled modules' browser assets at /modules/<key>/public/* — the URL the
1075
1089
  // loader's hallWidgetScripts projection points at. Mounted BEFORE the /builders
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.1042'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1043'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -213,15 +213,25 @@ function mountModuleRoutes(router, { isEnabled } = {}, opts) {
213
213
  // never injected), and contributeHallWidget itself no-ops a stale widget whose module
214
214
  // is off. No-op (empty list) until a module ships a hall widget — same SAFE-BY-
215
215
  // CONSTRUCTION posture as every other seam here. Returns [{ key, src }].
216
- function hallWidgetScripts({ isEnabled } = {}, opts) {
216
+ //
217
+ // ONE SEAM, ONE FILE PER PAGE (task 1004301). `file` names which page's widget is being
218
+ // asked for: hall-widget.js for the hall index (the default, and the only caller until
219
+ // then), task-widget.js for a task's own page. Same declaration (uiSections), same
220
+ // enabled-only asset mount, same "a disabled module simply is not there" — so a module
221
+ // adds a line to the task page without the page's own code ever naming it. The name is
222
+ // matched against PAGE_WIDGET_FILES rather than trusted, so no caller can turn this into
223
+ // a way to serve an arbitrary file.
224
+ const PAGE_WIDGET_FILES = Object.freeze(['hall-widget.js', 'task-widget.js']);
225
+ function hallWidgetScripts({ isEnabled, file = 'hall-widget.js' } = {}, opts) {
226
+ if (!PAGE_WIDGET_FILES.includes(file)) return [];
217
227
  const out = [];
218
228
  for (const { key, dir, manifest } of discovered(opts).loaded) {
219
229
  if (typeof isEnabled === 'function' && !isEnabled(key)) continue;
220
230
  const sections = (manifest.contributes && manifest.contributes.uiSections) || [];
221
231
  if (sections.length === 0) continue;
222
- const scriptPath = path.join(dir, 'public', 'hall-widget.js');
232
+ const scriptPath = path.join(dir, 'public', file);
223
233
  if (!fs.existsSync(scriptPath)) continue; // declarative-only uiSections (e.g. status-page section)
224
- out.push({ key, src: `/modules/${key}/public/hall-widget.js` });
234
+ out.push({ key, src: `/modules/${key}/public/${file}` });
225
235
  }
226
236
  return out;
227
237
  }