@bongos/core 1.19.1031 → 1.19.1033

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 (72) hide show
  1. package/.bongos-core.json +167 -57
  2. package/.claude/skills/grade-audit/SKILL.md +1 -1
  3. package/.claude/skills/grader-health/SKILL.md +1 -1
  4. package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +26 -0
  5. package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
  6. package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
  7. package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
  8. package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
  9. package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
  10. package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
  11. package/docs/adr/0338-modules-travel-through-a-bongos-hosted-store.md +119 -0
  12. package/docs/adr/README.md +4 -3
  13. package/docs/architecture.md +1 -1
  14. package/docs/file-map.md +4 -3
  15. package/docs/module-api-changelog.md +4 -0
  16. package/docs/recipes/autobongos-windows-host.md +3 -1
  17. package/docs/recipes/packaging-the-core.md +2 -0
  18. package/docs/recipes/redteam-patrol.md +2 -2
  19. package/docs/recipes/upgrading-the-core.md +1 -1
  20. package/modules/autonomy/gauge.js +57 -9
  21. package/modules/hall-ui/records/builders-roster.md +10 -0
  22. package/modules/hall-ui/records/catalog.md +10 -0
  23. package/modules/hall-ui/records/collaboration.md +14 -0
  24. package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
  25. package/modules/hall-ui/records/oversight-family.md +19 -0
  26. package/modules/hall-ui/records/panel-family.md +19 -0
  27. package/modules/hall-ui/records/reading-rooms.md +12 -0
  28. package/modules/hall-ui/records/record-family.md +13 -0
  29. package/modules/hall-ui/records/reference-room.md +9 -0
  30. package/modules/hall-ui/records/roster-and-ledger.md +9 -0
  31. package/modules/hall-ui/records/sky.md +23 -0
  32. package/modules/lifecycle/db-claim-reads.js +8 -2
  33. package/modules/lifecycle/dependency-advisory.js +2 -1
  34. package/modules/lifecycle/ship-preflight.js +91 -0
  35. package/package-lock.json +2 -2
  36. package/package.json +1 -1
  37. package/release-notes.json +7411 -0
  38. package/scripts/gds/address-park-sweep.sh +3 -1
  39. package/scripts/gds/autobongos-run.js +81 -1
  40. package/scripts/gds/baseline-staleness.js +1 -1
  41. package/scripts/gds/db-backup-nightly.sh +9 -4
  42. package/scripts/gds/db-backup-offsite.sh +4 -2
  43. package/scripts/gds/package-core.js +72 -1
  44. package/scripts/gds/provision-core-upgrade.js +128 -77
  45. package/scripts/gds/redteam-patrol.js +4 -4
  46. package/scripts/gds/release-notes.js +271 -0
  47. package/scripts/gds/seed-board-room-tasks.js +1 -1
  48. package/scripts/gds/ship-flow.js +5 -3
  49. package/scripts/gds/ship-land.js +86 -7
  50. package/scripts/gds/ship-preflight-steps.js +56 -3
  51. package/scripts/gds/stray-unit-sweep.sh +3 -1
  52. package/scripts/gds/upgrade-migrate.js +67 -0
  53. package/scripts/gds/upgrade.js +6 -4
  54. package/src/module-api.js +1 -1
  55. package/tests/adr_0162_citation.mjs +184 -0
  56. package/tests/autobongos_loop.mjs +114 -0
  57. package/tests/autonomy_gauge.mjs +56 -0
  58. package/tests/core_upgrade_runner.mjs +89 -1
  59. package/tests/db_backup_nightly.mjs +21 -3
  60. package/tests/db_backup_offsite.mjs +81 -0
  61. package/tests/grade_diff_hash.mjs +77 -2
  62. package/tests/hall_records.mjs +85 -0
  63. package/tests/helpers.mjs +25 -0
  64. package/tests/instance_db_migrated_on_code_move.mjs +8 -1
  65. package/tests/plain_cards.mjs +1 -1
  66. package/tests/redteam_patrol.mjs +1 -1
  67. package/tests/release_notes.mjs +327 -0
  68. package/tests/shell_stat_portability.mjs +49 -0
  69. package/tests/ship_land_conflict.mjs +181 -0
  70. package/tests/ship_preflight.mjs +160 -3
  71. package/tests/sweep_app_account.mjs +15 -2
  72. package/tests/upgrade_migrate.mjs +195 -0
@@ -40,6 +40,8 @@ If the no-leak gate fails, it names the file + the residual literal. Fix the red
40
40
 
41
41
  **The neutral floor** (task [#1002217](https://example.com/builders#/task/1002217), audit H4). Because the trunk carries no `config/branding.json`, the *branding-derived* redaction would strip nothing — so `package-core.js` always injects a fixed **`PRIOR_INSTANCE_FLOOR`** (prior-instance tokens/owners) into the doc-redaction + no-leak gate. Published **docs** are de-branded against it and the gate **fails closed** if a floor literal survives, even with no branding.json. **Functional** files still ship verbatim (the R88 fix) and are instead surfaced by the non-fatal `[leak-scan]` (task 1002220). To retire a new prior-instance token, add it to `PRIOR_INSTANCE_FLOOR` (kept in `package-core.js`, never in `mirror-redact.js` — that file's self-leak invariant forbids hardcoded literals).
42
42
 
43
+ **The release notes** (task 1004218). The tarball also carries `release-notes.json` at its root — what each release brought in, as the merge summaries its ships wrote — so the Deploy page on any project can say what an upgrade brings in without a core git checkout (no tenant box has one). It is the one file derived from the ref's **history** rather than from a committed file, built by [`scripts/gds/release-notes.js`](../../scripts/gds/release-notes.js): pinned in the manifest like every other file (so `--self-verify` reproduces it), its prose **redacted exactly like a doc** (the neutral floor applies — real summaries in the history name prior instances, and shipping them verbatim would fail the leak scan on every pack), and bounded at 1 MiB by dropping the oldest releases. **A shallow clone or a `--from-worktree` build ships without it**, printing `! release-notes.json not generated — <why>`; an upgrade onto such a version then says its notes are unavailable. CI packs at `fetch-depth: 0`, so every release it publishes carries them.
44
+
43
45
  ## Prove it boots (the standing PROVE-PUBLISH gates)
44
46
 
45
47
  Two standing gates keep "a published core boots clean from a bare install" honest (task [#1002221](https://example.com/builders#/task/1002221), audit H1/H2):
@@ -55,7 +55,7 @@ files normally. This is opt-in on purpose — the patrol must not *require* a
55
55
  long-lived token to be useful.
56
56
 
57
57
  **Red means a finding was LOST, not that a finding exists.** Findings are
58
- advisory (ADR 0162), so a run that files three of them is a success:
58
+ advisory (ADR 0158 §2), so a run that files three of them is a success:
59
59
 
60
60
  | the run… | exit |
61
61
  |---|---|
@@ -88,7 +88,7 @@ Reports land at `target: redteam-patrol/<class>` for Archon adjudication in the
88
88
  red-team dashboard — confirm / dispute / link-fix, exactly like a
89
89
  `/builder-redteam` filing (they share the pipeline and the bounty table; patrol
90
90
  reports are machine-filed, so confirming one pays nobody). The fix is always an
91
- ordinary task; the patrol never gates and never self-executes (ADR 0162).
91
+ ordinary task; the patrol never gates and never self-executes (ADR 0158 §2).
92
92
 
93
93
  ## Design boundaries
94
94
 
@@ -14,7 +14,7 @@
14
14
  2. **bump the pin** — vendor the new `bongos-core-<version>.tgz` and rewrite the `"@bongos/core": "file:vendor/…"` dependency.
15
15
  3. **`npm install`** — `node_modules/@bongos/core` becomes the new version.
16
16
  3b. **verify the integrity pin** (task [#1002216](https://example.com/builders#/task/1002216)) — recompute `tree_sha256` over the installed core and check the vendored `.tgz` bytes against `tarball.sha256`, comparing to the trusted reference manifest (the committed `vendor/…manifest.json`; the installed inner manifest for `--registry`). Runs **before** migrate so a corrupted core never touches the DB. A **mismatch is fatal → rollback**; a missing reference warns (fatal with `--require-pin`); opt out with `--skip-pin-verify`.
17
- 4. **`npm run migrate`** — the core's `migrate.sh` applies additive `core_*` migrations (merged three-root: core + instance + modules).
17
+ 4. **`npm run migrate`** — the core's `migrate.sh` applies additive `core_*` migrations (merged three-root: core + instance + modules). An instance whose `package.json` declares **no** `migrate` script — an adopted repo, since the adopt layer adds only the core dependency — runs the installed core's `scripts/migrate.sh` directly from the instance root instead, which is exactly what a scaffold's script runs; the pre-flight names it, so the dry run shows it (task [#1004274](https://example.com/builders#/task/1004274)). Before that fallback, such an instance failed here after the pin had moved and rolled back every time.
18
18
  5. **materialize `.claude/`** — refresh skills/settings/hooks from the new core (`materializeClaude`), rewriting invocations to the `bongos` bin.
19
19
  6. **restart** — `--service <unit>` or `--restart-cmd "<cmd>"`.
20
20
  7. **verify** — installed version == target + the **mandatory post-restart health check** (task [#1002222](https://example.com/builders#/task/1002222)): a bump that restarts the service **requires `--health-url <url>`** (it can't report success without confirming the new core serves). Opt out explicitly with `--no-health-check`; a bump with no managed restart needs no URL (it reports the new core installed-but-unconfirmed). A still-failing health poll fails the bump (auto-rollback).
@@ -114,11 +114,14 @@ function readGauge(raw, nowEpochS, config = {}) {
114
114
 
115
115
  const writtenAt = Number(raw.written_at);
116
116
  if (!Number.isFinite(writtenAt)) return { known: false, reason: 'gauge file has no readable written_at' };
117
-
118
117
  const ageS = nowEpochS - writtenAt;
119
- if (ageS > c.staleAfterS) {
120
- return { known: false, ageS, reason: `gauge is ${Math.round(ageS / 60)}m old (stale after ${Math.round(c.staleAfterS / 60)}m) — unknown, not headroom` };
121
- }
118
+ // NO FILE-LEVEL STALENESS BAIL (task 1004178). There used to be one here, and
119
+ // it short-circuited before the per-window logic below could run — which made
120
+ // the "staleness is per window, not per file" rule directly underneath it a
121
+ // dead letter for exactly the files that needed it. An old file whose windows
122
+ // have all turned over is not unknown; it is a file describing windows that
123
+ // have ended. The windows decide, and when none of them can, the return at the
124
+ // bottom says so with the file's age.
122
125
  // A clock that ran backwards is not evidence either.
123
126
  if (ageS < -60) return { known: false, ageS, reason: 'gauge is dated in the future — clock skew, treating as unknown' };
124
127
 
@@ -129,6 +132,26 @@ function readGauge(raw, nowEpochS, config = {}) {
129
132
  // on a fresh written_at forever — "unknown" quietly becoming "headroom".
130
133
  // Files written before observed_at existed fall back to written_at, which is
131
134
  // exactly what they meant.
135
+ // AN AGED-OUT READING WHOSE WINDOW HAS SINCE RESET IS NOT STALE — IT IS
136
+ // SUPERSEDED (task 1004178, found by the 1003907 proof run).
137
+ //
138
+ // WHY: an unattended host cannot refresh this file at all — the writer is a
139
+ // statusLine, which only renders in a terminal session. The evidence and the
140
+ // full account are in modules/autonomy/CLAUDE.md; what matters here is the
141
+ // consequence: without this, the runner holds 30 minutes in and never resumes.
142
+ //
143
+ // The narrow fact that rescues it: when `now` is past a window's own
144
+ // `resets_at`, the window that reading described has ENDED, and the current
145
+ // one began at that reset with zero used. That is what a reset IS, not a
146
+ // guess — which is why this does not breach "unknown is never headroom":
147
+ // nothing is being assumed about the observation, it is being RETIRED.
148
+ //
149
+ // What IS assumed is that nobody else spent the new window. That is why a
150
+ // derived window is MARKED, and why the caller must bound how much work it
151
+ // does on one. The true boundary is also unknowable — these windows roll from
152
+ // first use, not from a fixed clock — so `resetsAt` on a derived window is a
153
+ // lower bound, and the seven-day PACE check is skipped rather than run
154
+ // against a boundary we invented.
132
155
  const win = (w) => {
133
156
  const o = raw[w];
134
157
  if (!o || typeof o !== 'object') return null;
@@ -136,8 +159,14 @@ function readGauge(raw, nowEpochS, config = {}) {
136
159
  const resets = Number(o.resets_at);
137
160
  if (!Number.isFinite(used) || !Number.isFinite(resets)) return null;
138
161
  const observedAt = Number.isFinite(Number(o.observed_at)) ? Number(o.observed_at) : writtenAt;
139
- if (nowEpochS - observedAt > c.staleAfterS) return null; // this half is no longer evidence
140
- return { used, resetsAt: resets, observedAt, ageS: nowEpochS - observedAt };
162
+ if (nowEpochS - observedAt <= c.staleAfterS) {
163
+ return { used, resetsAt: resets, observedAt, ageS: nowEpochS - observedAt, derived: false };
164
+ }
165
+ // Aged out. Superseded ONLY if its window has actually turned over.
166
+ if (nowEpochS >= resets) {
167
+ return { used: 0, resetsAt: resets, observedAt: resets, ageS: nowEpochS - resets, derived: true };
168
+ }
169
+ return null; // old, and its window is still running — no longer evidence
141
170
  };
142
171
  const fiveHour = win('five_hour');
143
172
  const sevenDay = win('seven_day');
@@ -145,7 +174,7 @@ function readGauge(raw, nowEpochS, config = {}) {
145
174
  // Both windows are optional in the source ("may be absent"), but with neither
146
175
  // there is nothing to decide on.
147
176
  if (!fiveHour && !sevenDay) {
148
- return { known: false, ageS, reason: 'gauge carries no window fresh enough to use — absent or aged out' };
177
+ return { known: false, ageS, reason: `gauge is ${Math.round(ageS / 60)}m old and no window has turned over since — absent or aged out, which is unknown, not headroom` };
149
178
  }
150
179
  return { known: true, ageS, fiveHour, sevenDay };
151
180
  }
@@ -188,7 +217,9 @@ function decideCapacity({ gauge, now, config = {} } = {}) {
188
217
  return { go: false, unknown: false, sleepUntil: g.sevenDay.resetsAt, window: 'seven_day',
189
218
  reason: `seven-day window spent (${g.sevenDay.used.toFixed(1)}%) — sleeping to its reset` };
190
219
  }
191
- if (g.sevenDay.used > pace.allowedPct) {
220
+ // A DERIVED seven-day window carries a boundary we inferred, so pacing it
221
+ // would measure progress through a week we guessed at. Skipped, not faked.
222
+ if (!g.sevenDay.derived && g.sevenDay.used > pace.allowedPct) {
192
223
  const until = whenPaceAllows(g.sevenDay, c);
193
224
  // Never propose a wake in the past, and never past the reset itself.
194
225
  const sleepUntil = Math.min(Math.max(until, now + 1), g.sevenDay.resetsAt);
@@ -209,7 +240,24 @@ function decideCapacity({ gauge, now, config = {} } = {}) {
209
240
  const bits = [];
210
241
  if (g.fiveHour) bits.push(`5h ${g.fiveHour.used.toFixed(1)}%`);
211
242
  if (g.sevenDay) bits.push(`7d ${g.sevenDay.used.toFixed(1)}%`);
212
- return { go: true, unknown: false, sleepUntil: null, reason: `headroom (${bits.join(', ')}; gauge ${Math.round(g.ageS)}s old)` };
243
+ // A derived window is reported as such all the way out, so a caller can bound
244
+ // the work it does on one and a reader can see which kind of "go" this is.
245
+ const derived = !!((g.fiveHour && g.fiveHour.derived) || (g.sevenDay && g.sevenDay.derived));
246
+ const detail = derived
247
+ ? "the last reading's window has since reset, so it was retired rather than trusted"
248
+ : `gauge ${Math.round(g.ageS)}s old`;
249
+ // `derivedSince` is the reset the derived baseline stands on. The caller needs
250
+ // it because a derived window cannot be paced: a SEVEN-day window will almost
251
+ // never turn over during a night, so its stale reading is dropped and the
252
+ // weekly pace check — the one the module's own notes call the thing that
253
+ // actually binds a heavy week — does not run at all. A stale weekly figure
254
+ // cannot stand in for it either: usage only grows, so an under-reported value
255
+ // makes the check MORE permissive, which is the wrong direction to be wrong in.
256
+ // Bounding the work done since `derivedSince` is what replaces that pacing.
257
+ const derivedSince = derived
258
+ ? Math.min(...[g.fiveHour, g.sevenDay].filter((w) => w && w.derived).map((w) => w.observedAt))
259
+ : null;
260
+ return { go: true, unknown: false, sleepUntil: null, derived, derivedSince, reason: `headroom (${bits.join(', ')}; ${detail})` };
213
261
  }
214
262
 
215
263
  module.exports = {
@@ -0,0 +1,10 @@
1
+ # The builders page — /roster, the in-project roster (task 1003441)
2
+
3
+ **The BUILDERS page landed next (task 1003441, goal 1000075)** — the in-project roster, at `/roster`. It is not a redesign but a MOVE: the Roll was a `#leaderboard` HASH VIEW on `index.html`, which is why the Community group read as one real page plus a link into the home page. Four things it settled:
4
+
5
+ - **The load-bearing part was not the markup.** `shell.js` `VIEWS`, `dom-utils.js` `HALL_VIEWS` and `hall-widgets.js` all declared `leaderboard`; the table, its pager, its sort lenses (`LB_SORT_HEADERS`, `sortedLeaderboardRows`) and the achievement laurels (`ACH_EMOJI`) lived in `hall-render.js`. All of it MOVED to `roster.js` rather than being copied, so Home carries no dead renderer. `roster.html` is the board archetype at people.html’s shape and reuses `people.css` for the identity cell — one definition, both rosters.
6
+ - **`lbState` and `leaderboardPlace()` STAYED on Home**, shrunk to `{ rows: [] }`. The Standing card still says "#N on the leaderboard", and it needs the same rows; the fetch stays too. Verified live: the card reads `#4 on the leaderboard` with the table gone.
7
+ - **The hash REDIRECTS, it does not alias.** `#achievements` could fold into Standing because Standing exists on that page; there is no host view for a Roll that left. So `VIEW_REDIRECTS` in `shell.js` sends `#leaderboard` to `/roster` via `location.replace`, guarded to the index — without it `viewFromHash` returns nothing and `route()` silently leaves the reader on Home, a dead link that looks like it worked.
8
+ - **It is deliberately UNGATED,** and `tests/hall_page_gate_map.mjs` spells that out rather than defaulting. Its one data source, `GET /leaderboard`, is deliberately unauthenticated and mirrors the public status surface; a page must not be gated tighter than the API it fronts, and it was already reachable to a stranger as a view on the ungated index. No visibility predicate is applied and that is correct — privacy suppresses cross-project surfaces only, so a `private` builder still appears on the roster of a project they build on (ADR 0225).
9
+
10
+ Pinned by `tests/hall_nav.mjs` (the Community group's middle id is `roster`) and `tests/hall_page_gate_map.mjs`. Measured through `roster.states.json` — whose `out` state renders the REAL page, since the page is ungated.
@@ -0,0 +1,10 @@
1
+ # The catalog pages — modules and the repo atlas (task 1003472)
2
+
3
+ **The CATALOG pages landed next (task 1003472)** — modules and the repo atlas. Modules rides the oversight family's row (`oversight.css` before `modules.css`); the atlas is NOT a list page, so its scene stays and only the chrome around it moved. Four things it settled:
4
+
5
+ - **Modules are ROWS, and the description is why.** The task asked for rows to be tested against the cards rather than assumed, since modules was the one hall page already rendering its record's description. Against the real catalog **the description is what breaks the card**: it runs 120–1,200 characters, so the 280px auto-fill grid stood a 500px card beside a 120px one and the toggle — the row's one control — sat at a different height in every column. The row clamps the description to two lines (`.mod-row__desc`, lifted while the row's `<details class="mod-more">` is open) and the disclosure opens the facts the card never rendered. The page is ONE panel; the old "Contribute a module upstream" panel is gone — the submit door is a `.btn-ghost` on the row of the module it submits.
6
+ - **The switch is the family's `.ov-toggle`, and the state is never an accent fill.** Fourteen `button.mod-toggle[data-on="1"]` pills painted salmon were the loudest thing on the page and none of them was live. Enabled reads in ink, disabled in the faint ink, the accent on hover; a viewer below the floor (`rankLabel('metic')`) sees a `.fact-pill` state, not a dead control. Colour marks the exception: `upkeep undeclared` is the one warn pill; a maintained module's upkeep is a plain `.ov-fact`. The `.mod-chip` alias left the shell's pill list — modules takes `.fact-pill` by name.
7
+ - **The atlas keeps its nine colours and joins the shell everywhere else.** `--ab-sun … --ab-danger` stay fixed literals (ADR 0094; `tests/hall_catalog_world.mjs` pins them AND that nothing else in the sheet is a literal). The panel is the shell's `.scroll` — its private hairline and 4% accent wash are gone, which answers the "pane" question the task raised by the owner's standing `.scroll` decision (task 1003470) rather than reopening it. `.ab-modes` (a segmented box, the third toggle grammar) is the shell's `.kit-tabs` driven by `aria-selected`. The hub pulse went from 5.5s to **24s, the world's ambient floor**; the accent "live" chip, the glass hint pill and the two `--shadow-pop`s went with it.
8
+ - **The island index is the map's keyboard and phone path.** The scene is a hover surface: SVG `<g>`s with mouse listeners, **no focusable element**, and at 390px thirty islands in 360px with nothing under them. `drawIndex` renders every node of the current view as a dense `.ov-row` under a `kit-collapse` (`#ab-index`) whose title is a disclosure button (`aria-expanded`/`aria-controls`) laying the SAME `detailHTML` inline; hovering a row lights the island, and **the index opens itself under 640px (`matchMedia`), where the list is the read.**
9
+
10
+ Also gone, and the one rule in that list: `.ab-caption` takes the lede's `--measure`, never a fresh `90ch`. **Gotcha:** `gen-atlas.js` run from the MAIN checkout walks `.claude/worktrees/*` and emits thousands of context nodes — run it from a worktree, or copy a worktree's `atlas.json` over, before previewing the atlas there.
@@ -0,0 +1,14 @@
1
+ # The collaboration pages — collab and the copy desk (tasks 1003471, 1003942, 1004067)
2
+
3
+ **The COLLABORATION pages landed next (task 1003471)** — collab and the copy desk. They ride the oversight family's row (`oversight.css`, linked **before** each page's own sheet) rather than a fourth archetype: **a page whose repeated thing is a titled row with facts and actions IS that archetype at its own density.** Four things it settled:
4
+
5
+ - **A help request has no title, so the row DECIDES one.** `collab-lib.js` `titleOf` makes the ask's first sentence the 15px/500 title and the rest the sub line — the asker's own words, nothing clipped; a first sentence too long to be a line keeps the whole ask together. The row id stays unshown on purpose: an id with no door is noise (the archetype's mono id is a goal's, which has a page).
6
+ - **"Who is asking" is a server join, not a client guess.** `lifecycle/help-requests.js` reads its two lists through `LIST_COLS`/`LIST_FROM` — two `LEFT JOIN builders` for the asker and the by-name addressee — while `COLS` stays bare for the UPDATE's `RETURNING`, which cannot reach a joined alias. The row renders the name as a door to `/builders/profile?id=N`, with the login and then the bare id as fallbacks, so an older server still names someone. **The row's fact says DIRECTION, not just the name (task 1003274, owner report):** `collab-lib.directionLine(req, dir)` writes "You asked Lars by name" on your own asks and "Lars asked you by name" on what was asked of you, and `dir` comes from WHICH SERVER READ produced the row (`/help-requests/mine` vs `/for-me`) rather than from comparing `requested_by` to a viewer id. That matters: the old `addressedTo(req, viewerId)` named the addressee only when the viewer WAS the addressee, so the asker reading their own list — the commonest case for that branch — got "asked of one person", and the owner reasonably concluded their ask to Lars had been misrouted. `addressedTo` is deleted, not deprecated; `directionLine` takes no viewer id, and a test pins its arity so the comparison cannot come back. The craft/by-name tonal split (ADR 0187 §3) travelled with it — "Lars asked artists" stays a call anyone of that craft may take. `directionFact` in `collab.js` splices the profile link over the person's name **with a replacer FUNCTION**, because a builder-controlled display name containing `$&` would otherwise be substituted by `String.prototype.replace`.
7
+ - **The age is a pill in the right column, and the test moved with it.** `tests/collab_page.mjs` pinned a 3px coloured `border-left` for the stale band; this world marks state with a pill, never a bar, so the pin now reads the four `.cb-age--*` colour rules (faint, soft, warn, danger). "Asked of you" reads in runs under group heads — by name first, then each craft — because a personal ask is the one you owe an answer.
8
+ - **The copy desk's state was OPACITY, and opacity dims the panel too.** `.cd-tag--flagged/--settled/--moved/--gone` differed by .6/.75/.85/1 and the same ran through nine more rules, which is why the page read muddy rather than quiet. State is the `.fact-pill` vocabulary now (target gone = warn; already flagged = open; looked at = ok; **untouched = the plain pill, because colour marks the EXCEPTION** in a list that is almost entirely work). The duplicates page on a ROW list, not a ledger, because each row carries its places; the two dialogs are the family `.ov-modal` on a native `<dialog>` (its own focus trap and Escape; **the `::backdrop` scrim is the sheet's one sanctioned literal**), with the hints moved OUT of the labels — **the modal's label voice uppercases whatever sits inside a `<label>`.**
9
+
10
+ Also gone, and the one token rule in that list: `--accent` is the FILL token and must not be spent on a link (`.cb-ctx` was).
11
+
12
+ **Collab became a THREE-BLOCK room of PEOPLE (task 1003942, owner direction: "way too text heavy, doesn't feel collaborative" — asked of you left, your asks right, every open ask below).** The top two ride the shell's `.view-zone--band`; the third is the wide `.cb-block--all`, fed by a new read, `GET /help-requests` (`help.listOpenAll`: every open ask in the project, newest first, any signed-in builder, **takes nothing from the caller** so C01's `?craft=` hole cannot reopen). Each block is condensed to the **newest three** with a "Show all N" `.btn-ghost` that flips `[data-extra]` rows (`collab-lib.splitShown` — the button's number and the rows it reveals come from one split; no re-render, no motion), and each head carries its count and the faces of who is asking. Every row is the family's `.ov-row--lead` with a face in the lead column. Four rules travel with it: **the face is decided by the lib** (`collab-lib.faceOf(req, dir)`: the asker incoming and on the board, the addressee outgoing, a lettered dashed disc for a craft ask so it never passes for a person; `httpUrl` is roster.js's allowlist, so a src the page did not vouch for renders as the initial); **the board speaks in the third person** (`boardLine`: "Alpha asked Beta by name", never "you"); **a by-name ask is the one lit thing** (`.cb-row--live` puts the accent INK on its direction line — the personal call, and nothing in the sheet spends the accent fill, test-pinned); and **the prose budget is one lede on the whole page** (the social panel's), with the "declining costs nothing" sentence said once at the foot. Inside the two half-width blocks the row's right column drops under the words and the date yields to the age pill; the band goes to one column under 1100px. The avatar rides the existing builders JOIN (`requested_by_avatar_url` / `needs_builder_avatar_url` in `LIST_COLS`). The sidebar Collab item took the count badge the same day — `count: 'help_requests'` on the nav item, `count: n` on `helpRequestsNeed` — exactly the adoption task 1003737's mechanism promised; the three collab needs' stale `/#/collab` hrefs became `/collab` with it. **The harness needs `help-requests.json`** for the board block, beside the two it had.
13
+
14
+ **A COLLAB PILL MARKS A ROW WORSE THAN ITS ROOM, not every row (task 1004067, from idea 1001065).** The report was fourteen "waiting over a week" pills on fourteen rows. Task 1004041 fixed the WORDS (`collab-lib.ageLabel` gives each row its real age) and dropped the ring from the two calm bands; what it could not fix from inside a single row is a block where every row is genuinely past a week — there the condition is correct on all fourteen, and fourteen correct alarms are still not a signal. **The instrument was wrong, not the threshold.** So `paintBlock` computes `collab-lib.roomBaseline(list, nowMs)` — the severest band wearing a pill (severity >= 2) that covers >= `BASELINE_SHARE` (0.7) of at least `BASELINE_MIN_ROWS` (4) rows — writes `roomAgeLine` into the head's `#cb-<key>-age`, and `ageChip` adds `.cb-age--quiet` to every row at or below it. Four things travel with it: **the baseline is over the WHOLE list, not the shown three**, or pressing "Show all" would re-alarm the rows it had just quieted; **a room with no dominant band is untouched**, which is what keeps this inert on the board as it usually looks; **a calm room announces nothing** (an all-`fresh` block has no alarm to move to the head, and putting one there would invent the noise this removes); and **the per-row words are never quieted**, only the treatment — an old ask stays visibly aging rather than silently dead, which is the criterion the bands exist to satisfy. Run, not grepped, in `tests/collab_page.mjs`.
@@ -0,0 +1,11 @@
1
+ # The per-goal dependency forest — on the Goals page (tasks 1003480, 1004200)
2
+
3
+ **THE PER-GOAL DEPENDENCY FOREST landed on the Goals page (task 1003480, goal 1000089; ADR 0227 §6 item 4 and its §10.5 amendment)** — the drill-down the goal map's own header pointed at (the map is the project sky since task 1004232): a task-level picture of which of THIS goal's tasks depend on which, from `GET /goals/:id/dependency-forest`. `goal-forest-lib.js` was the goal map's sibling — the same layered-layout idea (a task sits one column right of what it depends on, deterministic) at a smaller scale, since a per-goal forest is small BY MEASUREMENT (ADR 0227 §10.5: largest 127 tasks, median 9) rather than needing the goal map's cluster-and-shelf packing. Five things worth knowing:
4
+
5
+ - **Its own full-width section, not a band column.** The existing Completion / Members / Conflicts band (task 1828) is three narrow columns; a node-link picture needs real width, so "Task dependencies" sits as its own `.scroll` block between the band and Tasks, drawn with `createElementNS` + `textContent` (the atlas idiom, which the retired goal-map.js shared — a task title never becomes markup) inside `.gf-scroll`, which names BOTH overflow axes (task 1004130).
6
+ - **No cap, no "view all", unlike its band neighbours.** Completion/Members/Conflicts are per-item lists that can genuinely grow long, so they cap-and-link into a dedicated full view. A dependency forest cannot — ADR 0227 §3's own finding is that the tasks it omits are precisely the ones with nothing to connect them to, so what the server returns is already the honest whole picture, every time.
7
+ - **The states are three, and distinct on purpose:** "Reading…" while the request is in flight, "No task in this goal depends on another task in this goal" for a real empty forest (never a blank canvas), and "Couldn't load…" only for an actual failure — a redirecting-to-auth error is swallowed silently instead, the `loadGoalTasks` precedent, because the page is already navigating away.
8
+ - **`loadDependencyForest`/`renderDependencyForest` are exported from `createGoalsRender`'s return** specifically so `tests/goal_forest_page.mjs` can call them directly against a minimal DOM stub, without needing the much larger fixture a full `renderDetail()` boot would need for members/completion/invites this task never touches.
9
+ - **The harness fixture is a single generic `goals__dependency-forest.json`** (task ids real, drawn from goal 25's own rows in `tasks.json`) — every goal id in the harness sees the same forest, which is fine for looking at the picture and not meant to describe any goal's real dependencies.
10
+ - **The drawing code is its own file, `goal-forest-render.js`** — not inline in `goals-render.js`: that file sits at the 1,500-line ratchet, so a new self-contained feature is a new file. `window.OTBGoalForestRender(ctx)` takes the same `{ API, state, sameId, getJSON }` slice of `ctx` and returns `{ loadDependencyForest, renderDependencyForest }`, which `goals-render.js` destructures and re-exports unchanged.
11
+ - **A node title is fitted to MEASURED width, never a character count (task 1004200).** A 24-character budget ran every title 34–41px past its 168px card, because bold proportional glyphs vary per glyph. `fitLines` (pure, in the shared `text-fit.js`, with the caller's `measure`) wraps the title into two lines at spaces and ellipsises the last; the renderer measures with `getComputedTextLength()` AFTER the svg is appended, refits once web fonts land, and puts a `clipPath` on every line as the backstop for a forest drawn while hidden. The full title stays in the node's `<title>` and the table. The goal map fits its three node lines the same way with `fitText`, one line each, loading the same `text-fit.js` (task 1004204).
@@ -0,0 +1,19 @@
1
+ # The oversight family — government, watch and gate, and the Metic+ pages (tasks 1003469, 1002332; harbor was removed in task 1003890)
2
+
3
+ **The OVERSIGHT family landed next (task 1003469)** — government, watch, harbor and gate, then the four Metic+ pages behind `requireGovernmentPage`. This was the REDESIGN, not the re-skin: task 1003307 had put the four in the world token-wise and the owner read the result as "minimal effort reskins", so this pass changed composition. `oversight.css` is the third archetype sheet — what an oversight ROW is in this world, the figure band inside a panel (`.ov-figs`, the record family's band restated), the group head, the inline form, the grant toggle, and the ONE confirm modal harbor and gate now share on `--z-modal`. Five things it settled:
4
+
5
+ - **The sheet is named `oversight`, not `governance`, and that is the vocabulary wall, not taste.** `scripts/gds/fitness.js` `checkVocabularyWall` bans "governance" and "role" on the government surface (ADR 0174: it is the GOVERNMENT now), and a `<link href="governance.css">` in `government.html` reds the build. Name a thing this family shares for what the four pages do together — oversight — and keep the word out of `government.*` entirely, comments included.
6
+ - **The government copy test pins two class names by their literal markup.** `tests/government_board_room_copy.mjs` looks for `<h2 class="gov-h">Constitution</h2>` followed by `<p class="gov-grid-note">`, so those two names stay in `government.html` and `government.css` gives them the shell's own voice (`.scroll__h` / `.scroll__lede` recipes). Do not "converge" them onto the shell names — the test reads the markup, not the render.
7
+ - **Three pages threw a fact away, and the row now shows it.** A rank's `description` was collected by the create form and never rendered (it is the rank row's sub line); Watch fetched `/sessions/search?limit=5` and read only `totals` (the five rows are a ledger under the tally, in `#sessions-recent`); the box's `claude_active` was a suffix nobody scanned for (it is a fact on the row and a clause on the summary tile).
8
+ - **Every Watch list is the kit ledger, and a list that pages delegates its actions.** `renderTable` and `<table class="watch">` are gone; nine lists ride `makeLedger`, the six that can grow with a `makePager` (the bar hides itself under one page). A row the kit re-renders on a page flip has no listener of its own, so reports, overrides and the roster wire ONE listener on the section (`delegate()`), guarded so a reload does not stack a second.
9
+ - **The shell styles text and search inputs, not number inputs.** `#gov-assign-builder` rendered at the browser's 21px and failed `controls-24px` on every government render until `.ov-field` restated the shell's 30px hairline pill for every control it holds. The other floor trap: a `max-width` on a flex child caps its flex BASIS, so a `flex: 1 1 100%` explainer line rode beside the button instead of under it — `min-width: 100%` is what makes a line take the row.
10
+
11
+ Also gone, and the one rule in that list: a sealed state is a `.scroll` panel whose lede is written from `OTB.rankLabel('metic')`, **never a hardcoded "Metic+"**, so the floor cannot drift from ADR 0157. (The same rule holds on collab, the roster, the session ledger and modules.)
12
+
13
+ **Watch's access-requests section became the applications REVIEW queue (task 1002332, BV1.R13)** — a project's applications ARE its own `access_requests` rows (ADR 0182 §Scope), and this section is the owner's surface for them until R4's console page absorbs it. Three things it added, all inside `renderGate`/`loadGate`:
14
+
15
+ - **Every row says whether the platform vouched for the name on it** — a `.fact-pill--ok` "Identity vouched" or a `.fact-pill--warn` "Name unvouched", from the `vouched` the queue read already returned (`applicant_github_id IS NOT NULL`, core_230). `POST /access-requests` is the one PUBLIC unauthenticated write, so anyone may apply under anyone's name; ADR 0201 calls the distinction UI-load-bearing, not decoration. It is never authority (ADR 0016) — both classes offer the identical buttons, and `tests/watch_applications_queue.mjs` asserts exactly that.
16
+ - **The three lifecycle states sit behind one `makeFilterBar` switch**, so the decided history is readable and the RESCIND (`invited → dismissed`) the route has always taken is finally offered — ADR 0208 recorded that nothing on this page did. A declined row offers no button: reviving one is the invite form above, which is what the server's own 409 names. The switch is rebuilt each render (the same lifecycle `wireInviteForm` lives with, since `section` replaces the whole body).
17
+ - **The `att-access` tile counts pending only.** Its sub line says "pending", so the count is guarded on the pending view — reading the history must not repaint the board with a number that means something else.
18
+
19
+ Also gone: harbor's 280px card deck with a 4px coloured "roof" band per box and gate's crate with a verdict band (state is a `.fact-pill`; the fleet reads in three grouped runs — running, parked or in transition, no box); government's bordered `.gov-section` boxes and the one `.gov-assign-row` five unrelated lists shared (each renders as the row it is); watch's third copy of the stat tile, its `table.watch`, its `.watch-btn`; three bare `<p>` sealed states (each is a `.scroll` panel whose lede is written from `OTB.rankLabel('metic')`, so the floor cannot drift from ADR 0157). The harness gained fixtures for `/boxes`, `/gate-approvals` and five session rows, because a page that only ever renders its error state cannot be redesigned by looking.
@@ -0,0 +1,19 @@
1
+ # The panel family — settings and profile, and the profile's three screens (tasks 1003308, 1003489; pair was removed in task 1003890)
2
+
3
+ **The PANEL family landed next (task 1003308)** — settings, profile and pair: the pages where a builder reads a label, a sentence of consequence, and acts on ONE control. (A fourth, `not-ready`, shipped with them and was deleted in task 1003450 — it was a bespoke refusal page for a rank tier the hall no longer has, and the shell's `?denied=` banner says the same thing on any page, naming the permission actually missing.) `panel.css` is the fourth archetype sheet (mock `docs/design/mocks/hall/settings/v3.html`); `settings.css`, `profile.css` and `pair.css` then hold only what one page has and the others do not. **The hall's my-projects page is GONE** (owner decision in this task's review): "my projects" is a user-level page that already lives on the hub, so the hall ships neither the page, nor a link, nor a redirect — `tests/hall_settings_world.mjs` pins the absence. Five things it settled:
4
+
5
+ - **The page scripts keep their names; the archetype aliases them.** `.sound-row`, `.sessions-row`, `.settings-btn`, `.craft-chip` and the rest are grouped onto the family's rules by selector list (the way `style.css` aliases `.gov-pill` onto `.fact-pill`) rather than renamed across three scripts — a class name was not what was wrong with these pages, six hand-rolled rows were. The two HTML-only pages DID take the family names (`.prow`, `.pinput`, `.pbtn`), because nothing else spells theirs.
6
+ - **The family takes the ROOM's head voice, without the room's air.** The mock draws "settings" large and lowercase in the display face, and it is right — on a settings page the document is the page. `.page-head--panel` on `.page-head--room` zeroes the top padding so a form starts at the content edge. The h1 is static markup on all five, so every state has it (the family had none: `one-header-main-h1` failed on every render).
7
+ - **A toggle is a `role=switch`; a pick is a radio.** `switchHtml` in `settings.js` writes `aria-checked` and a 44x24 track inside a 46x38 target (`.sw`), and keeps `data-on`, which every delegated click handler reads. The wandering level is four `.prow--pick` radio rows on `change`, the rank ceiling a disabled radio that says so in the label voice — not four "Choose" buttons in the toggle's class. `.craft-pref` (13x13, the smallest control in the hall) takes the board's <redacted> recipe.
8
+ - **The Discord pair is the one literal, named as such.** Eight literals across three sheets are gone; `#5865F2`/`#4853c4` stay under a `THIRD-PARTY MARK, KEPT ON PURPOSE` comment, and `tests/hall_settings_world.mjs` pins that those two (and their white) are the ONLY literals in `settings.css` and sit only on `.settings-btn--discord`.
9
+ - **The harness had no feed for two of these pages.** Sessions 404'd and pairing had no request, so the row and the device had only ever rendered as empty states — the CLAUDE.md gotcha of the oversight family again. Fixtures added: `me__sessions`, `auth__app-pair__info`; `me__cross-project` gained the totals the settings row reads.
10
+
11
+ Also fixed here, as the board task handed forward: the two sub-24px links on Home's Standing card and the thirteen ledger links on the profile take the `style.css` prose-link recipe. **Not changed on purpose:** the CLI token still renders through an escaped template (idea 1000884 is filed narrowly for the `createElement` rewrite); and the dev-box group's composition, because the harness runs without the module.
12
+
13
+ **The profile became three screens, and grew charts (task 1003489)** — the owner accepted the profile re-skin at the 1003308 review and found the page too long: seven sections stacked, the ledgers under the portfolio. It now borrows settings' rail whole — `OTBKit.makeSectionRail` in switch mode, `.profile-groups[data-active]` in `profile.css` showing one group, a section's own `[hidden]` still winning — under four keys the owner confirmed: **overview**, **works**, **activity** (the craft ledgers and the idea trail exactly as task 1003281 built them — the SAME works seen one craft at a time, plus goals and the idea trail, which is a different question and so a second key, not a duplicate) and **connections** (hub-only). **The key follows the section**: profile.js watches `#conn-scroll`'s `hidden` attribute and refreshes the rail, so a vanilla instance never lists a key that opens an empty screen. Deep links are the key (`/profile?id=95#activity`); the `#/…` overlay namespace passes through. Four things it settled:
14
+
15
+ - **The charts are `profile-charts.js`, and they need no library.** Works shipped and drachmae earned are cumulative step-lines (the value holds until the next day with a ship, then runs flat to today), the craft mix is bars, and on the viewer's own profile their pace by week (`/analytics/builder/:id` is own-or-archon, so it is asked for only when `selfId` is the builder — never a 403 toast). Works and craft derive client-side from `/builders/:id/activity?section=tasks`, followed to `total` up to 500 rows. **Drachmae earned is the credits ledger** (task 1003491) — `/me/credits` on your own profile, `/builders/:id/credits` (`builder.roster.read`, metic+) on another's — `delta` cumulated by day from `recorded_at`, so the line ends at the headline balance; a viewer the ledger refuses gets the activity rows' `credits_reward` estimate, labelled "estimated". Never the per-task `credits_awarded`: on a cost-plus instance (ADR 0146) it is 0 for every task, and summing it flatlined the owner's chart from July. The pure builders (`cumulativeByDay`, `craftMix`, `weeklyBuckets`, `estimateOf`, `clipToWindow`) are exposed for tests.
16
+ - **Dataviz rules, kept.** One axis; one series per chart, drawn in `--accent` (Home's per-chart colours were not copied — a single series has no identity to encode); 2px lines, ≤24px columns with a 4px rounded cap on a square base, a hairline solid axis in `--rule-soft`, an r=4 end dot with a 2px `--bg-card` ring; only the endpoint labelled, in ink; a crosshair readout on hover that the arrow keys walk too; a `<details>` table twin under each time chart, so no number is picture-only. The readout is written with `textContent`.
17
+ - **Honest empties, one line each.** Signed out (or a 401 mid-way) the grid is the same permission sentence the activity ledgers use; an empty record is "No works shipped yet"; a failed read says so. Never three empty cards.
18
+ - **The Works key, and its public fallback.** Works is the full server-paged ledger from `/builders/:id/activity?section=tasks` (any signed-in viewer, any builder); signed out, the page's PUBLIC `recent_ships` stand in with a sign-in line, so a stranger still sees the last few. The Overview no longer carries a works strip — that removes the duplicate the DONE-WHEN named.
19
+ - **A lookback control (owner request, task 1003489) scopes every chart** — 30 days / 90 days / 1 year / All time, one segmented row above the grid (the dataviz date-range-first rule). The cumulative charts clip their axis to the window and ENTER it at the running total, not at zero (`clipToWindow`); the craft mix counts within it; the pace chart passes the window to `/analytics/builder/:id?days=`. Remembered per viewer; changing it re-draws from the rows already in hand — no re-fetch. (A chart LIBRARY was floated by the owner and deliberately NOT taken here — the hall ships none, the hand-drawn charts satisfy the ask, and the swap is its own idea to weigh CSP/weight/theming/table-twin against.)
@@ -0,0 +1,12 @@
1
+ # The reading rooms — the primer and the diagrams (task 1003306)
2
+
3
+ **The READING ROOMS landed next (task 1003306).** `room.css` is the archetype, the second after `record.css`; `primer.css` and `diagrams.css` then hold only what one room has and the other does not. **The classes stay `primer-`-prefixed on purpose**: `diagram-viewer.js` upgrades `figure.diagram img, figure.primer-figure img` with one selector and `hall_audit` pins `.primer-article table`'s 760px scrollport by name, so the sheet is named for the room and the prefix is left alone. Four things it settled:
4
+
5
+ - **ONE markdown parser.** `primer.js` carried its own copy of `md-reader.js`'s grammar *minus blockquotes*, so every `> aside` in the primer silently collapsed into a paragraph. The copy is gone; the primer keeps only the two things that are genuinely its own — the diagram injected after a named h2, and the one rewrite that sends `drachmae-and-karma.md` to `/builders/drachmae` instead of GitHub. **Anything the primer's markdown needs next belongs in `md-reader.js`**, where the economy reader gets it too.
6
+ - **A soft gate must be `aria-disabled`, not `disabled`.** A real `disabled` button leaves the tab order, so a keyboard reader could never land on the acknowledge control and never heard why it was locked. The sheet paints the OPEN state off the same attribute the script writes, and the gate now shows a *measurement* (`Read 46% of the 90% needed`) rather than a refusal.
7
+ - **The `ch` trap, and its inverse.** `primer.css` capped the article at `72ch` under a comment claiming 60–72 characters; a `ch` is the font's ZERO glyph and Manrope's is wide, so it rendered about 90 — the biggest reading defect on the page. The measure is `--measure`, spent on **prose children only**: the card itself is uncapped so a table, a figure and a code block may take its full width. The room does **not** redeclare `--measure` at its larger body size — a `ch` scales with the font, so one measured token reads the same at 15px and 17px.
8
+ - **A fallback for a token that cannot be missing is a second, stale source of truth.** `diagram-viewer.css` carried eleven `var(--x, LITERAL)` pairs whose tokens all ship in `style.css`, so the literal never rendered and only encoded drift (its `--ink` fallback said `#2a2a2a` where the sheet ships `#17181a`). Gone with them: two raw shadows, a hand-written `z-index: 9999` (now `--z-diagram-full`, named in the shell ladder beside the new `--z-progress`), and a scroll lock on `body` where the shell's own drawer locks `html`.
9
+
10
+ Also gone, and the one rule in that list: **a page-local `:root` is forbidden both ways** (`roadmap.css` had the hall's only one) — the pack's `:root` is spliced at the end of `<head>` at (0,1,0), so a page's own `:root` neither wins reliably nor loses cleanly.
11
+
12
+ **Two page-head voices, one per archetype, and that is the pick, not drift.** `.page-head--board` is quiet because a working surface's own name is the least interesting thing on it; `.page-head--room` is a lowercase display line over the ground with a one-word eyebrow, because in a reading room the document *is* the page. The roadmap deck takes the **board's** voice — it is a figure surface, not a document. The modifier carries the voice only; the room's WIDTH (`--room-max`, 1010px — past that a reading card is more empty ground than page) lives in `room.css`, so Ranks can take the voice without loading the archetype and never ends up with a 1010px head over 1120px cards.
@@ -0,0 +1,13 @@
1
+ # The record family — the task page, the idea page and the idea composer (tasks 1003305, 1003437, 1003468)
2
+
3
+ **The RECORD family landed next (task 1003305)** — the task page, the idea page and the idea composer. Four things it settled:
4
+
5
+ - **A record is ONE archetype, in one sheet.** `record.css` holds the composition both record pages share (the way back, the kind eyebrow + status pill, the title with its mono id, the figure band, the sections, the dependency columns, the honest not-found heading); `task.css` and `idea.css` hold only what one has and the other does not. The panel is the shell's `.scroll` — v3 has exactly three card levels and a fourth is a design decision, not a local override. The **composer is not a record** and deliberately does not load `record.css`.
6
+ - **`--measure` is the BODY measure and nothing else.** 48ch was measured at the body size (~60 characters); spent on a 34px display heading it caps at ~950px and the title runs the panel's width. A heading's cap is its own em-based one (`24ch` on `.rec-title`). The inverse mistake is fresh `ch` arithmetic in prose, which is what `idea.css`'s old `70ch` was — it rendered ~88 characters.
7
+ - **A page must not reach into shell layout.** `ideas.css` set `.app-content > main { padding-right: 20rem }` and pinned its meter at a hand-copied `top: 6.5rem`. Both halves are the shell's now — `.app-content--railed` reserves the room and `.app-rail` places the panel, measured from `--rail-right-w` and `--topbar-h` so they cannot desync — and a page **opts in from its markup**. Don't re-paste the override.
8
+ - **`.btn` / `.btn--ghost` had no rule anywhere in the surface.** Six pages (ideas, idea, copy-desk, my-projects, collab, and the goals inbox) were rendering browser-default controls. They are defined in `style.css` now, saying what `.btn-accent` / `.btn-ghost` say, because the names are spelled into markup and into `tests/idea_spark_hall.mjs`. `.btn`'s edge is a **transparent** 1px, because `collab.css`'s `.btn--quiet` sets only a border-colour and a modifier cannot paint an edge that was never reserved.
9
+ - **Two rules the removal list left behind.** No `#fff` literal beside a token colour — that is the pairing `DESIGN.md` bans (it was on `.td-btn--primary`). And every page needs its `<h1>` **including in the not-found and error renders**, because `one-header-main-h1` measures the DOM it gets and not the happy path.
10
+
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
+
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`.
@@ -0,0 +1,9 @@
1
+ # The reference room — drachmae, at /drachmae (task 1003473)
2
+
3
+ **The REFERENCE room landed last (task 1003473)** — drachmae, the fifth and smallest redesign task under goal 1000081. It is the reading-room archetype (`room.css`) with ONE body modifier, `room--reference`, and not a second archetype: the room's composition — the head, the glass contents beside the `--bg-card` article, the measure — is right for a document people scan as much as for one they read, so what changes is the content rules (`room.css` §8, ADR 0228). Five things it settled:
4
+
5
+ - **A reference has no end to reach, so the linear-read instruments are off.** No progress hairline (the markup is simply absent; §1 of the sheet stays for the primer, whose gate is fed by it), no section counters (`content: none` on the h2's and the contents' `::before`), no "About a five-minute read". `primer.css` is no longer loaded here: it holds only the acknowledge gate.
6
+ - **The outline goes to h3 depth and is read from the DOM.** `drachmae.js` collects `h2[id], h3[id]` from the rendered article (so `*not*` and a link in a heading arrive as words, and the inserted live section is one more heading in order), nests h3s under their part (`.primer-toc__sub`), and lights the entry AND its part (`.is-trail`). On a phone the collapsed toggle rides under the top bar (`position: sticky` at the room's 760px breakpoint, on the card's ground at 92% because light's glass has no blur and prose showed through the bare tint) and its middle (`#primer-toc-current`) names the part the reader is in.
7
+ - **Every heading is an address.** `.room-anchor` on h2/h3/h4: the heading's own id as an `<a href>`, a 24px box always visible in the faint ink, copying the absolute link on click, the accent when it is the one the reader arrived at. `:target` resolved against nothing on a page whose document renders after load, so `landOnHash()` scrolls to it after the document, the live section and the fonts (a swap moved the landing 300px) and marks it `.is-target` until the next hash change.
8
+ - **The parser reaches h4 and slugs a linked heading by its text.** `md-reader.js` stopped at `###`, so the doc's six earning sources rendered as paragraphs with a literal `####` — the largest defect on the page, invisible to every floor. It is `#{1,4}` now in both regexes, the bold rule is lazy so `**… *list* …**` renders, and a markdown link in a heading slugs by its text. The primer has no `####` and no linked heading, so its render is unchanged; `room.css` gives the h4 a rule (the body face at the body size, one weight step up).
9
+ - **The live rate card is a section ahead of the closing links,** not an appendix after "dig deeper": `insertBountySection` puts it before the h2 matching `/^see it/i` and falls back to appending.
@@ -0,0 +1,9 @@
1
+ # The roster and ledger pages — goals, sessions and people (task 1003470; scouting became /people in task 1003440)
2
+
3
+ **The ROSTER and LEDGER pages landed next (task 1003470)** — goals, sessions, people and scouting. Goals is the archetype page (`docs/design/mocks/hall/goals/v3.html`) and the page the owner was looking at when he called the re-skin a re-skin, so this pass changed its COMPOSITION rather than its tokens. Five things it settled:
4
+
5
+ - **The goals list is ONE panel.** The filter bar, the count line, the readiness band and the groups sit on one `.scroll` (`.goals-panel`); the bar of twelve controls on the ground over a `--bg-card` panel PER GROUP is gone. Three lenses stay in the open (version, state, sort) and the five rarely turned sit behind the kit's Filters disclosure, **which counts and chips whatever is active so a hidden filter can never narrow the board silently.** `rows|cards` is a VIEW, not a filter: a segmented toggle on the count line writes a hidden `#f-goal-view`, so task 1003136's fork in `renderList` and the `otb.goals.view` pref are unchanged; the pressed side takes `--accent-tint` (the tint the world gives a current thing), **never the accent fill.**
6
+ - **Three languages on a row, not four.** The ring is the goal's own done-when (satisfied over total, **the arc in INK — the accent lights the pressable and a progress arc is neither**); the facts line is every label a goal carries; the right column is ONE thing — `claim next` as the shell's `.btn-accent` (**the row's only accent**), `what's blocking` as a ghost pill, or a one-line state with a drawn check. The readiness is the family's `.ov-figs` band with the roll line's own `.gr-mark` dots (**classes, not inline styles**), so the band and the rows read as one language.
7
+ - **The card is the row folded.** The toggle stays because the mock keeps it (a card wall is the better read when scanning for a NAME), but a card inside the panel draws no box — a `--bg-card` tile on a `--bg-card` panel is the card-in-a-card v3 removed — so `.goal-grid` paints the hairline BETWEEN its rows (a 1px row gap over `--rule-soft`, each cell on the panel's ground) and every cell renders the same facts from `goalRenderFacts`.
8
+ - **People and scouting are ONE page.** `/scouting` was the legacy door to the same `/api/gds/scouting` rollup, drawn a third way; it now renders `people.html`'s body and `scouting.js` is deleted. **Task 1003440 then deleted the door itself**: `scouting.html` and `scouting.states.json` are gone, the `SCOUTING_PAGE_RE` page gate with them, and `/builders/scouting` now 301s to `/people` (`SCOUTING_LEGACY_RE` in `serve-internal.js`) so an old bookmark still lands on the directory. `tests/hall_roster_world.mjs` pins both halves — the files are absent, and the redirect exists with no dead gate behind it. On the row the name is the shell's `.lb-name` to the builder's **GitHub** page — the one door every listed builder has, since the directory is cross-project and no local profile id exists. The gate copy is written from `OTB.rankLabel('metic')`; it used to say "Archon-only", wrong since ADR 0210.
9
+ - **The session ledger's nowrap + wrapper pair was a live bug.** `sessions.css` forced `white-space: nowrap` on every cell, `.ledger-table-wrap` existed only to give that a scrollport, and the shell's sticky `th` then stuck to the WRAPPER — the column heads rendered over the first row on desktop. Both are gone; the shell's card strip handles a phone as on every other ledger. **Every id on a row is a door** (the session id was painted in `--info` — a link to nowhere); the row shows what the session EARNED; the totals ride the shell's top strip (`.ov-summary`) with **six fixed columns so the sixth tile is never stranded on its own row**. The hand-rolled server-side pager is untouched (the sanctioned exception below).
@@ -0,0 +1,23 @@
1
+ # The sky — /thinking and /goal-map (tasks 1004231, 1004232, 1004233)
2
+
3
+ **THE SKY landed at `/thinking` (task 1004231 / sky part 3 of 5, goal 1000095; [ADR 0331](../../../docs/adr/0331-the-sky-is-the-ideators-home.md), ADR 0227 §11)** — the ideator's home. It replaced BOTH the room (task 1004098, `/thinking`) and the first ambient sky (task 1004105, `/sky` — the URL 301s here now) with the prototype the owner adopted as the design of record: `docs/design/mocks/sky/sky.template.html` is the **pixel contract** — copy, layout, colours, the thirteen tour steps, the toggles, the keys (`L` list, `?` tour, `0` fit, `/` compose) — built for real over `GET /sky` and `GET /sky/mine` (part 2). The room survives inside it as the list drawer: every drawn thought is a row with its whole sentence, one key away. Eight things it settled:
4
+
5
+ - **The page has no hall chrome, on purpose.** `thinking.html` is the template's markup (no shell.js, no sidebar); `thinking.css` is its `<style>`, one file; the world's tokens are not loaded — the template's values ARE the contract. The hall's doors (the record, the goal, the task) open from the panel, resolved against `hallBase()` (apex `/builders` or the subdomain). The title stamps `{{worldName}}` like every hall page.
6
+ - **One script became seven files under the 1,500-line ratchet, on ONE shared object.** `window.OTBSky`: `SKY.S` is every `let` the prototype's closure held; everything else is a function or a constant. Load order is pinned in the HTML — `sky-data.js` (helpers, the vocabulary, the two adaptors, the bands, the world, the thoughts, the five reads) → `sky-layout.js` (clock, routes, explore, camera) → `sky-draw.js` (the drawing, the hit test) → `sky-interaction.js` (card, menu, pointer, keys, lenses, stats, jump, legend) → `sky-panel.js` (the panel, the drawer) → `sky-composer.js` (gravity, quick + full filing, the nod) → `sky-tour.js` (the thirteen steps, the frame loop, the boot). A file only CALLS a later file through `SKY.name(...)`, never at load time. The README says a split is not a deviation; a changed behaviour inside one is — and every deviation a gate or a route forced is listed in the task's ship notes, one line each.
7
+ - **The bands are DERIVED from `GET /versions`, never named by hand.** The prototype listed this instance's version ids; the core cannot (a scaffolded instance would have every goal outside every band and crash on `g.band`). `bandsFor`: the versions still building sit at the centre (the present), shipped versions ring outward newest first, and small versions share a band until it holds six goals so no ring is razor-thin — the grouping the prototype drew by eye. "Trace only the latest" reads the present band's versions for the same reason. `tests/hall_sky.mjs` pins that no `sky-*.js` file names a version.
8
+ - **The two heuristic layers are LEDGER FACTS since part 5** (task 1004233; the README's "must not ship" rows, shipped). `filed_via` rides on every `GET /sky/mine` row (`hall | cli | discord | planning | api`, or NULL for a row older than the column): `sky-data.js` `SESSION_VIAS` puts cli / api / discord behind the "claude layer" pill (off by default), hall and planning always show, and a NULL row shows by default (the owner's rule) with the panel saying "filed before the ledger recorded where" — nothing is guessed from text. Born-of marks read `S.BORN`, filled from `GET /sky`'s `born` (the ideas module's read of the ratify tables through the `ideas.bornOf` port): a goal born of a thought wears the nucleus ring, a ring born of one the accent notch, and the thought sits at the well's core or on its ring. An absent read is an empty map and no mark, never a heuristic.
9
+ - **Gravity is the real verdict, and the filing is the hall form's own body.** Typing runs the composer's debounce (300ms) → `GET /goals/suggestions` per building version (the hall composer's read) → `POST /inbox/preview-landing` over the pick; the sentence READS the verdict and decides nothing (a `floats` verdict lights no well however well the words match). Quick files `{ title, body_md: '', kind: 'feature' }` plus the `goal_id` the verdict named; full files `{ title, grade: 'full', template_key, field_values }` — the five questions are `GET /inbox/template`'s, asked one at a time after the line, with skip and cancel, never a copy kept in the page (`tests/idea_hall_composer.mjs`'s rule). A filing that fails puts the line back and says why in the receipt. Without a template read the "full idea" switch is hidden: there is nothing to file as.
10
+ - **The nod is drawn only for `idea.triage`, and runs the existing promote path.** `/me` now resolves that atom (`NAV_PERMISSION_ATOMS`, `src/bongos/routes/me.js`); the panel draws "nod — make it claimable" on a protostar in a well awaiting a nod, and pressing it is `POST /tasks/:id/promote` on the routed task in the thought's lineage — which stays the wall (it gates on `task.promote`; both floors are metic under the compat seed).
11
+ - **A title never becomes markup.** The prototype builds HTML strings; every title, criterion and goal name passes through `esc()` on the way in, and `tests/hall_sky.mjs` runs a hostile title through the drawer, the panel and the hover card.
12
+ - **THE THREE ANSWERS STAY THREE, and size is one curve.** `diameter()` is affine in the server's `scale` (6 + 22·scale) and nothing else — a remnant is as wide as its work was; an unreadable brightness (`attributable: false`) leaves the star at `lit: 1` and says "still alive — unknown"; a thought with no work says nothing about what survived. `tests/ideator_large_dead_thought_proof.mjs` runs the real size and state hops into this page and reads the diameter and the light back.
13
+
14
+ Pinned by `tests/hall_sky.mjs` (31 cases: the reads, the five objects, the two channels, the wells, the drawer, empty and failed skies, the two hidden layers, the bands, the tour and the keys) and `tests/hall_thinking_room.mjs` (18: the drawer's rows and filters, quick and full filing byte for byte, gravity, the nod), both booting the real page through `bootSkyPage` in `tests/helpers.mjs` — the page's OWN markup parsed into a mini-DOM with a stub canvas, so a lost button fails a test. **The harness fixtures are `sky.json` + `sky__mine.json`** (`scripts/hall-preview/fixtures/`): the wells are `goals__graph.json`'s nodes with the criteria the per-goal fixtures carry, and the thoughts are `inbox__by-builder.json`'s rows, so the drawer and the old room can never disagree about a thought. Against the kit's own stub neither read exists, so `thinking.states.json` renders the EMPTY sky — a real state — and skips the first-visit tour first.
15
+
16
+ **THE PROJECT SKY landed at `/goal-map` (task 1004232 / sky part 4 of 5, goal 1000095; ADR 0331, ADR 0227 §11)** — the same page as `/thinking`, opened in its "the whole project" lens, and it REPLACED the goal map that held the URL (task 1003479: the layered node-link picture, the stand-alone list and the table — `goal-map.{html,css,js}` + `goal-map-lib.js` and `tests/goal_map_lib.mjs` are gone). It holds the nav slot the map was denied — "Sky", directly under Goals, gate `member` (owner, 2026-09-24; ADR 0227 §11 is the licence) — and Goals' own "see which goals wait on each other" link still lands here. Six things it settled:
17
+
18
+ - **One page, two doors.** `goal-map.html` is `thinking.html`'s markup, verbatim but for the body (`data-page="goal-map" data-sky-lens="all"`), the brand line and the switcher starting in the whole-project lens, and the canvas's own words. The sheet (`thinking.css`, which names both bodies) and the seven `sky-*.js` files are SHARED, never copied: `S.home` (read once in `sky-data.js` from `data-sky-lens`) is the lens `sky-tour.js` boots into and hands back to after the tour, and `sky-interaction.js`'s switcher LEAVES for the other page when the lens asked for lives there — "the whole project" goes to `/goal-map` from `/thinking`, "simple" and "your thinking" go to `/thinking` from here. Every lens still exists inside both pages because the tour walks all three. One cache-bust between them (`?v=…-sky4` on both).
19
+ - **What the lens draws is the template's, from the same reads.** Wells sized by credits (settled 0.8×, collapsed 0.5×, both keeping their mass), alive as MOTION (a builder holding a task now, or more than three landing this week — never status), routes as rim arrows charted by a shuttle that pulls the camera back to hold both ends, the shuttle's five-line menu (every route from this goal · every route in the sky · only the latest · hide unrelated · land every shuttle), the clock, band click to fit. `GET /goals/graph` is no longer read by any page — the sky's own reads carry the wells and the routes.
20
+ - **Explore is on by default, and "my ideas" is off.** A first visit asks "where do you begin" (where my thoughts are · one door per open well · show me everything); a goal is revealed when you begin in it or chart a route to it, and the ledger, the filters and the search know only what you have reached (`sky-seen`, `sky-explore` in localStorage — shared with `/thinking`, since it is one sky). No thought is laid over the project until the "my ideas" pill is pressed; "what if" waits behind it.
21
+ - **THE LEDGER DRAWER IS THE ACCESSIBLE PATH** — the old goal map's table, kept (ADR 0227 §11): `L` opens it; every goal is a row that counts both directions of its dependencies ("waits on N · waited on by N") and the panel names them as doors; the filters are alive / wells / settled / collapsed / with routes. Every drawn thing is a row, one key away — spec decision 11's surviving rule.
22
+ - **ADR 0227 §1's refusal stands.** Tasks resolve onto their rings only inside ONE well past the zoom threshold (dust density on the rings below it), never for every goal at once — at the whole-sky fit no task is a body — and there is no "show every task" control anywhere on the page or in the shuttle menu. `tests/goal_map_page.mjs` pins both, plus the nav slot, the two doors, the wells, alive, the routes, the menu, explore, "my ideas", the ledger, the clock, the bands, the tour and a hostile title.
23
+ - **Same tier, same gates as `/thinking`.** `requireBuilderPage` (the tier the map sat on); no hall chrome (`STANDALONE_EXEMPT` in `tests/hall_audit.mjs`); `goal-map.states.json` renders the EMPTY sky against the kit stub like `thinking.states.json`, and the populated sky against the harness (`sky.json` + `sky__mine.json`). `goal-forest-*` (the per-goal forest on the goal detail page) is untouched.
@@ -157,8 +157,14 @@ async function getLastClaimHolder(taskId) {
157
157
  // /grade-recover reads a failed grade as having no findings at all. Lifting is
158
158
  // additive: `signals.issues` stays exactly where it is for anything already
159
159
  // reading it.
160
- async function getTaskGrade(taskId) {
161
- const { rows } = await pool.query(
160
+ //
161
+ // `deps.pool` is the family's test-only pool seam (task 538), honoured ONLY under
162
+ // NODE_ENV==='test' (the same line moduleScopeForActiveClaims uses above), so
163
+ // tests/grade_diff_hash.mjs can drive the lift with a fake row instead of a
164
+ // real Postgres (task 1003490). Production callers pass the task id alone.
165
+ async function getTaskGrade(taskId, deps = {}) {
166
+ const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
167
+ const { rows } = await activePool.query(
162
168
  `SELECT diff_hash, passed, score, threshold, grader_kind, grader_version,
163
169
  rubric_scores, signals, notes_md
164
170
  FROM task_grades
@@ -21,7 +21,8 @@
21
21
  // `node scripts/gds/audit-deps.js --min high` as one more housekeeping
22
22
  // step (idea 1000276's half — see .claude/skills/goal-review/SKILL.md).
23
23
  //
24
- // HARD CONSTRAINT, same as goal-advisory.js and ADR 0162: advisory only. This
24
+ // HARD CONSTRAINT, same as goal-advisory.js: advisory only (a finding addressed
25
+ // to a human never acts by itself — ADR 0158 §2). This
25
26
  // must never refuse a POST, and `buildDependencyAdvisory` swallows its own
26
27
  // failures for the same reason goal-advisory.js's does — the task row is
27
28
  // ALREADY COMMITTED by the time this runs, so a throw here would turn a
@@ -15,6 +15,9 @@
15
15
  // touches[] was empty, leaving empty-touches tasks unprotected — with this
16
16
  // plain clean-tree check. Strictly stronger; untracked / .gitignored files
17
17
  // are ignored so autonomous sessions with scratch files aren't refused.)
18
+ // Ignored, but no longer UNMENTIONED: untrackedLeftBehind +
19
+ // untrackedWarningLines (task 1001436) name the untracked files a ship
20
+ // with committed work is about to leave behind, as a warning only.
18
21
  //
19
22
  // 2. cleanupMacFinderDupRefs — find + delete `.git/refs/.../FOO 2` files
20
23
  // that macOS Finder / iCloud creates when it duplicates a regular file
@@ -96,6 +99,91 @@ function classifyEmptyShip(porcelainOut, { allowEmpty = false } = {}) {
96
99
  return { refuse: true, reason: 'nothing-to-ship', uncommitted: [] };
97
100
  }
98
101
 
102
+ // ---- The RESIDUAL phantom ship (task 1001436) --------------------------------
103
+ //
104
+ // classifyEmptyShip closes the case where NOTHING is committed and the
105
+ // deliverable sits uncommitted. It cannot see the weaker case beside it: the
106
+ // builder commits real work, so the branch is ahead of main and the ship goes
107
+ // ahead, but one more NEW file was never `git add`ed. The merge carries only
108
+ // committed history, so that file silently does not ship — the task reads
109
+ // "shipped" with part of its artifact missing, and nothing said so.
110
+ //
111
+ // The answer is a loud WARNING, never a refusal. trackedDirty ignores untracked
112
+ // files on purpose: autonomous sessions leave scratch in the tree, and refusing
113
+ // on it would false-refuse real ships. So this only names the files — before the
114
+ // grade and the merge, while stopping is still free.
115
+ //
116
+ // SCOPE. The filing suggested narrowing the list to touches[] or to docs/ src/
117
+ // scripts/. Neither is used, because each hides exactly the file this exists to
118
+ // catch: touches[] is advisory and often empty, and a forgotten file is by
119
+ // definition one nobody thought to declare; a directory allowlist misses tests/,
120
+ // modules/ and migrations/, where a forgotten new file most often lives (a new
121
+ // test beside a committed fix). The one SYSTEMATIC source of noise is the ship's
122
+ // own input — the builder-ship skill writes notes.md + summary.txt into the tree
123
+ // and passes them as --notes-file / --summary-file — so those are excluded
124
+ // exactly, by path, via `inputs`. Surveyed before choosing (2026-09-25, 16 live
125
+ // worktrees): 12 untracked entries, none of them root-level scratch, and 5 under
126
+ // tests/ or modules/ — where a docs/ src/ scripts/ allowlist would have hidden them.
127
+
128
+ // Pure: the untracked ('??') paths in raw `git status --porcelain` output, in
129
+ // output order. .gitignored files never appear without --ignored, so this is
130
+ // exactly "untracked and not ignored". Quoted paths are unquoted the same way
131
+ // parsePorcelain does it.
132
+ function untrackedPaths(porcelainOut) {
133
+ if (!porcelainOut) return [];
134
+ const files = [];
135
+ for (const raw of porcelainOut.split('\n')) {
136
+ if (raw.slice(0, 2) !== '??') continue;
137
+ const file = raw.slice(3).replace(/^"|"$/g, '');
138
+ if (file) files.push(file);
139
+ }
140
+ return files;
141
+ }
142
+
143
+ // One spelling for a repo-relative path, so an input given as `./notes.md` or
144
+ // with Windows separators still matches porcelain's `notes.md`.
145
+ function normalizeRelPath(p) {
146
+ return String(p || '').replace(/\\/g, '/').replace(/^(\.\/)+/, '');
147
+ }
148
+
149
+ // Pure: which untracked files will be LEFT BEHIND by this ship.
150
+ // committedFileCount — files in the branch's committed diff vs the grader
151
+ // baseline. Zero returns [] on purpose: with nothing committed,
152
+ // classifyEmptyShip already REFUSES and lists these same files, and a second
153
+ // message about them would only compete with the refusal.
154
+ // inputs — repo-relative paths the ship itself was handed (notes, summary,
155
+ // response, visual). They are inputs to the ship, not deliverables.
156
+ function untrackedLeftBehind(porcelainOut, { committedFileCount = 0, inputs = [] } = {}) {
157
+ if (!(Number(committedFileCount) > 0)) return [];
158
+ const skip = new Set((inputs || []).map(normalizeRelPath).filter(Boolean));
159
+ return untrackedPaths(porcelainOut).filter((f) => !skip.has(normalizeRelPath(f)));
160
+ }
161
+
162
+ // Pure: the warning block for untrackedLeftBehind's result; [] when there is
163
+ // nothing to say. `cap` bounds the listing so a tree full of scratch cannot bury
164
+ // the rest of the ship output. The wording holds on all three preflight callers
165
+ // (first ship, --regrade, the confirmed-task resume): each runs this before it
166
+ // resolves, grades or merges anything.
167
+ function untrackedWarningLines(files, { taskId = null, cap = 15 } = {}) {
168
+ if (!files || files.length === 0) return [];
169
+ const n = files.length;
170
+ const task = taskId != null && taskId !== '' ? ` with task ${taskId}` : '';
171
+ const lines = [
172
+ '',
173
+ `⚠ Untracked-file warning: ${n} untracked file(s) in this worktree will NOT ship${task}:`,
174
+ ];
175
+ for (const f of files.slice(0, cap)) lines.push(` - ${f}`);
176
+ if (n > cap) lines.push(` … and ${n - cap} more`);
177
+ lines.push(
178
+ ' The merge carries committed work only, so these stay behind.',
179
+ ' If one of them belongs to this task: stop now (Ctrl-C — nothing has been resolved at this step),',
180
+ ' commit it, and re-run. If they are scratch, ignore this.',
181
+ ' Warning only — not blocking; the ship proceeds.',
182
+ '',
183
+ );
184
+ return lines;
185
+ }
186
+
99
187
  function gitDirFor(cwd) {
100
188
  // Resolve the *actual* refs directory for a given worktree. In a linked
101
189
  // worktree, `.git` is a file containing `gitdir: <path>` that points at
@@ -218,6 +306,9 @@ module.exports = {
218
306
  parsePorcelain,
219
307
  trackedDirty,
220
308
  classifyEmptyShip,
309
+ untrackedPaths,
310
+ untrackedLeftBehind,
311
+ untrackedWarningLines,
221
312
  cleanupMacFinderDupRefs,
222
313
  gitDirFor,
223
314
  findMainWorktree,
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1031",
3
+ "version": "1.19.1033",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1031",
9
+ "version": "1.19.1033",
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.1031",
3
+ "version": "1.19.1033",
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",