@bongos/core 1.19.1044 → 1.19.1046

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 (89) hide show
  1. package/.bongos-core.json +106 -121
  2. package/docs/adr/0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md +14 -2
  3. package/docs/adr/README.md +1 -1
  4. package/docs/copy-inventory.md +142 -158
  5. package/docs/copy-registry.json +173 -360
  6. package/docs/file-map.md +2 -3
  7. package/docs/module-api-changelog.md +4 -0
  8. package/docs/recipes/windows-builders.md +1 -1
  9. package/modules/hall-ui/public/atlas.html +1 -1
  10. package/modules/hall-ui/public/blockers.html +1 -1
  11. package/modules/hall-ui/public/board-room.html +1 -1
  12. package/modules/hall-ui/public/claim-action.js +250 -0
  13. package/modules/hall-ui/public/collab.css +1 -1
  14. package/modules/hall-ui/public/collab.html +1 -1
  15. package/modules/hall-ui/public/copy-desk.html +1 -1
  16. package/modules/hall-ui/public/deploy.html +1 -1
  17. package/modules/hall-ui/public/diagrams.html +1 -1
  18. package/modules/hall-ui/public/drachmae.html +1 -1
  19. package/modules/hall-ui/public/fleet.html +1 -1
  20. package/modules/hall-ui/public/gate.html +1 -1
  21. package/modules/hall-ui/public/goals-page.js +28 -44
  22. package/modules/hall-ui/public/goals-render.js +7 -28
  23. package/modules/hall-ui/public/goals.html +6 -3
  24. package/modules/hall-ui/public/government.html +1 -1
  25. package/modules/hall-ui/public/idea.html +1 -1
  26. package/modules/hall-ui/public/ideas.html +1 -1
  27. package/modules/hall-ui/public/index.html +1 -1
  28. package/modules/hall-ui/public/modules.html +1 -1
  29. package/modules/hall-ui/public/primer.html +1 -1
  30. package/modules/hall-ui/public/profile.html +1 -1
  31. package/modules/hall-ui/public/project-settings.html +1 -1
  32. package/modules/hall-ui/public/ranks.html +1 -1
  33. package/modules/hall-ui/public/roadmap.html +1 -1
  34. package/modules/hall-ui/public/{people.css → roster.css} +7 -11
  35. package/modules/hall-ui/public/roster.html +5 -5
  36. package/modules/hall-ui/public/roster.js +6 -7
  37. package/modules/hall-ui/public/roster.states.json +1 -1
  38. package/modules/hall-ui/public/sessions.html +1 -1
  39. package/modules/hall-ui/public/settings.html +2 -2
  40. package/modules/hall-ui/public/settings.js +2 -2
  41. package/modules/hall-ui/public/shell.js +3 -10
  42. package/modules/hall-ui/public/studio.html +1 -1
  43. package/modules/hall-ui/public/style.css +2 -9
  44. package/modules/hall-ui/public/task.html +1 -1
  45. package/modules/hall-ui/public/watch.html +1 -1
  46. package/modules/hall-ui/public/work.html +5 -2
  47. package/modules/hall-ui/public/work.js +35 -123
  48. package/modules/hall-ui/records/builders-roster.md +1 -1
  49. package/modules/hall-ui/records/roster-and-ledger.md +2 -2
  50. package/modules/platform-identity/account-visibility.js +3 -3
  51. package/modules/platform-identity/platform-identity.js +2 -2
  52. package/modules/platform-identity/routes/profile.js +1 -1
  53. package/modules/platform-identity/routes/sso.js +4 -3
  54. package/modules/platform-identity/scouting-authz.js +2 -2
  55. package/modules/platform-identity/tests/platform-identity.mjs +1 -1
  56. package/package-lock.json +2 -2
  57. package/package.json +1 -1
  58. package/release-notes.json +12 -0
  59. package/src/bongos/auth.js +9 -24
  60. package/src/bongos/serve-internal.js +13 -19
  61. package/src/module-api.js +1 -1
  62. package/tests/account_privacy_flags.mjs +1 -1
  63. package/tests/auth_resolve_failure.mjs +6 -6
  64. package/tests/claim_action.mjs +417 -0
  65. package/tests/core_upgrade_owner_door.mjs +1 -1
  66. package/tests/edit_hints.mjs +8 -8
  67. package/tests/effective_visibility_predicate.mjs +1 -1
  68. package/tests/goal_category_ui.mjs +3 -0
  69. package/tests/goal_criterion_confirm_ui.mjs +3 -0
  70. package/tests/goal_forest_page.mjs +3 -0
  71. package/tests/goal_invite_consent.mjs +3 -1
  72. package/tests/goal_mine_lens.mjs +3 -0
  73. package/tests/government_parity.mjs +1 -1
  74. package/tests/hall_builders_page_pass.mjs +2 -2
  75. package/tests/hall_mine_lens.mjs +3 -0
  76. package/tests/hall_nav.mjs +8 -8
  77. package/tests/hall_page_gate_map.mjs +14 -11
  78. package/tests/hall_roster_world.mjs +47 -78
  79. package/tests/module_contributions.mjs +0 -1
  80. package/tests/nav_permission_atoms.mjs +1 -1
  81. package/tests/{scouting_page_gate.mjs → page_gate_second_door.mjs} +34 -29
  82. package/tests/profile_rollup_consent.mjs +4 -4
  83. package/tests/profile_route.mjs +3 -26
  84. package/tests/scouting_owner_access_and_reach.mjs +4 -8
  85. package/modules/hall-ui/public/people.html +0 -79
  86. package/modules/hall-ui/public/people.js +0 -321
  87. package/modules/hall-ui/public/people.states.json +0 -89
  88. package/tests/people_directory_clip.mjs +0 -269
  89. package/tests/people_name_only.mjs +0 -263
@@ -235,7 +235,7 @@
235
235
 
236
236
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
237
237
 
238
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
238
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
239
239
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
240
240
  <script src="/builders/palette.js" defer></script>
241
241
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -243,6 +243,9 @@
243
243
  <script src="/builders/hall-kit.js"></script>
244
244
  <!-- shared TASK-DOMAIN helpers (window.OTBTaskLib) — priority/touches/prompt/refusal-copy, one copy for both boards (task 1002463). -->
245
245
  <script src="/builders/task-lib.js"></script>
246
+ <!-- task 1003458: the hall's ONE claim/release action + refusal copy (window.OTBClaimAction).
247
+ Before goals-render.js, which destructures errCode/errDetails/friendlyClaimError from it. -->
248
+ <script src="/builders/claim-action.js?v=2026-09-26-claimaction1"></script>
246
249
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
247
250
  <script src="/builders/brand-apply.js?v=2026-06-27-r75"></script>
248
251
  <script src="/builders/goals-lib.js?v=2026-08-21-goalslib1"></script>
@@ -258,7 +261,7 @@
258
261
  <!-- task 1003206: the page RENDERERS, the second half of the carve goals-lib.js
259
262
  began — goals-lib took the pure helpers, this takes the view painters.
260
263
  Loaded BEFORE goals-page.js, which calls window.OTBGoalsRender(ctx). -->
261
- <script src="/builders/goals-render.js?v=2026-08-24-goalsrender1"></script>
262
- <script src="/builders/goals-page.js?v=2026-08-21-goals7"></script>
264
+ <script src="/builders/goals-render.js?v=2026-09-26-claimaction1"></script>
265
+ <script src="/builders/goals-page.js?v=2026-09-26-claimaction1"></script>
263
266
  </body>
264
267
  </html>
@@ -134,7 +134,7 @@
134
134
 
135
135
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
136
136
 
137
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
137
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
138
138
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
139
139
  <script src="/builders/palette.js" defer></script>
140
140
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -52,7 +52,7 @@
52
52
 
53
53
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
54
54
 
55
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
55
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
56
56
  <script src="/builders/palette.js" defer></script>
57
57
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
58
58
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
@@ -225,7 +225,7 @@
225
225
 
226
226
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
227
227
 
228
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
228
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
229
229
  <script src="/builders/palette.js" defer></script>
230
230
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
231
231
  <!-- task 1003069 (R08) — the quick-filing form's routed result links a task
@@ -102,7 +102,7 @@
102
102
  <script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
103
103
  <!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
104
104
  BEFORE builders.js so the chrome ids exist when events are wired. -->
105
- <script src="/builders/shell.js?v=2026-09-26-swupdate" defer></script>
105
+ <script src="/builders/shell.js?v=2026-09-26-nopeople" defer></script>
106
106
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
107
107
  <script src="/builders/palette.js" defer></script>
108
108
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -80,7 +80,7 @@
80
80
 
81
81
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
82
82
 
83
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
83
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
84
84
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
85
85
  <script src="/builders/palette.js" defer></script>
86
86
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
@@ -115,7 +115,7 @@
115
115
 
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
118
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
119
119
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
120
120
  <script src="/builders/palette.js" defer></script>
121
121
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -187,7 +187,7 @@
187
187
 
188
188
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
189
189
  chrome exists before the page script runs. -->
190
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
190
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
191
191
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
192
192
  <script src="/builders/palette.js" defer></script>
193
193
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -58,7 +58,7 @@
58
58
 
59
59
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
60
60
 
61
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
61
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
62
62
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
63
63
  <script src="/builders/palette.js" defer></script>
64
64
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -68,7 +68,7 @@
68
68
 
69
69
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
70
70
  chrome exists before the page script runs. -->
71
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
71
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
72
72
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
73
73
  <script src="/builders/palette.js" defer></script>
74
74
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -41,7 +41,7 @@
41
41
 
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
44
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
45
45
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
46
46
  <script src="/builders/palette.js" defer></script>
47
47
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -1,13 +1,11 @@
1
- /* people.css — page-local styles for the cross-project builder directory
2
- (task 1003470, goal 1000081), served at /people — its legacy door /scouting
3
- was deleted in task 1003440 and now 301s here. Only the identity cell lives
4
- here: the
5
- ledger, the filter bar, the pager, the chips and the empty state are the
6
- shell's and the kit's. Tokens are the world's real names — no compat alias. */
1
+ /* roster.css — page-local styles for the Builders page (/roster). The identity
2
+ cell was shared with the cross-project directory at /people until task
3
+ 1003444 deleted that page from the hall; it lives here now. Only the
4
+ identity cell and the place/id cell live here: the ledger, the filter bar,
5
+ the pager and the empty state are the shell's and the kit's. Tokens are the
6
+ world's real names — no compat alias. */
7
7
 
8
- /* The who cell: avatar, then the name over the login. A row that carries an
9
- avatar centres its figures on it, so the numbers line up with the name. */
10
- #people-body .ledger td { vertical-align: middle; }
8
+ /* The who cell: avatar, then the name over the login. */
11
9
  .dir-who { display: inline-flex; align-items: center; gap: 11px; min-width: 0; }
12
10
  .dir-avatar {
13
11
  width: 30px; height: 30px; border-radius: 50%; flex: none;
@@ -33,5 +31,3 @@
33
31
  font-size: 11.5px;
34
32
  color: var(--ink-faint);
35
33
  }
36
- .dir-crafts { display: inline-flex; flex-wrap: wrap; gap: 6px; }
37
- .dir-none { color: var(--ink-faint); }
@@ -9,9 +9,9 @@
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
- <!-- task 1003441: the identity cell (avatar, name, login) is shared with the
13
- cross-project directory — one definition, both rosters. -->
14
- <link rel="stylesheet" href="/builders/people.css">
12
+ <!-- task 1003441: the identity cell (avatar, name, login). It was shared with
13
+ the cross-project /people page until task 1003444 deleted it. -->
14
+ <link rel="stylesheet" href="/builders/roster.css">
15
15
  </head>
16
16
  <body data-page="roster" data-page-title="Builders">
17
17
 
@@ -25,7 +25,7 @@
25
25
  rewritten by roster.js once the roster has loaded. -->
26
26
  <div class="page-head page-head--board">
27
27
  <h1 class="page-head__h">Builders</h1>
28
- <p class="page-head__sub" id="roster-sub">Who builds on this project, ranked by work shipped. This project's own ledger — unlike the cross-project directory, it lists everyone who builds here.</p>
28
+ <p class="page-head__sub" id="roster-sub">Who builds on this project, ranked by work shipped. This project's own ledger — everyone who builds here is listed.</p>
29
29
  </div>
30
30
 
31
31
  <main id="hall-main">
@@ -50,7 +50,7 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
53
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
54
54
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
55
55
  <script src="/builders/palette.js" defer></script>
56
56
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -5,9 +5,9 @@
5
5
  // Community nav group read as one real page plus a link into the home page. It
6
6
  // is a page now, and `/#leaderboard` redirects here (shell.js VIEW_REDIRECTS).
7
7
  //
8
- // PROJECT-OWN, not cross-project. /people is the cross-project directory behind
9
- // `project.curate`; this is the project's own ledger and is deliberately
10
- // ungated, because its one data source — GET /leaderboard
8
+ // PROJECT-OWN, not cross-project: the hall's only builder directory since task
9
+ // 1003444 deleted /people (cross-project discovery is the hub's, ADR 0337). It
10
+ // is deliberately ungated, because its one data source — GET /leaderboard
11
11
  // (modules/economy/routes/leaderboard.js) — is deliberately unauthenticated and
12
12
  // mirrors the public status surface. No visibility predicate is applied and that
13
13
  // is CORRECT: privacy suppresses cross-project surfaces only, so a builder
@@ -77,10 +77,9 @@
77
77
  function httpUrl(u) {
78
78
  return /^https?:\/\//i.test(String(u || '')) ? String(u) : null;
79
79
  }
80
- // The identity cell, shared with /people through people.css. The name opens
81
- // the builder's profile PAGE here: this is a project surface, so a local
82
- // profile id exists — unlike the cross-project directory, which can only
83
- // offer GitHub. Guarded on b.id so a row without one degrades to plain text
80
+ // The identity cell (roster.css). The name opens the builder's profile PAGE:
81
+ // this is a project surface, so a local profile id exists. Guarded on b.id
82
+ // so a row without one degrades to plain text
84
83
  // rather than a broken /profile?id=undefined link (task 1467).
85
84
  function whoHtml(b) {
86
85
  var name = esc(builderName(b));
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "Builders — this project's OWN roster, ranked by credits earned (task 1003441, goal 1000075). Was the #leaderboard hash view on index.html; /#leaderboard now redirects here. Deliberately UNGATED, because its one data source (GET /leaderboard) is deliberately unauthenticated and mirrors the public status surface — so unlike people.states.json the `out` state below renders the real page rather than a seal. Populated look: the hall-preview harness (scripts/hall-preview/fixtures/leaderboard.json carries thirteen builders with avatars, ranks, karma and tokens).",
2
+ "_": "Builders — this project's OWN roster, ranked by credits earned (task 1003441, goal 1000075). Was the #leaderboard hash view on index.html; /#leaderboard now redirects here. Deliberately UNGATED, because its one data source (GET /leaderboard) is deliberately unauthenticated and mirrors the public status surface — so unlike a gated page's states file the `out` state below renders the real page rather than a seal. Populated look: the hall-preview harness (scripts/hall-preview/fixtures/leaderboard.json carries thirteen builders with avatars, ranks, karma and tokens).",
3
3
  "page": "/builders/roster",
4
4
  "surface": "hall-ui",
5
5
  "stub": {
@@ -99,7 +99,7 @@
99
99
 
100
100
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
101
101
  chrome exists before the page script runs. -->
102
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
102
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
103
103
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
104
104
  <script src="/builders/palette.js" defer></script>
105
105
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -96,7 +96,7 @@
96
96
  <p class="scroll__lede">
97
97
  Your track record across every project you build on, shown on your
98
98
  public profile. Hiding it takes effect immediately everywhere —
99
- your profile, the People directory, and recruiting views.
99
+ your profile, the recruiting directory, and a recruiter's look-up of one builder.
100
100
  </p>
101
101
  <div id="cross-project-rows"></div>
102
102
  <p class="settings-status" id="cross-project-status" role="status" aria-live="polite"></p>
@@ -436,7 +436,7 @@
436
436
 
437
437
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
438
438
  chrome exists before the page scripts run. -->
439
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
439
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
440
440
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
441
441
  <script src="/builders/palette.js" defer></script>
442
442
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -490,7 +490,7 @@
490
490
  // GET/PATCH /api/bongos/me/cross-project (platform-identity module — hub only).
491
491
  // One on/off toggle: "show my cross-project record on my public profile".
492
492
  // Enforcement is server-side in the module's read functions, so flipping it
493
- // takes effect immediately on the profile and /people. The section
493
+ // takes effect immediately on the profile and /scouting. The section
494
494
  // reveals ONLY when the module's endpoint answers (404 on a vanilla instance →
495
495
  // stays hidden, no error state). P04 later re-homes this in the Profile rail
496
496
  // group; storage is account-global on the hub (not behavior_prefs).
@@ -562,7 +562,7 @@
562
562
  privacyRow('visibility', 'Public profile page', isPublic ? 'Public' : 'Private', isPublic,
563
563
  'Public means anyone can open your profile page and find you by name. Private hides the page from everyone — including you: it answers "not found" to every visitor, and you read your own record here in Settings instead.', '') +
564
564
  privacyRow('discoverable', 'Discoverable by recruiters', discoverable ? 'On' : 'Off', discoverable,
565
- `Recruiters looking for builders can find you. ${grant}${despitePrivate} Turning this off removes you from both recruiting surfaces — the People directory, and a recruiter's look-up of one builder — straight away.`,
565
+ `Recruiters looking for builders can find you. ${grant}${despitePrivate} Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter's look-up of one builder — straight away.`,
566
566
  follows ? ' <span class="voices__default">· follows your profile setting</span>' : '');
567
567
  }
568
568
 
@@ -92,7 +92,6 @@
92
92
  goals: '<circle cx="9" cy="9" r="6.5"/><circle cx="9" cy="9" r="3.5"/><circle cx="9" cy="9" r="0.8" fill="currentColor"/>',
93
93
  roadmap: '<path d="M3 15.5V3.5l4 2 4-2 4 2v10l-4-2-4 2-4-2Z"/><path d="M7 3.5v10M11 5.5v10"/>',
94
94
  leaderboard: '<path d="M3.5 15.5v-6M9 15.5V3.5m5.5 12v-9"/>',
95
- people: '<circle cx="6.5" cy="6.5" r="2.5"/><path d="M2.5 15c.5-2.4 2-3.7 4-3.7s3.5 1.3 4 3.7"/><circle cx="12.5" cy="7" r="2"/><path d="M12 11.5c1.8.1 3 1.3 3.5 3.3"/>',
96
95
  docs: '<path d="M4 3.5h7l3 3v10H4v-13Z"/><path d="M11 3.5v3h3"/>',
97
96
  // task 1003117 (R06 of goal 1000074): the Studio (artist mode). A sprout —
98
97
  // the zen-garden reading of "a place an artist wants to be", so it does not
@@ -293,15 +292,9 @@
293
292
  // page, so no `view` key. Labelled Builders — 'roster' is the word
294
293
  // modules/hall-ui/CLAUDE.md uses, but Builders is what a reader scans for.
295
294
  { id: 'roster', label: 'Builders', icon: 'leaderboard', href: '/roster', gate: 'signed' },
296
- // task 1002577: the cross-project builder directory (/scouting graduated
297
- // into the hall). Gated to MATCH its data — /api/bongos/scouting checks
298
- // `project.curate`, whose catalog floor is METIC. This read 'archon' until
299
- // task 1002710: the nav was stricter than the wall, so a Metic could open
300
- // /people by URL but never saw the link. Cosmetic either way —
301
- // requireProjectCuratePage is the wall — but a nav that disagrees with the
302
- // gate teaches the ladder wrong. The module gate hides the tab on
303
- // instances without the platform-identity hub.
304
- { id: 'people', label: 'People', icon: 'people', href: '/people', gate: 'government', module: 'platform-identity' },
295
+ // task 1003444: 'People', the cross-project directory, was deleted (owner
296
+ // call 2026-09-25, ADR 0337 D5). Cross-project discovery is the hub's, and
297
+ // /people redirects to /roster.
305
298
  ] },
306
299
  { group: 'Docs', gate: 'signed', items: [
307
300
  // task 1002710: primer + diagrams were gate:'member' (rank !== xenos) to
@@ -66,7 +66,7 @@
66
66
 
67
67
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
68
68
 
69
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
69
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
70
70
  <script src="/builders/palette.js" defer></script>
71
71
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
72
72
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -3391,15 +3391,8 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
3391
3391
  does not exist, so "Someone helped" rendered full-size on every row. */
3392
3392
  .btn--sm { min-height: 26px; padding: 0 11px; font-size: 12.5px; }
3393
3393
 
3394
- /* people.js:45 — the builder's name cell. The hall's only definition used to
3395
- live in scouting.html's inline <style>, so the same data read bold on
3396
- Scouting and plain on People. It lives here now, and scouting.html itself
3397
- went with task 1003440. */
3398
- .who { font-weight: 600; }
3399
-
3400
- /* people.js:105 — notice() sets body.className='note'. .note lives in
3401
- my-projects.css, which People never loads, so every gated / empty / error
3402
- message on that page rendered unstyled. */
3394
+ /* The notice line: roster.js and blockers.js notice() set body.className='note'
3395
+ for an empty / error message, and neither page's sheet defines it. */
3403
3396
  .note { margin: 0; font-size: 13.5px; color: var(--ink-soft); }
3404
3397
 
3405
3398
  .empty-state { padding: 8px 0; }
@@ -57,7 +57,7 @@
57
57
 
58
58
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
59
59
  time so the chrome exists before the deferred page scripts execute. -->
60
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
60
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
61
61
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
62
62
  <script src="/builders/palette.js" defer></script>
63
63
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -162,7 +162,7 @@
162
162
 
163
163
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
164
164
  chrome exists before the page script runs. -->
165
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
165
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
166
166
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
167
167
  <script src="/builders/palette.js" defer></script>
168
168
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -171,7 +171,7 @@
171
171
 
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
- <script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
174
+ <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
175
175
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
176
176
  <script src="/builders/palette.js" defer></script>
177
177
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -180,13 +180,16 @@
180
180
  <script src="/builders/hall-kit.js" defer></script>
181
181
  <!-- shared TASK-DOMAIN helpers (window.OTBTaskLib) — priority/touches/prompt/refusal-copy, one copy for both boards (task 1002463). -->
182
182
  <script src="/builders/task-lib.js" defer></script>
183
+ <!-- task 1003458: the hall's ONE claim/release action + refusal copy (window.OTBClaimAction).
184
+ Before work.js, which destructures friendlyClaimError from it. -->
185
+ <script src="/builders/claim-action.js?v=2026-09-26-claimaction1" defer></script>
183
186
  <!-- the hall's one definition of "mine" (window.OTBMineLens) — goals you are on, claims you hold, ships you made; shared with the goal board. -->
184
187
  <script src="/builders/mine-lens.js" defer></script>
185
188
  <!-- the Completed tab's project-wide momentum card (window.OTBWorkSummary), carved out of work.js. -->
186
189
  <script src="/builders/work-summary.js" defer></script>
187
190
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail" defer></script>
188
191
  <script src="/builders/brand-apply.js?v=2026-06-19-r56" defer></script>
189
- <script src="/builders/work.js?v=2026-07-03-workboard3" defer></script>
192
+ <script src="/builders/work.js?v=2026-09-26-claimaction1" defer></script>
190
193
  <script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
191
194
  </body>
192
195
  </html>
@@ -36,6 +36,10 @@
36
36
  // file's exact variants (date-only fmtDate; a 2200ms clearing toast).
37
37
  // fmtAgo / toAuth / getJSON / postJSON stay local below.
38
38
  const { escapeHtml, $, fmtNum, errCode, errDetails } = window.OTB; // task 1002462: envelope readers
39
+ // The hall's ONE claim/release refusal copy (task 1003458). This board's own
40
+ // copy is gone: a second surface that wants a claim button consumes this
41
+ // instead of lifting the switch, which is how the two drifted apart before.
42
+ const { friendlyClaimError, makeClaimAction } = window.OTBClaimAction;
39
43
  // Shared task-domain helpers (task 1002463). These lived in two copies here and
40
44
  // in the other board and had already drifted — goals-page.js had silently lost
41
45
  // friendlyBatchReason's MODULE_CONFLICT case. Destructured so every call site
@@ -180,6 +184,18 @@
180
184
  return data;
181
185
  }
182
186
 
187
+ // The hall's ONE claim/release action (task 1003458). Everything this board
188
+ // used to hand-roll four times — the POST, the refusal sentence, the
189
+ // stale-view refresh, the all-or-nothing batch report — lives in
190
+ // claim-action.js now; doClaim/doRelease/doBulkClaim/doBulkRelease below keep
191
+ // only what is genuinely this page: button text, the re-entrancy guard, the
192
+ // confirm() wording and the selection. friendlyBatchReason is injected rather
193
+ // than imported there, so claim-action.js stays dependency-free.
194
+ // refreshLive is a hoisted declaration; toast and API are already bound above.
195
+ const claimAction = makeClaimAction({
196
+ postJSON, toast, API, refresh: refreshLive, describeBatchReason: friendlyBatchReason,
197
+ });
198
+
183
199
  // ---- paginated lists (task 1931/1955 → kit, task 2170) --------------------
184
200
  // All four Work-page lists paginate through the shared OTBKit pager (the
185
201
  // canonical copy of the helper that used to live here). localStorage keys are
@@ -627,51 +643,6 @@
627
643
  }
628
644
  }
629
645
  }
630
-
631
- // ---- claim / release actions ---------------------------------------------
632
-
633
- // Map the server's typed error code to a builder-readable line. The codes are
634
- // exactly those POST /claims and POST /claims/:id/resolve can return.
635
- function friendlyActionError(status, data) {
636
- // task 1002462: read the ADR 0116 envelope. `data.error` is an OBJECT, so the
637
- // old `const code = data && data.error` compared a string to an object and
638
- // EVERY case below fell through to the generic default — a wrong-but-plausible
639
- // toast on every claim/release refusal.
640
- const det = errDetails(data);
641
- switch (errCode(data)) {
642
- case 'ALREADY_CLAIMED':
643
- return 'Someone else claimed that task first. The board will refresh.';
644
- case 'TASK_NOT_READY':
645
- return 'That task is no longer ready to be claimed.';
646
- case 'TOUCHES_CONFLICT': {
647
- const titles = Array.isArray(det.conflicts) ? det.conflicts.map((c) => c.title).filter(Boolean) : [];
648
- return titles.length
649
- ? `That task overlaps an active claim: ${titles.join('; ')}. Wait for it to ship.`
650
- : 'That task overlaps another active claim. Wait for it to ship.';
651
- }
652
- case 'DEPS_NOT_SHIPPED':
653
- return 'That task waits on unshipped dependencies. Pick another for now.';
654
- case 'rank_too_low_for_task':
655
- case 'XENOS_RESTRICTED':
656
- case 'INSUFFICIENT_RANK':
657
- return 'Your rank is not yet high enough for that task.';
658
- case 'newcomer_must_ship_first':
659
- return det.what_to_do || 'Ship your current claim before opening another.';
660
- case 'NOT_YOUR_CLAIM':
661
- return 'That claim is not yours to release.';
662
- case 'ALREADY_RESOLVED':
663
- return 'That claim was already resolved. The board will refresh.';
664
- case 'BUILDER_INACTIVE':
665
- return 'Your account is inactive. Contact an Archon.';
666
- default:
667
- return `The board could not complete that (${status}). Try again.`;
668
- }
669
- }
670
-
671
- // Maps a single batch-item failure reason (POST /claims/batch,
672
- // /claims/batch/validate) to a builder-readable fragment. Distinct from
673
- // friendlyActionError above because a batch item's shape differs (reason,
674
- // not error; extra fields like conflictsWithTaskId/module_key).
675
646
  async function copyText(text) {
676
647
  try { await navigator.clipboard.writeText(text); return true; }
677
648
  catch (_) {
@@ -735,18 +706,15 @@
735
706
  async function scheduleValidate(key, ids) {
736
707
  if (validateDebounce) clearTimeout(validateDebounce);
737
708
  validateDebounce = setTimeout(async () => {
738
- try {
739
- const result = await postJSON(`${API}/claims/batch/validate`, { task_ids: ids });
740
- validateCache.key = key;
741
- validateCache.parallelSafe = !!result.parallel_safe;
742
- validateCache.byId = new Map((result.results || []).map((r) => [String(r.task_id), r]));
743
- } catch (_) {
744
- // Best-effort — leave Parallel disabled-with-a-generic-title on failure
745
- // rather than risk copying a prompt for tasks we couldn't verify.
746
- validateCache.key = key;
747
- validateCache.parallelSafe = false;
748
- validateCache.byId = new Map();
749
- }
709
+ // The dry run lives in claim-action.js with the rest of the claim path; it
710
+ // is best-effort there, so a failure lands as parallelSafe:false with an
711
+ // empty map rather than a throw — Parallel stays disabled with a generic
712
+ // title instead of copying a prompt for tasks nobody verified. The
713
+ // debounce, the cache key and the re-render stay here: page furniture.
714
+ const out = await claimAction.validateMany(ids);
715
+ validateCache.key = key;
716
+ validateCache.parallelSafe = out.parallelSafe;
717
+ validateCache.byId = out.byId;
750
718
  renderBulkToolbar();
751
719
  }, 350);
752
720
  }
@@ -911,33 +879,8 @@
911
879
  const btn = $('#bulk-claim-btn');
912
880
  if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
913
881
  try {
914
- if (ids.length === 1) {
915
- await postJSON(`${API}/claims`, { task_id: ids[0], bind_session: false });
916
- toast(`You claimed #${ids[0]}. Open a terminal to start work.`);
917
- } else {
918
- await postJSON(`${API}/claims/batch`, { task_ids: ids, bind_session: false });
919
- toast(`Claimed ${ids.length} tasks. Open a terminal to start work.`);
920
- }
921
- selection.claim.clear();
922
- await refreshLive();
923
- } catch (err) {
924
- if (err && err.message === 'redirecting to auth') return;
925
- // task 1002462: same envelope defect — this tested `err.data.error === 'batch_failed'`
926
- // against an object, so an all-or-nothing batch refusal rendered as the generic toast
927
- // and the per-task reasons were dropped on the floor.
928
- if (err.status === 409 && errCode(err.data) === 'batch_failed') {
929
- // One read covers both envelopes — errDetails falls back to the body (task 1003657).
930
- const bdet = errDetails(err.data);
931
- const failures = Array.isArray(bdet.failures) ? bdet.failures : [];
932
- const lines = failures.map((f) => `#${f.task_id} (${friendlyBatchReason(f)})`);
933
- toast(lines.length
934
- ? `The batch could not be claimed — ${lines.join('; ')}.`
935
- : 'The batch could not be claimed. It was left untouched (all-or-nothing).');
936
- await refreshLive().catch(() => {});
937
- } else {
938
- toast(friendlyActionError(err.status, err.data));
939
- if (err.status === 409) await refreshLive().catch(() => {});
940
- }
882
+ const out = await claimAction.claimMany(ids);
883
+ if (out.ok) selection.claim.clear();
941
884
  } finally {
942
885
  renderBulkToolbar();
943
886
  }
@@ -952,22 +895,8 @@
952
895
  const btn = $('#bulk-release-btn');
953
896
  if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
954
897
  try {
955
- if (entries.length === 1) {
956
- await postJSON(`${API}/claims/${encodeURIComponent(entries[0][0])}/resolve`, { outcome: 'abandoned' });
957
- toast(`Released #${entries[0][1]}. It returns to the queue.`);
958
- } else {
959
- const result = await postJSON(`${API}/claims/batch/release`, { claim_ids: entries.map(([cid]) => Number(cid)) });
960
- const released = Array.isArray(result.released) ? result.released.length : 0;
961
- const failed = Array.isArray(result.failures) ? result.failures.length : 0;
962
- toast(failed
963
- ? `Released ${released} of ${entries.length} — ${failed} could not be released.`
964
- : `Released ${released} tasks. They return to the queue.`);
965
- }
966
- selection.release.clear();
967
- await refreshLive();
968
- } catch (err) {
969
- if (err && err.message === 'redirecting to auth') return;
970
- toast(friendlyActionError(err.status, err.data));
898
+ const out = await claimAction.releaseMany(entries);
899
+ if (out.ok) selection.release.clear();
971
900
  } finally {
972
901
  renderBulkToolbar();
973
902
  }
@@ -979,18 +908,10 @@
979
908
  pending.add(key);
980
909
  if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
981
910
  try {
982
- // bind_session:false — a web claim has no working session, so leave it
983
- // unbound (see claims.js resolveCreatorSessionId) and the builder can ship
984
- // it later from any of their own terminals.
985
- await postJSON(`${API}/claims`, { task_id: Number(taskId), bind_session: false });
986
- toast(`You claimed #${taskId}. Open a terminal to start work.`);
987
- await refreshLive();
988
- } catch (err) {
989
- if (err && err.message === 'redirecting to auth') return;
990
- toast(friendlyActionError(err.status, err.data));
991
- // A conflict/already-claimed means our view is stale — pull fresh truth.
992
- if (err.status === 409) await refreshLive().catch(() => {});
993
- if (btn) { btn.disabled = false; btn.textContent = 'Claim'; }
911
+ const out = await claimAction.claim(taskId);
912
+ // Leave the button disabled while the page is navigating to sign-in —
913
+ // re-enabling it invites a click that cannot land.
914
+ if (!out.ok && !out.redirected && btn) { btn.disabled = false; btn.textContent = 'Claim'; }
994
915
  } finally {
995
916
  pending.delete(key);
996
917
  }
@@ -1003,17 +924,8 @@
1003
924
  pending.add(key);
1004
925
  if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
1005
926
  try {
1006
- // No DELETE /claims/:id exists — release is resolve(outcome=abandoned),
1007
- // the same path /builder-release uses. Scoped to the caller's own claim
1008
- // server-side (NOT_YOUR_CLAIM).
1009
- await postJSON(`${API}/claims/${encodeURIComponent(claimId)}/resolve`, { outcome: 'abandoned' });
1010
- toast(`Released #${taskId}. It returns to the queue.`);
1011
- await refreshLive();
1012
- } catch (err) {
1013
- if (err && err.message === 'redirecting to auth') return;
1014
- toast(friendlyActionError(err.status, err.data));
1015
- if (err.status === 409) await refreshLive().catch(() => {});
1016
- if (btn) { btn.disabled = false; btn.textContent = 'Release'; }
927
+ const out = await claimAction.release(claimId, taskId);
928
+ if (!out.ok && !out.redirected && btn) { btn.disabled = false; btn.textContent = 'Release'; }
1017
929
  } finally {
1018
930
  pending.delete(key);
1019
931
  }
@@ -2,7 +2,7 @@
2
2
 
3
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
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.
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, until task 1003444 deleted `/people` and the sheet became `roster.css`.
6
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
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
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).