@bongos/core 1.24.25 → 1.24.27

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 (114) hide show
  1. package/.bongos-core.json +173 -108
  2. package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +12 -0
  3. package/docs/copy-inventory.md +280 -256
  4. package/docs/copy-registry.json +572 -306
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-inventory.json +10 -4
  7. package/docs/page-readings.json +4337 -4313
  8. package/docs/recipes/ui-look-before-you-ship.md +1 -0
  9. package/modules/autonomy/fence.js +16 -1
  10. package/modules/government/protected-surfaces.json +2 -0
  11. package/modules/hall-ui/public/ai-studio.html +2 -2
  12. package/modules/hall-ui/public/atlas.html +1 -1
  13. package/modules/hall-ui/public/blockers.html +2 -2
  14. package/modules/hall-ui/public/blockers.js +3 -1
  15. package/modules/hall-ui/public/board-room.html +3 -1
  16. package/modules/hall-ui/public/city.html +1 -1
  17. package/modules/hall-ui/public/collab.html +4 -4
  18. package/modules/hall-ui/public/collab.js +4 -0
  19. package/modules/hall-ui/public/commands.html +3 -3
  20. package/modules/hall-ui/public/copy-desk.html +3 -3
  21. package/modules/hall-ui/public/core-move-notice.js +9 -6
  22. package/modules/hall-ui/public/criterion-uat-ui.js +1 -1
  23. package/modules/hall-ui/public/deploy-handoff.js +203 -0
  24. package/modules/hall-ui/public/deploy.html +6 -2
  25. package/modules/hall-ui/public/deploy.js +7 -1
  26. package/modules/hall-ui/public/diagrams.html +1 -1
  27. package/modules/hall-ui/public/drachmae.html +2 -2
  28. package/modules/hall-ui/public/fleet.html +3 -1
  29. package/modules/hall-ui/public/gate.html +3 -1
  30. package/modules/hall-ui/public/goal-map.html +1 -1
  31. package/modules/hall-ui/public/goals-page.js +1 -1
  32. package/modules/hall-ui/public/goals.css +3 -2
  33. package/modules/hall-ui/public/goals.html +3 -3
  34. package/modules/hall-ui/public/goals.js +1 -1
  35. package/modules/hall-ui/public/government.html +3 -1
  36. package/modules/hall-ui/public/hall-widgets.js +9 -9
  37. package/modules/hall-ui/public/idea.html +1 -1
  38. package/modules/hall-ui/public/ideas.html +3 -3
  39. package/modules/hall-ui/public/index.html +3 -1
  40. package/modules/hall-ui/public/lightbulb.js +1 -1
  41. package/modules/hall-ui/public/market-watch-item.html +2 -2
  42. package/modules/hall-ui/public/market-watch-rules.html +2 -2
  43. package/modules/hall-ui/public/market-watch-settings.html +3 -1
  44. package/modules/hall-ui/public/market-watch.html +2 -2
  45. package/modules/hall-ui/public/modules.html +2 -2
  46. package/modules/hall-ui/public/my-agents.html +3 -3
  47. package/modules/hall-ui/public/primer.html +2 -2
  48. package/modules/hall-ui/public/profile.html +6 -6
  49. package/modules/hall-ui/public/project-settings.html +3 -1
  50. package/modules/hall-ui/public/ranks.html +4 -4
  51. package/modules/hall-ui/public/roadmap.html +2 -2
  52. package/modules/hall-ui/public/roster.html +2 -2
  53. package/modules/hall-ui/public/sessions.html +3 -1
  54. package/modules/hall-ui/public/settings-software-update.js +23 -9
  55. package/modules/hall-ui/public/settings-specialities.js +1 -1
  56. package/modules/hall-ui/public/settings.html +10 -6
  57. package/modules/hall-ui/public/settings.js +3 -0
  58. package/modules/hall-ui/public/shell.js +25 -73
  59. package/modules/hall-ui/public/street.html +1 -1
  60. package/modules/hall-ui/public/studio.html +1 -1
  61. package/modules/hall-ui/public/studio.js +3 -3
  62. package/modules/hall-ui/public/style.css +209 -40
  63. package/modules/hall-ui/public/task-detail.js +2 -2
  64. package/modules/hall-ui/public/task.html +1 -1
  65. package/modules/hall-ui/public/thinking.html +1 -1
  66. package/modules/hall-ui/public/tweak-editor.html +2 -2
  67. package/modules/hall-ui/public/update-pill.js +623 -0
  68. package/modules/hall-ui/public/watch.html +3 -1
  69. package/modules/hall-ui/public/watch.js +1 -1
  70. package/modules/hall-ui/public/work.html +4 -4
  71. package/modules/hall-ui/records/loader.md +35 -0
  72. package/modules/hall-ui/records/update-pill.md +12 -0
  73. package/modules/lifecycle/github-push.js +1 -0
  74. package/modules/lifecycle/routes/tasks.js +2 -2
  75. package/modules/lifecycle/runner-self-guard.js +122 -0
  76. package/modules/lifecycle/runner-self.js +154 -0
  77. package/modules/public-landing/public/account.html +3 -3
  78. package/modules/public-landing/public/assets/cosmos.css +45 -0
  79. package/modules/public-landing/public/community.html +4 -4
  80. package/modules/public-landing/public/connections.html +1 -1
  81. package/modules/public-landing/public/guild.html +1 -1
  82. package/modules/public-landing/public/invitations.html +4 -4
  83. package/modules/public-landing/public/projects.html +26 -26
  84. package/modules/status-ui/public/index.html +2 -2
  85. package/modules/status-ui/public/index.states.json +12 -0
  86. package/modules/status-ui/public/status.js +10 -2
  87. package/modules/status-ui/public/style.css +57 -0
  88. package/modules/ui-design/kit/README.md +1 -1
  89. package/modules/ui-design/kit/serve.js +8 -1
  90. package/package-lock.json +2 -2
  91. package/package.json +1 -1
  92. package/release-notes.json +24 -0
  93. package/scripts/bongos/autobongos-loop.js +4 -0
  94. package/scripts/bongos/autobongos-run.js +21 -4
  95. package/scripts/bongos/autobongos-tweak-lane.js +5 -3
  96. package/scripts/bongos/ship-flow.js +20 -0
  97. package/scripts/hall-preview/README.md +1 -0
  98. package/scripts/hall-preview/server.js +9 -0
  99. package/src/bongos/apex-hall-redirects.js +53 -0
  100. package/src/bongos/serve-internal.js +3 -18
  101. package/src/module-api.js +1 -1
  102. package/tests/autobongos_host_access.mjs +5 -3
  103. package/tests/autobongos_self_modification.mjs +420 -0
  104. package/tests/core_update_banner.mjs +6 -92
  105. package/tests/core_upgrade_owner_door.mjs +4 -56
  106. package/tests/deploy_handoff.mjs +206 -0
  107. package/tests/government_protected_surfaces.mjs +15 -0
  108. package/tests/hall_loading.mjs +153 -0
  109. package/tests/hub_connections_page.mjs +1 -1
  110. package/tests/hub_invitations_page.mjs +1 -1
  111. package/tests/loading_mark.mjs +203 -0
  112. package/tests/nav_permission_atoms.mjs +3 -2
  113. package/tests/software_update.mjs +39 -2
  114. package/tests/update_pill.mjs +431 -0
@@ -34,6 +34,8 @@
34
34
  </div>
35
35
 
36
36
  <main id="hall-main">
37
+ <!-- task 1005043: the page loader, shown until the sealed notice or a pane has something showing. -->
38
+ <p class="hall-loading hall-loading--page hall-loading--zones" role="status">Loading…</p>
37
39
  <!-- Restricted notice — shown by watch.js when the viewer is below the
38
40
  floor, with the floor written in from the pack's rank label. -->
39
41
  <section class="scroll" id="sealed-scroll" aria-labelledby="sealed-h" hidden>
@@ -166,7 +168,7 @@
166
168
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
167
169
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
168
170
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
169
- <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
171
+ <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
170
172
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
171
173
  <script src="/builders/palette.js" defer></script>
172
174
  <!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
@@ -754,7 +754,7 @@
754
754
  if (!btn || !target.contains(btn)) return;
755
755
  const slug = btn.getAttribute('data-doc-open');
756
756
  const view = $('#engagement-doc-view');
757
- if (view) view.innerHTML = foot('Reading…');
757
+ if (view) view.innerHTML = '<p class="hall-loading" role="status">Reading…</p>';
758
758
  try {
759
759
  const res = await fetchJson(`/security/docs/${encodeURIComponent(slug)}`);
760
760
  const doc = res.doc || {};
@@ -105,7 +105,7 @@
105
105
  <p class="claim-count-line" id="claim-count" aria-live="polite"></p>
106
106
 
107
107
  <div class="task-list" id="task-list">
108
- <div class="profile__placeholder">…</div>
108
+ <div class="hall-loading" role="status">Loading…</div>
109
109
  </div>
110
110
  <div id="ready-pager-mount"></div>
111
111
  </section>
@@ -117,7 +117,7 @@
117
117
 
118
118
  <table class="ledger" id="wip-table">
119
119
  <thead><tr><th></th><th>#</th><th>Task</th><th>Builder</th><th>For</th><th></th></tr></thead>
120
- <tbody><tr><td colspan="6" class="ledger__empty">…</td></tr></tbody>
120
+ <tbody><tr><td colspan="6" class="ledger__empty"><span class="hall-loading" role="status">Loading…</span></td></tr></tbody>
121
121
  </table>
122
122
  <div id="wip-pager-mount"></div>
123
123
  </section>
@@ -139,7 +139,7 @@
139
139
  <div class="stat-row" id="works-summary" style="margin-bottom: 14px" hidden></div>
140
140
  <p class="scroll__lede">The most recently shipped tasks.</p>
141
141
  <div class="history" id="history-list">
142
- <div class="profile__placeholder">…</div>
142
+ <div class="hall-loading" role="status">Loading…</div>
143
143
  </div>
144
144
  <div id="done-pager-mount"></div>
145
145
  </section>
@@ -174,7 +174,7 @@
174
174
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
175
175
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
176
176
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
177
- <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
177
+ <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
178
178
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
179
179
  <script src="/builders/palette.js" defer></script>
180
180
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -0,0 +1,35 @@
1
+ # The loader — the Bongos mark wherever a hall page waits for its data (task 1005043)
2
+
3
+ Goal 1000095. Owner ask, 2026-11-03: a smooth load screen with the Bongos logo whenever the hall
4
+ waits for data, in the spirit of monday.com's Vibe data loaders (vibe.monday.com, Skeleton: grey
5
+ shapes breathing 0.4 to 1 and back over 0.8s, in the place the content will land).
6
+
7
+ - **One component, in `style.css`: `.hall-loading`.** CSS only. `::before` is the ring and the core
8
+ (a mask of shell.js's brand-mark geometry, painted `--ink` at 30%), `::after` is the dot (the same
9
+ box, painted `--accent`). Both turn together round the still core, easing out of and back into the
10
+ logo's own pose every 1.6s; the ink breathes on half that beat. The element's own text is the
11
+ caption. It arrives 240ms late (filter: opacity), so a read that answers fast never shows it.
12
+ Sizes: the default (40px, a panel), `--page` (64px, a page's main area), `--inline` (16px, a line).
13
+ - **A page opts in by putting the class on the placeholder it already had**, and gives it
14
+ `role="status"`. The words did not change; a bare `…` became `Loading…`. A placeholder a script
15
+ rewrites IN PLACE (atlas's caption, sessions' status line, the goal dependency ledes, the profile's
16
+ activity placeholder) keeps its plain words, because the class would stay on the finished sentence.
17
+ - **Pages that are blank while they wait get a page loader that puts itself away.** The sealed/body
18
+ pages (board-room, deploy, fleet, gate, government, market-watch-settings, project-settings,
19
+ sessions) open their `<main>` on `.hall-loading--wait`, which shows only while every section after
20
+ it is `[hidden]`. Home and Watch, whose zones and panes are always present, use
21
+ `.hall-loading--zones`: it shows while no showing zone has a showing child. Both are `:has()`
22
+ rules inside `@supports`, so a browser without it simply never shows them. No page script hides
23
+ them. Settings' layout is never `[hidden]`, so `hideLayout()` puts its loader away.
24
+ - **Lists that paint later carry a loader inside them**: Blockers' list (its first `renderList`
25
+ replaces it), Collab's three ask lists (its failure path now stops them and says why, where the page
26
+ used to stay blank), the table placeholder rows (the loader sits inside the `ledger__empty` cell).
27
+ - **The skeletons breathe too.** `.kit-skel` and the goal list's `.gr-skel__ring` take the loader's
28
+ breath, so the Goals list's row skeleton is the Vibe pattern exactly.
29
+ - **DESIGN.md holds.** Transform and filter only; every colour a token (the black in the masks is
30
+ alpha); the Kill Switch stops it all and leaves the logo. The 20s ambient floor is for the plates:
31
+ a loader is the live thing on the page, which is also why its dot may spend the accent.
32
+ - **Not covered:** the apex pages (account, projects, community) and the status page run their own
33
+ sheets; the city, street, goal map and thinking sky draw their own worlds and kept their own lines.
34
+ - **Look at it:** `node scripts/hall-preview/server.js --port 4630 --fixture-me --latency 5000` holds
35
+ every read 5s (task 1005043 added `--latency`). Tests: `tests/hall_loading.mjs`.
@@ -0,0 +1,12 @@
1
+ # The Update pill — a newer core, taken in place from any hall page (task 1005041)
2
+
3
+ The owner, 2026-11-03: upgrading Bongos from a project's hall was "way too obstructing and not obvious what to do", and it should feel like the Claude app, which just tells you to restart. A hosted project's owner used to meet a full-width banner, go to Settings, press Update, land on the hub's deploy door on another site, press Check, then Confirm, then wait there. What this settled:
4
+
5
+ - **A pill, not a strip.** `update-pill.js` draws one button at the head of the top bar's right side (the Link GitHub chip's spot and shape, one step louder: an accent dot), for the same viewers the banner had (`core.pin.move`). Four faces: `Update`, `Updating…`, `Reload` (filled accent, the Claude moment) and `Update held`. It has no dismiss: it is quiet enough to leave. The one-time "Bongos is updated" note (`core-move-notice.js`) is the same card, pinned under the top bar's right end, not a second strip.
6
+ - **The card is step one, Update now is step two (ADR 0293 D4).** The card names the version (`from → to`, small mono under plain-language highlights from the release notes) and Update now sends exactly that one. A door that names another version is never sent: the card shows the door's version and the next press takes it.
7
+ - **The act stays where it was.** On the platform the pill calls its own door (`/provisioning/control-plane`, then the core-upgrade POST). On a hosted project it opens the hub's door as a small window (`&handoff=1`); `deploy-handoff.js` there says `ready`, hears `go` with the version, and moves the project with the owner's own hub session. The hub hears only the window that opened it, from the project's registered address (`domain`), naming the version its own preview would move to. No word from the hall means one press in the window, never a move on load. Self-hosted: the card names `bongos upgrade`; operator-run: "Ask for this update".
8
+ - **Progress is read off the project's own `/version`**, with the hold (`GET /core-update` `held`) asked every fifth look for a rollback. The run is remembered in `localStorage` (`bongos.update.run`) so it follows the owner across pages and survives the restart; it is forgotten when the version lands or after 45 minutes.
9
+ - **Settings uses the same flow.** Settings → Software update's Update button hands its reading to `OTBUpdatePill.updateNow`; its `href` stays the door's for a page whose pill script did not load.
10
+ - **shell.js got smaller.** The banner's 109 lines became a 61-line loader that fetches `/core-update` once per page and loads the pill only when there is news or a run in flight (1,499 → 1,451 lines).
11
+
12
+ Tests: `tests/update_pill.mjs` (the pure half, and the real shell + pill in a DOM stub: who sees it, never a link, both doors, refusals, a blocked window, held, a run across pages, done → Reload), `tests/deploy_handoff.mjs` (the hub's three refusals, and its page run for real). Each origin, opener and version check was deleted once and watched to go red. Harness fixture: `core-update.json` (behind, so the pill shows).
@@ -1347,6 +1347,7 @@ async function getPullRequestFiles({ number }, deps = {}) {
1347
1347
  for (const f of batch) {
1348
1348
  files.push({
1349
1349
  path: f.filename,
1350
+ previous_path: f.previous_filename || null, // a rename's old path (task 1004546)
1350
1351
  mode: '100644',
1351
1352
  deleted: f.status === 'removed',
1352
1353
  status: f.status,
@@ -37,7 +37,7 @@ const { gradeQualityWhere } = require('../db-shared');
37
37
  const { helperJoinSql } = require('../page-tweak-artist'); // task 1004432: a page tweak's applier as a helper
38
38
  const auth = api;
39
39
  const db = require('../db');
40
- const pageTweakReads = require('../page-tweak-reads'); const { HELD_NOT_ARMED, prMergeSentence } = require('../page-tweak-hold'); const autoPickupFiles = require('../autopickup-files'); const githubPrClose = require('../github-pr-close'); // ADR 0341 D7 (tests stub the read); ADR 0371 D8
40
+ const pageTweakReads = require('../page-tweak-reads'); const { HELD_NOT_ARMED, prMergeSentence } = require('../page-tweak-hold'); const autoPickupFiles = require('../autopickup-files'); const githubPrClose = require('../github-pr-close'); const runnerSelfGuard = require('../runner-self-guard'); const { sharedPrReads } = require('../ship-files'); // ADR 0341 D7 (tests stub the read); ADR 0371 D8; task 1004546
41
41
  const hierarchy = require('../hierarchy');
42
42
  const { suggestCriteria } = require('../criterion-suggest');
43
43
  // The orphan-task advisory (task 1002539) — pure, fail-open; see goal-advisory.js.
@@ -1184,7 +1184,7 @@ module.exports = function buildTasksRouter() {
1184
1184
  `Merging triggers .github/workflows/deploy-prod.yml -> ${branding().domains.publicOrigin}. ` +
1185
1185
  prMergeSentence(held);
1186
1186
  const pr = await githubPush.openOrFindPullRequest({ branch, title, body });
1187
- const apRefusal = await autoPickupFiles.autoPickupFileRefusal({ taskId: id, prNumber: pr.number }); if (apRefusal) { await githubPrClose.closePullRequest({ number: pr.number }).catch(() => null); return res.fail(apRefusal.code, 409, { ...apRefusal, message: autoPickupFiles.refusalMessage(apRefusal) }); } // ADR 0371 D8 (task 1004678)
1187
+ const prReads = sharedPrReads(); for (const [guard, refusalFor] of [[autoPickupFiles, autoPickupFiles.autoPickupFileRefusal], [runnerSelfGuard, runnerSelfGuard.runnerSelfEditRefusal]]) { const refusal = await refusalFor({ taskId: id, prNumber: pr.number, branch }, { getPullRequestFiles: prReads.getPullRequestFiles }); if (refusal) { await githubPrClose.closePullRequest({ number: pr.number }).catch(() => null); return res.fail(refusal.code, 409, { ...refusal, message: guard.refusalMessage(refusal) }); } } // the PR's own files: an auto-pickup round stays on its page (ADR 0371 D8, task 1004678); a runner never changes its own files (task 1004546)
1188
1188
  // Best-effort by contract (pr-assign.js never rejects); the catch is for a future
1189
1189
  // edit that forgets — an assignment must not be able to fail a pushed ship.
1190
1190
  const assignment = await prAssign.assignPullRequest({ number: pr.number, login: builderLogin })
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ // modules/lifecycle/runner-self-guard.js — the server refuses to publish a
4
+ // runner's change to its own supervisor, safety checks or kill switch, or to any
5
+ // ADR 0043 protected file (task 1004546, ADR 0307 §6).
6
+ //
7
+ // WHY. The runner's own checks (the pre-claim fence and the ship-time diff check)
8
+ // run on the runner's machine, where the worker runs with blanket permissions and
9
+ // could edit or skip them. This one runs on the deployed server, against the pull
10
+ // request's files as GitHub reports them, so nothing on that machine can change it.
11
+ // A runner host holds no push credential (ADR 0055), so publish-branch is its only
12
+ // road to main, and that route asks this file once per publish, beside the
13
+ // auto-pickup files check and in the same shape (autopickup-files.js).
14
+ //
15
+ // TWO LISTS, OVER THE REAL DIFF. The runner's own files (runner-self.js, matched by
16
+ // that file's matcher, required directly) and the ADR 0043 registry (the fence
17
+ // already refuses a DECLARED protected touch; this refuses one the work actually
18
+ // made). The registry is what holds the route line calling this file: tasks.js is
19
+ // on it, so a runner cannot publish its removal.
20
+ //
21
+ // THE REGISTRY MATCHER COMES THROUGH THE DOORWAY. A module reaches core only
22
+ // through src/module-api.js, and task 1004540's grade found that a matcher
23
+ // resolved there could be swapped by a change to that file. So the doorway is on
24
+ // the runner's own list (owner ruling 2026-11-03), and a runner cannot publish a
25
+ // change to it. The matcher is also checked before it is trusted: it must flag two
26
+ // paths the registry has always protected, and one that does not is treated as
27
+ // unreadable, which refuses.
28
+ //
29
+ // WHICH CLAIM IS A RUNNER'S. Its worktree name (runner-self.js RUNNER_TREE_RE),
30
+ // read from the claim row. If that read FAILS, the branch name stands in (the
31
+ // runner's branch is its worktree name), so a database hiccup does not switch the
32
+ // wall off for a runner while an ordinary publish still never depends on the read.
33
+ // Telling a runner's session from its owner's on the server is task 1004547.
34
+ //
35
+ // FAILS CLOSED on the files: a read that throws, a list at GitHub's 300-file cap,
36
+ // or a matcher that fails its check all refuse. An unattended change that cannot
37
+ // be checked goes to a person.
38
+ //
39
+ // Its own small file, on the protected list and on the runner's own list, because
40
+ // the grade of task 1004540 refused the same check written into the shared route
41
+ // helpers and claim reads, which neither list covers.
42
+
43
+ const api = require('../../src/module-api');
44
+ const githubPush = require('./github-push');
45
+ const { PR_FILES_CAP } = require('./ship-files');
46
+ const runnerSelf = require('./runner-self');
47
+
48
+ // The task's latest claim, released or not: a publish runs after the ship has
49
+ // resolved it. Both names are read because the server may store a disambiguated
50
+ // worktree_name beside the one the runner asked for.
51
+ const CLAIM_SQL = `
52
+ SELECT c.worktree_name, c.requested_worktree_name
53
+ FROM claims c WHERE c.task_id = $1
54
+ ORDER BY c.claimed_at DESC, c.id DESC LIMIT 1`;
55
+
56
+ // What a failed claim read answers: not "no claim", which an ordinary publish
57
+ // with no claim row also answers.
58
+ const CLAIM_UNREADABLE = Object.freeze({ unreadable: true });
59
+
60
+ async function readLatestClaim(taskId, exec = api.pool) {
61
+ try {
62
+ const { rows } = await exec.query(CLAIM_SQL, [taskId]);
63
+ return rows[0] || null;
64
+ } catch (_) {
65
+ return CLAIM_UNREADABLE;
66
+ }
67
+ }
68
+
69
+ function isRunnerPublish(claim, branch) {
70
+ if (claim === CLAIM_UNREADABLE) return runnerSelf.isRunnerTree(branch);
71
+ return !!claim && (runnerSelf.isRunnerTree(claim.worktree_name) || runnerSelf.isRunnerTree(claim.requested_worktree_name));
72
+ }
73
+
74
+ // Paths the registry has protected since ADR 0043; a matcher that misses either
75
+ // is not the registry's.
76
+ const MATCHER_CANARIES = Object.freeze(['modules/lifecycle/routes/tasks.js', 'migrations/001_canary.sql']);
77
+ function verifiedMatcher(match) {
78
+ try {
79
+ if (MATCHER_CANARIES.every((f) => (match([f]) || []).length === 1)) return match;
80
+ } catch (_) { /* a matcher that throws is not trusted */ }
81
+ return null;
82
+ }
83
+
84
+ const unreadable = () => ({ code: 'self_modification_unreadable', files: [], protected: [] });
85
+
86
+ // The route's one call. Returns null (publish as usual) or the refusal
87
+ // { code, files, protected }. `deps` is the test seam, and the route passes a
88
+ // shared getPullRequestFiles so this and the auto-pickup check read GitHub once.
89
+ async function runnerSelfEditRefusal({ taskId, prNumber, branch = null }, deps = {}) {
90
+ const claim = await (deps.readLatestClaim || readLatestClaim)(taskId);
91
+ if (!isRunnerPublish(claim, branch)) return null;
92
+ const match = verifiedMatcher(deps.matchProtected || ((files) => api.permissionPathCheck.matchProtected(files)));
93
+ if (!match) return unreadable();
94
+ let rows;
95
+ try {
96
+ rows = await (deps.getPullRequestFiles || ((a) => githubPush.getPullRequestFiles(a)))({ number: Number(prNumber) });
97
+ } catch (_) {
98
+ return unreadable();
99
+ }
100
+ const list = Array.isArray(rows) ? rows : [];
101
+ if (list.length >= PR_FILES_CAP) return unreadable();
102
+ // A rename is checked at both ends: moving a guard away is an edit to it.
103
+ const changed = [];
104
+ for (const f of list) {
105
+ if (f && typeof f.path === 'string' && f.path) changed.push(f.path);
106
+ if (f && typeof f.previous_path === 'string' && f.previous_path) changed.push(f.previous_path);
107
+ }
108
+ return runnerSelf.diffRefusal({ files: changed, protectedHits: match(changed) });
109
+ }
110
+
111
+ function refusalMessage(r) {
112
+ const why = r.code === 'self_modification_unreadable'
113
+ ? "An Autobongos runner's pull request could not be checked in full for changes to the runner's own or protected files. This task goes to a person."
114
+ : runnerSelf.selfEditMessage(r);
115
+ return `${why} The pull request was closed (task 1004546).`;
116
+ }
117
+
118
+ module.exports = {
119
+ readLatestClaim,
120
+ runnerSelfEditRefusal,
121
+ refusalMessage,
122
+ };
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ // modules/lifecycle/runner-self.js — the files an Autobongos runner must never
4
+ // change: its own supervisor, its safety checks and its kill switch (task 1004546,
5
+ // carrying task 1004540's work; ADR 0307 §6).
6
+ //
7
+ // WHY. Owner rule, 2026-10-02: the runner never edits its own supervisor, safety
8
+ // checks or kill switch. The fence only refused tasks that hit the ADR 0043
9
+ // registry, and none of the runner's own files were on it, so an unattended worker
10
+ // could be handed a bug whose fix loosened the fence, the gauge, the grade cap or
11
+ // the kill switch. This is the ONE list of those files and the ONE spelling of what
12
+ // a runner's claim looks like. Three places ask it:
13
+ //
14
+ // - the runner, BEFORE the claim, over the task's declared touches[]
15
+ // (scripts/bongos/autobongos-run.js pickTask, then modules/autonomy/fence.js
16
+ // decideTask as the gate of record);
17
+ // - ship.js, inside the worker, over the branch's real diff, so a worker is
18
+ // stopped before it spends a grade (selfEditRefusal);
19
+ // - the server, at publish, over the pull request's own files as GitHub reports
20
+ // them (runner-self-guard.js). This is the wall: the worker can unset its own
21
+ // environment, it cannot change what the deployed server checks.
22
+ //
23
+ // The last two also refuse an ADR 0043 protected file in the real diff (see
24
+ // diffRefusal), which is what keeps a runner from deleting the server guard's call.
25
+ //
26
+ // A RUNNER-ONLY LIST, NOT THE ADR 0043 REGISTRY. A person may still edit these
27
+ // files at their own rank; only an unattended worker may not. Widening the registry
28
+ // would have raised the floor for every human builder to stop one machine.
29
+ //
30
+ // THE LIST COVERS ITS OWN ENFORCEMENT. Every file that implements a check above is
31
+ // on it: this file, the server guard, the fence, the supervisor, the ship pipeline
32
+ // that runs the ship check, the tests that pin them. The route line that calls the
33
+ // server guard sits in modules/lifecycle/routes/tasks.js, which is deliberately NOT
34
+ // listed (the owner ruled that listing tasks.js whole would refuse a large share of
35
+ // ordinary work). It is on the ADR 0043 registry, which the real-diff checks also
36
+ // read, and tests/autobongos_self_modification.mjs drives the real publish route,
37
+ // so removing that line is refused for a runner and turns CI red for anyone.
38
+ //
39
+ // Requires nothing, so the runner, ship.js and the server all read the same list
40
+ // without the module doorway in between (the grade of task 1004540 found that a
41
+ // matcher resolved through src/module-api.js could be swapped there). The doorway
42
+ // itself is on the list too, for what the server guard still reads through it.
43
+
44
+ // One pattern per line: an exact path, a directory ending in '/', or a prefix
45
+ // ending in '*' that stays inside one directory.
46
+ const RUNNER_SELF_SURFACES = Object.freeze([
47
+ // The supervisor and its CLIs (scripts/gds/ holds their forwarders).
48
+ { pattern: 'scripts/bongos/autobongos-*', why: 'the supervisor, its loop, verify, grade cap, tweak lane and Windows service' },
49
+ { pattern: 'scripts/gds/autobongos-*', why: 'forwarders to the supervisor' },
50
+ { pattern: 'scripts/bongos/autonomy-*', why: 'the usage gauge and the autonomy gate' },
51
+ { pattern: 'scripts/gds/autonomy-*', why: 'forwarders to the gauge and the gate' },
52
+ { pattern: 'scripts/bongos/sequence.js', why: 'the picker and its halt matrix' },
53
+ // Its safety checks and kill switch.
54
+ { pattern: 'modules/autonomy/', why: 'the fence, gauge, cadence, heartbeat and the kill-switch routes' },
55
+ { pattern: 'modules/lifecycle/runner-self.js', why: 'this list' },
56
+ { pattern: 'modules/lifecycle/runner-self-guard.js', why: 'the server refusal at publish' },
57
+ { pattern: 'modules/lifecycle/autopickup.js', why: "the server's auto-pickup limits" },
58
+ { pattern: 'modules/lifecycle/autopickup-files.js', why: 'the auto-pickup page-files wall' },
59
+ { pattern: 'modules/government/catalog.js', why: 'the kill switch is a system permission declared here' },
60
+ { pattern: 'modules/government/protected-surfaces.json', why: 'the ADR 0043 registry the fence also reads' },
61
+ { pattern: 'src/bongos/permission-path-check.js', why: "the registry's matcher" },
62
+ // Owner ruling 2026-11-03, reversing the earlier "not the doorway": the server
63
+ // guard reaches the registry matcher and the claims pool through it, so a runner
64
+ // that could rewrite it could swap both. About 43 tasks in 90 days touched it.
65
+ { pattern: 'src/module-api.js', why: 'the module doorway the server guard reaches the registry matcher and the pool through' },
66
+ // The only review between a worker and main, and the road to it.
67
+ { pattern: 'modules/grading/', why: 'the grader' },
68
+ { pattern: 'scripts/bongos/ship.js', why: 'the ship driver' },
69
+ { pattern: 'scripts/bongos/ship-*', why: 'the ship phases, including the ship-time self-edit check' },
70
+ { pattern: 'modules/lifecycle/github-push.js', why: 'the server-mediated publish and its pull-request files read' },
71
+ { pattern: 'modules/lifecycle/ship-files.js', why: 'the files-read cap the server guard fails closed on' },
72
+ // The tests that pin all of the above.
73
+ { pattern: 'tests/autobongos_*', why: 'the tests that pin the runner' },
74
+ { pattern: 'tests/autonomy_*', why: 'the tests that pin the autonomy module' },
75
+ ]);
76
+
77
+ // A repo path as git spells it. Windows separators and a leading './' are folded,
78
+ // and case is ignored, so a declared touch cannot slip past on its spelling.
79
+ function normalisePath(p) {
80
+ return String(p || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '').toLowerCase();
81
+ }
82
+
83
+ function matches(file, pattern) {
84
+ const pat = pattern.toLowerCase();
85
+ if (pat.endsWith('/')) return file.startsWith(pat);
86
+ if (pat.endsWith('*')) {
87
+ const prefix = pat.slice(0, -1);
88
+ return file.startsWith(prefix) && !file.slice(prefix.length).includes('/');
89
+ }
90
+ return file === pat;
91
+ }
92
+
93
+ // The runner's own files among `files`: [{ file, pattern }], in input order.
94
+ function matchRunnerSelf(files) {
95
+ const out = [];
96
+ for (const raw of Array.isArray(files) ? files : []) {
97
+ const file = normalisePath(raw);
98
+ if (!file) continue;
99
+ const hit = RUNNER_SELF_SURFACES.find((s) => matches(file, s.pattern));
100
+ if (hit) out.push({ file: String(raw), pattern: hit.pattern });
101
+ }
102
+ return out;
103
+ }
104
+
105
+ // What a runner's claim looks like: the worktree name autobongos-run.js's
106
+ // worktreeName() generates. The supervisor, its boot reconciliation and the server
107
+ // guard all read this one regex.
108
+ const RUNNER_TREE_RE = /^autobongos-\d+-[0-9a-f]{6}$/;
109
+ const isRunnerTree = (name) => RUNNER_TREE_RE.test(String(name || ''));
110
+
111
+ // Set by the supervisor on every worker it spawns, so ship.js knows it is running
112
+ // unattended. A worker can unset it; the server guard does not read it.
113
+ const RUNNER_WORKER_ENV = 'AUTOBONGOS_WORKER';
114
+
115
+ // The verdict over a runner's REAL diff, shared by ship.js and the server: null, or
116
+ // { code, files, protected }. `files` are the runner's own; `protected` are ADR
117
+ // 0043 registry hits, which ADR 0307 §6 says never reach an unattended worker. The
118
+ // pre-claim fence reads those from the declared touches; this reads them from what
119
+ // the work actually changed. That is also what holds the one line in
120
+ // modules/lifecycle/routes/tasks.js that calls the server guard: that file is on
121
+ // the registry, so a runner cannot publish a change to it.
122
+ function diffRefusal({ files, protectedHits = [] } = {}) {
123
+ const own = [...new Set(matchRunnerSelf(files).map((h) => h.file))];
124
+ const prot = [...new Set((protectedHits || []).map((h) => (h && h.file) || h).filter((f) => typeof f === 'string' && f))];
125
+ if (!own.length && !prot.length) return null;
126
+ return { code: own.length ? 'self_modification' : 'runner_protected_path', files: own, protected: prot };
127
+ }
128
+
129
+ // The ship-time check: null outside a runner's worker, else diffRefusal.
130
+ // `matchProtected` is the ADR 0043 matcher, passed in because this file requires
131
+ // nothing; it is only called inside a worker.
132
+ function selfEditRefusal({ files, env = process.env, matchProtected = null } = {}) {
133
+ if (!env || env[RUNNER_WORKER_ENV] !== '1') return null;
134
+ return diffRefusal({ files, protectedHits: matchProtected ? matchProtected(files) : [] });
135
+ }
136
+
137
+ function selfEditMessage(refusal) {
138
+ const r = refusal || {};
139
+ const parts = [];
140
+ if (r.files && r.files.length) parts.push(`its own supervisor, safety checks or kill switch (${r.files.slice(0, 10).join(', ')})`);
141
+ if (r.protected && r.protected.length) parts.push(`a protected permission, migration, infrastructure or pipeline file (${r.protected.slice(0, 10).join(', ')})`);
142
+ return `An Autobongos runner may not change ${parts.join(', or ') || 'these files'}. This task goes to a person.`;
143
+ }
144
+
145
+ module.exports = {
146
+ RUNNER_SELF_SURFACES,
147
+ matchRunnerSelf,
148
+ RUNNER_TREE_RE,
149
+ isRunnerTree,
150
+ RUNNER_WORKER_ENV,
151
+ diffRefusal,
152
+ selfEditRefusal,
153
+ selfEditMessage,
154
+ };
@@ -164,7 +164,7 @@
164
164
  </a>
165
165
  </section>
166
166
 
167
- <p class="loading" id="loading">Reading your account…</p>
167
+ <p class="loading loading-mark loading-mark--page" id="loading" role="status">Reading your account…</p>
168
168
  <div class="alert" id="pageAlert" role="alert"></div>
169
169
 
170
170
  <!-- signed in: ships hidden, revealed once GET /me/cross-project answers -->
@@ -223,7 +223,7 @@
223
223
  <h2 id="aWaysIn-h">ways to sign in</h2>
224
224
  <p class="aSide">GitHub, Google or both. Each one opens this same account, with the same credits, ranks and history.</p>
225
225
  <div class="aBody">
226
- <p class="aWhat" id="waysLoading">Reading your ways to sign in…</p>
226
+ <p class="aWhat loading-mark loading-mark--inline" id="waysLoading" role="status">Reading your ways to sign in…</p>
227
227
  <ul class="mineRows" id="waysList"></ul>
228
228
  <p class="finePrint">Linking never joins two accounts. If the other one already has its own account here, we say so and change nothing.</p>
229
229
  <p class="projMsg" id="waysMsg" role="status" aria-live="polite"></p>
@@ -296,7 +296,7 @@
296
296
  <h2 id="aBuilding-h">projects you build on</h2>
297
297
  <p class="aSide">The projects you have joined as a builder. Only you see this list.</p>
298
298
  <div class="aBody">
299
- <p class="aWhat" id="buildingLoading">Reading the projects you build on…</p>
299
+ <p class="aWhat loading-mark loading-mark--inline" id="buildingLoading" role="status">Reading the projects you build on…</p>
300
300
  <p class="aWhat" id="buildingEmpty" hidden>You’re not building on any other projects yet.</p>
301
301
  <ul class="mineRows" id="buildingList"></ul>
302
302
  <p class="projMsg" id="buildingMsg" role="status" aria-live="polite"></p>
@@ -545,6 +545,51 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
545
545
  .alert summary{cursor:pointer;font-size:12.5px;font-weight:700;}
546
546
  .alert details p{margin-top:6px;font-family:var(--font-mono);font-size:12px;font-weight:500;overflow-wrap:anywhere;}
547
547
  .loading{font-size:14.5px;font-weight:600;color:var(--ink-faint);padding:14px 0;}
548
+ /* ── the loader (task 1005053; the hall's, task 1005043) ──────────────────────
549
+ The Bongos mark wherever an apex page waits for its data: the bar's own mark
550
+ (the ring, the core and the accent dot) as two masks on the placeholder the
551
+ page already had, whose words stay as its caption. The ring and the dot turn
552
+ together round the still core, easing out of and back into the logo's pose
553
+ each beat, and the ink breathes, after the Vibe skeleton the owner pointed at
554
+ (vibe.monday.com: 0.4 to 1 and back, 0.8s). Three sizes: the default for a
555
+ section, --page for a page with nothing else to show yet, --inline for a line
556
+ in a band or a list.
557
+
558
+ DESIGN.md holds it: CSS only, transform and filter only (the breath and the
559
+ arrival are filter: opacity(), not opacity), every colour a var() or a mix of
560
+ one; the black in the masks is alpha, not paint. The 20s floor is for the
561
+ ambient plates: a loader is the live thing on the page, so it keeps a loader's
562
+ tempo and its dot spends the accent. It arrives after 240ms, so a read that
563
+ answers faster never shows it; under the Kill Switch it is the still logo.
564
+ It is a grid, not a positioned dot: the ring and the dot share one cell, so
565
+ the dot stays on the ring whatever padding a page gives its placeholder. */
566
+ .loading-mark{
567
+ --lm-size:40px;
568
+ display:grid;justify-items:center;align-items:center;gap:12px;
569
+ padding:28px 0;text-align:center;color:var(--ink-faint);
570
+ animation:loading-mark-arrive 500ms cubic-bezier(.16,1,.3,1) 240ms both;
571
+ }
572
+ .loading-mark::before,.loading-mark::after{
573
+ content:"";grid-area:1/1;width:var(--lm-size);height:var(--lm-size);
574
+ -webkit-mask:var(--lm-shape) center/contain no-repeat;mask:var(--lm-shape) center/contain no-repeat;
575
+ animation:loading-mark-turn 1.6s cubic-bezier(.45,.05,.55,.95) infinite;
576
+ }
577
+ /* the ring and the core: one ink, breathing */
578
+ .loading-mark::before{
579
+ --lm-shape:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cpath d='M137 31 A78 78 0 1 0 170 66' fill='none' stroke='black' stroke-width='15' stroke-linecap='round'/%3E%3Ccircle cx='100' cy='100' r='55'/%3E%3C/svg%3E");
580
+ background:color-mix(in srgb,var(--ink) 30%,var(--bg-card));
581
+ animation:loading-mark-turn 1.6s cubic-bezier(.45,.05,.55,.95) infinite,loading-mark-breathe .8s ease-in-out infinite alternate-reverse;
582
+ }
583
+ /* the dot: the accent, laid exactly over the mark and turning with it */
584
+ .loading-mark::after{
585
+ --lm-shape:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Ccircle cx='156' cy='46' r='12'/%3E%3C/svg%3E");
586
+ background:var(--accent);
587
+ }
588
+ .loading-mark--page{--lm-size:64px;padding:72px 0;gap:16px;}
589
+ .loading-mark--inline{--lm-size:16px;grid-template-columns:var(--lm-size) minmax(0,1fr);justify-items:start;gap:8px;padding:0;text-align:left;}
590
+ @keyframes loading-mark-arrive{from{filter:opacity(0);transform:translateY(6px);}}
591
+ @keyframes loading-mark-turn{to{transform:rotate(1turn);}}
592
+ @keyframes loading-mark-breathe{from{filter:opacity(.45);}}
548
593
  .empty{border:2px dashed var(--line);border-radius:18px;padding:22px;text-align:center;}
549
594
  .empty p{font-size:14.5px;font-weight:600;color:var(--ink-soft);margin-bottom:14px;line-height:1.55;}
550
595
  .foot{position:relative;z-index:1;text-align:center;font-size:13px;font-weight:600;color:var(--foot-ink);padding:0 20px 34px;text-shadow:var(--chrome-glow);}
@@ -230,7 +230,7 @@
230
230
  same for every viewer but "Builders you may know", shown signed in like its menu item. -->
231
231
  <section id="home" data-view="home" aria-labelledby="homeH">
232
232
  <h2 id="homeH">Top builders</h2>
233
- <p class="commMsg" id="homeLoading" role="status">Reading the top builders…</p>
233
+ <p class="commMsg loading-mark" id="homeLoading" role="status">Reading the top builders…</p>
234
234
  <p class="projMsg err" id="homeFailed" role="alert" hidden></p>
235
235
  <ul class="homeCrowns" id="homeCrowns" hidden>
236
236
  <li class="homeCard"><h3 class="commSub">Builder of the week</h3><p class="crownWhen" id="crownWeekWhen"></p><div class="homeBody" id="crownWeek"></div></li>
@@ -282,7 +282,7 @@
282
282
  </div>
283
283
  <p class="commNote" id="boardNote">{{Currency}} earned across every project, since each builder started.</p>
284
284
  <div class="commFor" id="boardFor" hidden></div>
285
- <p class="commMsg" id="boardLoading" role="status">Reading the leaderboard…</p>
285
+ <p class="commMsg loading-mark" id="boardLoading" role="status">Reading the leaderboard…</p>
286
286
  <p class="projMsg err" id="boardFailed" role="alert" hidden></p>
287
287
  <p class="commMsg" id="boardEmpty" hidden></p>
288
288
  <ol class="commRows" id="boardList" data-unit="{{currency}}" aria-labelledby="boardH"></ol>
@@ -295,7 +295,7 @@
295
295
  every viewer, and states the rule in full (the spec: the formula is public). -->
296
296
  <section id="week" data-view="week" aria-labelledby="weekH" hidden>
297
297
  <h2 id="weekH">Builder of the week</h2>
298
- <p class="commMsg" id="weekLoading" role="status">Reading the builder of the week…</p>
298
+ <p class="commMsg loading-mark" id="weekLoading" role="status">Reading the builder of the week…</p>
299
299
  <p class="projMsg err" id="weekFailed" role="alert" hidden></p>
300
300
  <div class="commCrown" id="weekCrown" hidden></div>
301
301
  <p class="commMsg" id="weekNone" hidden></p>
@@ -339,7 +339,7 @@
339
339
  <section id="recruit" data-view="recruiters" aria-labelledby="recruitH" hidden>
340
340
  <h2 id="recruitH">Open to recruiters</h2>
341
341
  <p class="lede">Builders who keep their numbers hidden but are open to hearing from projects. Only project owners see who they are, and only by name.</p>
342
- <p class="commMsg" id="recruitLoading" role="status">Checking whether you can see this list…</p>
342
+ <p class="commMsg loading-mark" id="recruitLoading" role="status">Checking whether you can see this list…</p>
343
343
  <div class="commDoor" id="recruitDoor" hidden>
344
344
  <p>Sign in to see whether this list is open to you.</p>
345
345
  <a class="btn" id="recruitSignin" href="/api/bongos/auth/web/start?return=/community/recruiters&amp;repos=1">
@@ -137,7 +137,7 @@
137
137
  </a>
138
138
  </section>
139
139
 
140
- <p class="loading" id="loading">Reading your connections…</p>
140
+ <p class="loading loading-mark loading-mark--page" id="loading" role="status">Reading your connections…</p>
141
141
  <!-- a 403 from the connections routes is about the VIEWER: their platform
142
142
  profile is not active yet (ADR 0202 D4 §1), so they cannot take part -->
143
143
  <p class="mineEmpty" id="inactive" hidden>Connections open once your profile is active. It turns on the first time you sign in to a project with your {{productName}} account, in your browser.</p>
@@ -178,7 +178,7 @@
178
178
  </div>
179
179
 
180
180
  <main class="gld" id="guild">
181
- <p class="loading" id="loading">Reading this guild…</p>
181
+ <p class="loading loading-mark loading-mark--page" id="loading" role="status">Reading this guild…</p>
182
182
  <div class="alert" id="pageAlert" role="alert"></div>
183
183
 
184
184
  <!-- NO GUILD HERE: an unknown slug, a malformed one, and an unlisted guild seen
@@ -147,7 +147,7 @@
147
147
  </a>
148
148
  </section>
149
149
 
150
- <p class="loading" id="loading">Reading your invitations…</p>
150
+ <p class="loading loading-mark loading-mark--page" id="loading" role="status">Reading your invitations…</p>
151
151
 
152
152
  <!-- ══ INVITATIONS · JOIN BY ADDRESS (task 1003774, moved here by task 1004276) ══
153
153
  No Accept button on a project's invite — ADR 0335 retired that hub-only flip
@@ -798,9 +798,9 @@
798
798
  return !s || p.name.toLowerCase().indexOf(s) !== -1 || p.host.indexOf(s) !== -1;
799
799
  });
800
800
  }
801
- function pickNote(text) {
801
+ function pickNote(text, waiting) {
802
802
  var li = document.createElement('li');
803
- li.className = 'joinList__note';
803
+ li.className = 'joinList__note' + (waiting ? ' loading-mark loading-mark--inline' : '');
804
804
  li.setAttribute('role', 'presentation');
805
805
  li.textContent = text;
806
806
  return li;
@@ -838,7 +838,7 @@
838
838
  picker.active = -1;
839
839
  input.removeAttribute('aria-activedescendant');
840
840
  if (!picker.projects) {
841
- list.appendChild(pickNote('Loading projects…'));
841
+ list.appendChild(pickNote('Loading projects…', true));
842
842
  } else if (!picker.projects.length) {
843
843
  closePicker();
844
844
  return;