@bongos/core 1.19.457 → 1.19.458

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
@@ -127,7 +127,7 @@
127
127
  │ ├── sessions/ ← CORE-DOMAIN carve (BV1.R82, default-on): the session_records corpus + data plane (upload, cross-builder search/deep-dive), the BFG read-the-room scorer, session-pulse/stale-claim helpers. Registers no port — reached via HTTP POST to its own /sessions route; resolves the economy `reward` port for the cost-plus token reward. CLAUDE.md, module.json, routes/sessions.js, bfg.js, pulse.js.
128
128
  │ ├── autonomy/ ← CORE-DOMAIN carve (BV1.R83, default-on): the MAS autonomy surfaces — the $0 deterministic autonomy gate (System 3, inert until armed), the conductor/architect CLIs, and the in-process routine timers (incl. the weekly peer-vote-tally poller). Registers no port; resolves the lifecycle `tallyPeerVotes` doorway. CLAUDE.md, module.json, routes/autonomy.js, autonomy.js.
129
129
  │ ├── status-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R84, default-on): the public status dashboard at status.<apex>, moved from public-status/. NO routes/db/port — declares contributes.webSurfaces:[{host:"status.",dir:"public"}], served by the loader web-surface seam (moduleWebSurfaces → serve-internal). CLAUDE.md, module.json, public/{index.html,status.js,style.css,cursors/,snapshots/}.
130
- │ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Two ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305) and `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). A page sheet then holds only what one page has and the other does not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip.
130
+ │ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Three 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) and `oversight.css` (the OVERSIGHT family — government, watch, harbor, gate, task 1003469: the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface). 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.
131
131
  │ ├── 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).
132
132
  │ ├── 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. CLAUDE.md, module.json, registry.js, flags.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
133
133
  │ ├── 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), 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. Reserved home styles/ (1003323) with a README. config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
@@ -1351,5 +1351,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1351
1351
  landed since 1.19.455 with no explicit bump. run 33560584235. (task 1002620)
1352
1352
  1.19.457 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1353
1353
  landed since 1.19.456 with no explicit bump. run 33561330390. (task 1002620)
1354
+ 1.19.458 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1355
+ landed since 1.19.457 with no explicit bump. run 33566876510. (task 1002620)
1354
1356
  ---------------------------------------------------------------------------
1355
1357
  ```
@@ -1,93 +1,25 @@
1
- /* gate.css — Page-local styles for the Gate page (Archon-only).
2
- Lifted verbatim out of the page's inline <style> block (task 1002176);
3
- the ?v= cache-bust is stamped automatically from this file's content hash
1
+ /* gate.css — Page-local styles for the Gate (task 1003469 rebuild).
2
+ One panel of oversight rows, one held pull request per row, over the shell's
3
+ glass summary strip; everything structural is oversight.css. What is the
4
+ gate's own: the hold reasons (a hard floor reads in the danger ink), and the
5
+ changed-file run. The ?v= cache-bust is stamped from this file's content hash
4
6
  at boot (src/asset-version.js). */
5
7
 
6
- /* Page-local styles for the Gate — approvals for held pull requests. Built
7
- on the shared design tokens (style.css): each held PR is a card with a
8
- verdict-colored top band and per-file reasons for the hold. */
9
- .gate-summary {
10
- display: grid;
11
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
12
- gap: 14px;
13
- margin: 6px 0 22px;
14
- }
15
- .gate-stat {
16
- border: var(--line) solid var(--rule-soft); border-radius: var(--radius-sm);
17
- background: var(--bg-card); padding: 12px 14px;
18
- }
19
- .gate-stat__label {
20
- font-size: 11.5px; font-weight: 600;
21
- letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-soft);
22
- }
23
- .gate-stat__value {
24
- font-size: 1.6rem; font-weight: 600; color: var(--ink);
25
- font-variant-numeric: tabular-nums; margin-top: 2px;
26
- }
8
+ /* A hold reason: the path in the machine's face, the reason beside it. A hard
9
+ floor is the danger ink on the path and a name on the line, never a flag glyph. */
10
+ .gate-reason .ov-key { max-width: 40%; }
11
+ .gate-reason--floor .ov-key { color: var(--danger); }
12
+ .gate-reason__floor { font-family: var(--label-face); font-size: 10.5px; letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--danger); white-space: nowrap; }
27
13
 
28
- .gate-list { display: flex; flex-direction: column; gap: 16px; }
14
+ /* The changed files: one chip per path, wrapping, the overflow named as a count. */
15
+ .gate-files { display: flex; flex-wrap: wrap; gap: 6px 8px; margin: 12px 0 0; }
16
+ .gate-files .fact-chip { text-transform: none; letter-spacing: normal; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
17
+ .gate-files__more { align-self: center; font-size: 12px; color: var(--ink-faint); }
29
18
 
30
- .gate-crate {
31
- border: var(--line) solid var(--rule-soft); border-radius: var(--radius);
32
- background: var(--bg-card); overflow: hidden;
33
- }
34
- /* Verdict band along the top, painted by the held-verdict. */
35
- .gate-crate__seal { height: 4px; background-color: var(--danger); }
36
- .gate-crate--needs_trust .gate-crate__seal { background-color: var(--warn); }
37
- .gate-crate__body { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 10px; }
38
- .gate-crate__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
39
- .gate-crate__title { font-weight: 600; color: var(--ink); font-size: 15px; }
40
- .gate-crate__title a { color: inherit; text-decoration: none; }
41
- .gate-crate__title a:hover { text-decoration: underline; }
42
- .gate-crate__meta { color: var(--ink-soft); font-size: 12.5px; }
43
- .gate-crate__meta a { color: var(--accent-ink); }
44
-
45
- /* Base: the shell's .fact-pill (task 1003307). States are hairlines, not fills. */
46
- .gate-pill--escalate { color: var(--danger); }
47
- .gate-pill--needs_trust { color: var(--warn); }
48
- .gate-pill--trusted { color: var(--info); }
49
-
50
- .gate-reasons { list-style: none; padding: 0; margin: 0; }
51
- .gate-reasons li {
52
- padding: 6px 0; border-bottom: var(--line) solid var(--rule-soft);
53
- font-size: 13px; color: var(--ink);
54
- }
55
- .gate-reasons li:last-child { border-bottom: 0; }
56
- .gate-reasons code {
57
- font-family: var(--font-mono); font-size: 12px;
58
- color: var(--accent-ink); background: var(--glass);
59
- padding: 1px 5px; border-radius: 4px;
60
- }
61
- .gate-reasons .floor { color: var(--danger); font-weight: 600; }
62
-
63
- .gate-files { font-size: 12.5px; color: var(--ink-soft); }
64
- .gate-files code { font-family: var(--font-mono); }
65
-
66
- .gate-actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 2px; }
67
- .gate-btn {
68
- font: inherit; font-size: 13px; font-weight: 600;
69
- padding: 7px 16px; border: var(--line) solid var(--accent);
70
- background: var(--accent); color: var(--accent-on);
71
- border-radius: var(--radius-sm); cursor: pointer;
72
- }
73
- .gate-btn:hover:not(:disabled) { background: var(--accent-lift); border-color: var(--accent-lift); }
74
- .gate-btn:disabled { opacity: 0.45; cursor: default; }
75
- .gate-btn--ghost { background: var(--bg-card); color: var(--ink); border-color: var(--rule); }
76
- .gate-btn--ghost:hover:not(:disabled) { background: var(--glass); border-color: var(--rule); }
77
- .gate-approved-note { color: var(--ok); font-size: 13px; }
78
-
79
- .gate-empty, .gate-sealed { color: var(--ink-soft); padding: 16px 0; }
80
- .gate-checking { color: var(--ink-faint); font-size: 12.5px; margin-top: 10px; }
81
-
82
- /* Lightweight confirm modal (mirrors harbor). */
83
- .gate-modal-bg {
84
- position: fixed; inset: 0; background: color-mix(in srgb, var(--ink) 40%, transparent);
85
- display: flex; align-items: center; justify-content: center; z-index: var(--z-modal); padding: 20px;
86
- }
87
- .gate-modal {
88
- background: var(--bg-card); border: var(--line) solid var(--rule); border-radius: var(--radius);
89
- max-width: 480px; width: 100%; padding: 20px 22px; box-shadow: var(--shadow-pop);
19
+ /* On a phone the path takes the line with its floor label at the right, and the
20
+ reason drops under it in full width. */
21
+ @media (max-width: 640px) {
22
+ .gate-reason .ov-key { max-width: none; }
23
+ .gate-reason > span:nth-of-type(2) { flex: 1 1 100%; order: 3; }
24
+ .gate-reason__floor { margin-left: auto; }
90
25
  }
91
- .gate-modal h2 { color: var(--ink); margin: 0 0 8px; font-size: 17px; font-weight: 600; }
92
- .gate-modal p { color: var(--ink); font-size: 13.5px; margin: 0 0 12px; }
93
- .gate-modal__actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; }
@@ -9,6 +9,7 @@
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
+ <link rel="stylesheet" href="/builders/oversight.css">
12
13
  <link rel="stylesheet" href="/builders/gate.css">
13
14
  </head>
14
15
  <body data-page="gate" data-page-title="Gate">
@@ -19,25 +20,35 @@
19
20
  <div class="app-body">
20
21
  <header class="app-topbar" id="app-topbar"></header>
21
22
  <div class="app-content">
22
- <!-- task 1003307: the board page head. Gate shipped with NO <h1> at all
23
- (one-header-main-h1 measured 0 on every render), so the re-skin
24
- carries the structural fix rather than porting the hole. The heading
25
- is the LABEL voice, not a display line: on a working surface the
26
- page's own name is the least interesting thing on it. -->
23
+ <!-- The board page head (task 1003307): the LABEL voice, not a display
24
+ line, because on a working surface the page's own name is the least
25
+ interesting thing on it. -->
27
26
  <div class="page-head page-head--board">
28
27
  <h1 class="page-head__h">Gate</h1>
29
28
  <p class="page-head__sub" id="gate-sub">Loading…</p>
30
29
  </div>
31
30
 
32
31
  <main id="hall-main">
33
- <p class="gate-sealed" id="gate-sealed" hidden>Archons only.</p>
32
+ <!-- task 1003469: the sealed state is a panel with a heading, not a bare
33
+ line on the ground. gate.js writes the real floor into the lede from
34
+ the pack's rank label, so the copy cannot drift from ADR 0157. -->
35
+ <section class="scroll" id="gate-sealed" aria-labelledby="gate-sealed-h" hidden>
36
+ <h2 class="scroll__h" id="gate-sealed-h">Restricted</h2>
37
+ <p class="scroll__lede" id="gate-sealed-body">This page opens at a higher rank.</p>
38
+ </section>
34
39
 
35
40
  <section id="gate-body" hidden>
36
- <div class="gate-summary" id="gate-summary"></div>
37
- <div class="gate-list" id="gate-list">
38
- <p class="gate-empty">Loading held pull requests…</p>
39
- </div>
40
- <p class="gate-checking" id="gate-checking" hidden></p>
41
+ <!-- The glass summary strip (the shell's .stat-row), then ONE panel of
42
+ oversight rows, one held pull request each — the bordered crate
43
+ with a verdict band along its top is gone (task 1003469). -->
44
+ <div class="stat-row ov-summary" id="gate-summary" aria-label="Held changes summary"></div>
45
+
46
+ <section class="scroll" aria-labelledby="gate-list-h">
47
+ <h2 class="scroll__h" id="gate-list-h">Held pull requests</h2>
48
+ <p class="scroll__lede">Each one touched a protected surface and stopped at the gate check. Approving records your seal on that commit and re-runs the check.</p>
49
+ <div id="gate-list"><span class="kit-skel"></span></div>
50
+ <p class="ov-foot" id="gate-checking" hidden></p>
51
+ </section>
41
52
  </section>
42
53
  </main>
43
54
 
@@ -53,6 +64,7 @@
53
64
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
54
65
  <script src="/builders/palette.js" defer></script>
55
66
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
56
- <script src="/builders/gate.js?v=2026-07-03-gate-shell"></script>
67
+ <script src="/builders/hall-kit.js"></script>
68
+ <script src="/builders/gate.js?v=2026-09-02-gate-rows"></script>
57
69
  </body>
58
70
  </html>
@@ -3,9 +3,9 @@
3
3
  // Archon-only approval view for gate-surface PRs (task 1179, ADR 0058 amendment).
4
4
  // A pull request that touches the build's protected machinery is held by the
5
5
  // gate-review CI check until the owner approves. This page lists every such PR
6
- // with WHY it was stopped (the same classifier the CI runs), and an Approve
7
- // button that records the owner's seal (a server-set gate-owner-approved status)
8
- // and re-runs the check so the PR merges.
6
+ // as one oversight row (task 1003469) with WHY it was stopped (the same
7
+ // classifier the CI runs), and an Approve button that records the owner's seal
8
+ // (a server-set gate-owner-approved status) and re-runs the check so the PR merges.
9
9
  //
10
10
  // Like The Watch + The Harbor, this reads /api/bongos/me to show a Sealed notice for
11
11
  // non-Archons — DEFENCE IN DEPTH only. GET /gate-approvals + POST
@@ -21,6 +21,8 @@
21
21
  // through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
22
22
  const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
23
23
  const { escapeHtml, fmtInt } = window.OTB;
24
+ const { emptyStateHtml } = window.OTBKit;
25
+ const rankLabel = (rank) => window.OTB.rankLabel(rank, { empty: '' });
24
26
  const toast = (msg) => window.OTB.toast(msg);
25
27
 
26
28
  // ---- fetch helpers (mirror harbor.js / watch.js) -------------------------
@@ -46,8 +48,10 @@
46
48
  const shortSha = (s) => (typeof s === 'string' ? s.slice(0, 7) : '');
47
49
  const VERDICT_LABEL = { escalate: 'Hard floor', needs_trust: 'Needs trust', pass: 'Clear' };
48
50
  const verdictLabel = (v) => VERDICT_LABEL[v] || (v || 'held');
51
+ // How many files a row shows before it names the rest as a count.
52
+ const FILES_SHOWN = 12;
49
53
 
50
- // ---- modal (mirrors harbor) ----------------------------------------------
54
+ // ---- modal (the oversight family's, oversight.css) -----------------------
51
55
 
52
56
  function closeModal() {
53
57
  const root = document.getElementById('gate-modal-root');
@@ -58,20 +62,23 @@
58
62
  const root = document.getElementById('gate-modal-root');
59
63
  root.innerHTML = '';
60
64
  const bg = document.createElement('div');
61
- bg.className = 'gate-modal-bg';
65
+ bg.className = 'ov-modal-bg';
62
66
  bg.innerHTML =
63
- `<div class="gate-modal" role="dialog" aria-modal="true">
64
- <h2>${escapeHtml(title)}</h2>
67
+ `<div class="ov-modal" role="dialog" aria-modal="true" aria-labelledby="gate-modal-h">
68
+ <h2 class="scroll__h" id="gate-modal-h">${escapeHtml(title)}</h2>
65
69
  ${bodyHtml}
66
- <div class="gate-modal__actions">
67
- <button class="gate-btn gate-btn--ghost" data-act="cancel">Cancel</button>
68
- <button class="gate-btn" data-act="confirm">${escapeHtml(confirmLabel)}</button>
70
+ <div class="ov-modal__actions">
71
+ <button type="button" class="btn-ghost" data-act="cancel">Cancel</button>
72
+ <button type="button" class="btn-accent" data-act="confirm">${escapeHtml(confirmLabel)}</button>
69
73
  </div>
70
74
  </div>`;
71
75
  bg.addEventListener('click', (e) => { if (e.target === bg) closeModal(); });
76
+ bg.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
72
77
  bg.querySelector('[data-act=cancel]').addEventListener('click', closeModal);
73
78
  bg.querySelector('[data-act=confirm]').addEventListener('click', () => onConfirm(bg));
74
79
  root.appendChild(bg);
80
+ const confirm = bg.querySelector('[data-act=confirm]');
81
+ if (confirm) confirm.focus();
75
82
  }
76
83
 
77
84
  // ---- actions -------------------------------------------------------------
@@ -79,13 +86,13 @@
79
86
  function confirmApprove(pr) {
80
87
  const floors = (pr.reasons || []).filter((r) => r.hard_floor).map((r) => r.path);
81
88
  const what = floors.length
82
- ? `the hard-floor surface ${floors.slice(0, 3).map((p) => `<code>${escapeHtml(p)}</code>`).join(', ')}`
89
+ ? `the hard-floor surface ${floors.slice(0, 3).map((p) => `<span class="ov-key">${escapeHtml(p)}</span>`).join(', ')}`
83
90
  : 'a protected build surface';
84
91
  openModal({
85
92
  title: `Approve PR #${pr.number}?`,
86
93
  bodyHtml:
87
94
  `<p>Record your owner approval for <strong>${escapeHtml(pr.title || `PR #${pr.number}`)}</strong>${pr.task ? ` (task ${pr.task})` : ''}.</p>
88
- <p>This clears ${what}. The gate check re-runs and the PR merges once it goes green. Your approval applies only to commit <code>${escapeHtml(shortSha(pr.head_sha))}</code> — a later push must be re-approved.</p>`,
95
+ <p>This clears ${what}. The gate check re-runs and the PR merges once it goes green. Your approval applies only to commit <span class="ov-key">${escapeHtml(shortSha(pr.head_sha))}</span>; a later push must be re-approved.</p>`,
89
96
  confirmLabel: 'Approve & re-run',
90
97
  onConfirm: async (modal) => {
91
98
  const btn = modal.querySelector('[data-act=confirm]');
@@ -106,66 +113,70 @@
106
113
 
107
114
  // ---- render --------------------------------------------------------------
108
115
 
109
- function renderCrate(pr) {
110
- const mod = pr.verdict === 'needs_trust' ? 'needs_trust' : 'escalate';
111
- const reasonsHtml = (pr.reasons || []).map((r) =>
112
- `<li><code>${escapeHtml(r.path)}</code> — <span class="${r.hard_floor ? 'floor' : ''}">${r.hard_floor ? '⚑ ' : ''}${escapeHtml(r.reason)}</span></li>`
113
- ).join('') || '<li>This PR is held by the gate-review check.</li>';
116
+ // One held PR, one oversight row (oversight.css .ov-row). This is the page
117
+ // that shows the most facts in the hall — title, verdict, trust, task, PR,
118
+ // author, sha, diff, every hold reason and the changed files — and every one
119
+ // of them survives the rebuild: the verdict is a state pill instead of a band,
120
+ // the reasons are hairline detail lines, the files are chips.
121
+ function renderRow(pr) {
122
+ const floor = pr.verdict !== 'needs_trust';
123
+ const reasons = (pr.reasons || []).map((r) =>
124
+ `<li class="gate-reason${r.hard_floor ? ' gate-reason--floor' : ''}">` +
125
+ `<span class="ov-key">${escapeHtml(r.path)}</span>` +
126
+ `<span>${escapeHtml(r.reason)}</span>` +
127
+ (r.hard_floor ? '<span class="gate-reason__floor">hard floor</span>' : '') +
128
+ '</li>').join('')
129
+ || '<li><span>This PR is held by the gate-review check.</span></li>';
114
130
 
115
131
  const files = pr.files || [];
116
- const shownFiles = files.slice(0, 12).map((f) => `<code>${escapeHtml(f.path)}</code>`).join(' · ');
117
- const moreFiles = files.length > 12 ? ` · +${files.length - 12} more` : '';
132
+ const chips = files.slice(0, FILES_SHOWN).map((f) =>
133
+ `<span class="fact-chip" title="${escapeHtml(f.path)}">${escapeHtml(f.path)}</span>`).join('');
134
+ const more = files.length > FILES_SHOWN ? `<span class="gate-files__more">+${files.length - FILES_SHOWN} more</span>` : '';
118
135
  const filesHtml = files.length
119
- ? `<p class="gate-files">Changed files (${files.length}): ${shownFiles}${moreFiles}</p>`
136
+ ? `<div class="gate-files" aria-label="Changed files"><span class="ov-fact">${fmtInt(files.length)} file${files.length === 1 ? '' : 's'}</span>${chips}${more}</div>`
120
137
  : '';
121
138
 
122
- const taskHtml = pr.task
123
- ? `<a href="/#/task/${encodeURIComponent(pr.task)}">task ${escapeHtml(String(pr.task))}</a> · `
124
- : '';
125
- const trustedPill = pr.author_trusted
126
- ? `<span class="gate-pill gate-pill--trusted">author trusted</span>` : '';
127
-
128
- const actionsHtml = pr.already_approved
129
- ? `<span class="gate-approved-note">✓ Approved — re-check pending.</span>`
130
- : `<button class="gate-btn" data-act="approve">Approve</button>`;
131
-
132
- const el = document.createElement('article');
133
- el.className = `gate-crate gate-crate--${mod}`;
139
+ const facts = [
140
+ `<span class="fact-pill fact-pill--${floor ? 'danger' : 'warn'}">${escapeHtml(verdictLabel(pr.verdict))}</span>`,
141
+ pr.author_trusted ? '<span class="fact-pill fact-pill--info">author trusted</span>' : '',
142
+ pr.task ? `<span class="ov-fact"><a href="/#/task/${encodeURIComponent(pr.task)}">task ${escapeHtml(String(pr.task))}</a></span>` : '',
143
+ `<span class="ov-fact">by <b>${escapeHtml(pr.author || 'unknown')}</b></span>`,
144
+ `<span class="ov-mono">${escapeHtml(shortSha(pr.head_sha))}</span>`,
145
+ `<span class="ov-fact"><a href="${escapeHtml(pr.url)}/files" target="_blank" rel="noopener">view diff</a></span>`,
146
+ ].filter(Boolean).join('');
147
+
148
+ const action = pr.already_approved
149
+ ? '<span class="ov-fact ov-ok">Approved. Re-check pending.</span>'
150
+ : '<button type="button" class="btn-accent" data-act="approve">Approve</button>';
151
+
152
+ const el = document.createElement('li');
153
+ el.className = 'ov-row';
134
154
  el.innerHTML =
135
- `<div class="gate-crate__seal" aria-hidden="true"></div>
136
- <div class="gate-crate__body">
137
- <div class="gate-crate__head">
138
- <span class="gate-crate__title"><a href="${escapeHtml(pr.url)}" target="_blank" rel="noopener">${escapeHtml(pr.title || `PR #${pr.number}`)}</a></span>
139
- <span>
140
- <span class="gate-pill gate-pill--${mod}">${escapeHtml(verdictLabel(pr.verdict))}</span>
141
- ${trustedPill}
142
- </span>
143
- </div>
144
- <p class="gate-crate__meta">
145
- ${taskHtml}<a href="${escapeHtml(pr.url)}" target="_blank" rel="noopener">#${escapeHtml(String(pr.number))}</a>
146
- · by ${escapeHtml(pr.author || 'unknown')} · <code>${escapeHtml(shortSha(pr.head_sha))}</code>
147
- · <a href="${escapeHtml(pr.url)}/files" target="_blank" rel="noopener">view diff ↗</a>
148
- </p>
149
- <ul class="gate-reasons">${reasonsHtml}</ul>
155
+ `<div class="ov-row__main">
156
+ <a class="ov-row__title" href="${escapeHtml(pr.url)}" target="_blank" rel="noopener">${escapeHtml(pr.title || `PR #${pr.number}`)}<span class="ov-mono">#${escapeHtml(String(pr.number))}</span></a>
157
+ <div class="ov-row__facts">${facts}</div>
158
+ <ul class="ov-lines">${reasons}</ul>
150
159
  ${filesHtml}
151
- <div class="gate-actions">${actionsHtml}</div>
152
- </div>`;
160
+ </div>
161
+ <div class="ov-row__right">${action}</div>`;
153
162
 
154
163
  const approveBtn = el.querySelector('[data-act=approve]');
155
164
  if (approveBtn) approveBtn.addEventListener('click', () => confirmApprove(pr));
156
165
  return el;
157
166
  }
158
167
 
168
+ // The glass strip above the panel: the shell's .stat-row, each tile a figure
169
+ // with the one clause that makes it a fact rather than a number.
159
170
  function renderSummary(awaiting) {
160
171
  const floors = awaiting.filter((p) => p.verdict === 'escalate').length;
161
172
  const trust = awaiting.filter((p) => p.verdict === 'needs_trust').length;
162
- const stats = [
163
- ['Awaiting your approval', fmtInt(awaiting.length)],
164
- ['Hard-floor holds', fmtInt(floors)],
165
- ['Trust holds', fmtInt(trust)],
173
+ const tiles = [
174
+ ['Awaiting your approval', fmtInt(awaiting.length), 'held at the gate check'],
175
+ ['Hard-floor holds', fmtInt(floors), 'touch a surface only an owner may clear'],
176
+ ['Trust holds', fmtInt(trust), 'an author the gate does not yet trust'],
166
177
  ];
167
- document.getElementById('gate-summary').innerHTML = stats.map(([label, value]) =>
168
- `<div class="gate-stat"><div class="gate-stat__label">${escapeHtml(label)}</div><div class="gate-stat__value">${value}</div></div>`).join('');
178
+ document.getElementById('gate-summary').innerHTML = tiles.map(([label, num, sub]) =>
179
+ `<div class="stat-tile"><span class="stat-tile__label">${escapeHtml(label)}</span><span class="stat-tile__num">${num}</span><span class="stat-tile__sub">${escapeHtml(sub)}</span></div>`).join('');
169
180
  }
170
181
 
171
182
  async function load() {
@@ -177,7 +188,7 @@
177
188
  } catch (err) {
178
189
  if (err && err.kind === '401') return;
179
190
  if (err && err.kind === '403') { showSealed(); return; }
180
- list.innerHTML = `<p class="gate-empty">Could not load held pull requests just now.</p>`;
191
+ list.innerHTML = emptyStateHtml('Could not load held pull requests just now.');
181
192
  return;
182
193
  }
183
194
 
@@ -185,15 +196,18 @@
185
196
  renderSummary(awaiting);
186
197
 
187
198
  if (awaiting.length === 0) {
188
- list.innerHTML = `<p class="gate-empty">Nothing is waiting — no held changes need your approval.</p>`;
199
+ list.innerHTML = emptyStateHtml('Nothing is waiting. No held change needs your approval.');
189
200
  } else {
190
201
  list.innerHTML = '';
191
- for (const pr of awaiting) list.appendChild(renderCrate(pr));
202
+ const rows = document.createElement('ul');
203
+ rows.className = 'ov-rows';
204
+ for (const pr of awaiting) rows.appendChild(renderRow(pr));
205
+ list.appendChild(rows);
192
206
  }
193
207
 
194
208
  if (resp.checking > 0) {
195
209
  checkingEl.hidden = false;
196
- checkingEl.textContent = `${resp.checking} more PR${resp.checking === 1 ? '' : 's'} still being checked — refresh shortly.`;
210
+ checkingEl.textContent = `${resp.checking} more PR${resp.checking === 1 ? '' : 's'} still being checked. Refresh shortly.`;
197
211
  } else {
198
212
  checkingEl.hidden = true;
199
213
  }
@@ -203,10 +217,13 @@
203
217
  document.getElementById('gate-sealed').hidden = false;
204
218
  document.getElementById('gate-body').hidden = true;
205
219
  document.getElementById('gate-sub').textContent = 'Restricted.';
220
+ // The floor in the pack's own words (ADR 0157: metic), never a rank spelled into the source.
221
+ const body = document.getElementById('gate-sealed-body');
222
+ if (body) body.textContent = `Available from the ${rankLabel('metic')} rank up.`;
206
223
  }
207
224
 
208
225
  async function init() {
209
- // Confirm rank=archon for a friendly sealed notice; the server gates the page
226
+ // Confirm the viewer's rank for a friendly sealed notice; the server gates the page
210
227
  // + every endpoint regardless (cosmetic defence-in-depth).
211
228
  let me;
212
229
  try {
@@ -46,6 +46,23 @@
46
46
  },
47
47
  "out": {
48
48
  "auth": false
49
+ },
50
+ "approve": {
51
+ "auth": true,
52
+ "actions": [
53
+ [
54
+ "wait",
55
+ 700
56
+ ],
57
+ [
58
+ "clickIfVisible",
59
+ "[data-act=approve]"
60
+ ],
61
+ [
62
+ "wait",
63
+ 250
64
+ ]
65
+ ]
49
66
  }
50
67
  },
51
68
  "reducedMotion": {
@@ -56,5 +73,6 @@
56
73
  600
57
74
  ]
58
75
  ]
59
- }
76
+ },
77
+ "_states": "task 1003469: `approve` opens the confirm modal on the first held PR. clickIfVisible, because the kit's own stub carries no /gate-approvals fixture and renders the empty list — the modal is measured against the harness."
60
78
  }