@bongos/core 1.24.29 → 1.24.31

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 (113) hide show
  1. package/.bongos-core.json +198 -108
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +12 -0
  4. package/clients/bongos-client/index.cjs +12 -0
  5. package/clients/bongos-client/index.d.ts +12 -0
  6. package/clients/bongos-client/index.mjs +12 -0
  7. package/docs/api/openapi.json +174 -1
  8. package/docs/api-reference.md +12 -2
  9. package/docs/copy-inventory.md +226 -224
  10. package/docs/copy-registry.json +583 -549
  11. package/docs/module-api-changelog.md +2 -0
  12. package/docs/page-inventory.json +18 -37
  13. package/docs/page-readings.json +306 -361
  14. package/modules/copy-desk/module.json +1 -1
  15. package/modules/copy-desk/routes/copy-desk-studio.js +103 -0
  16. package/modules/copy-desk/tests/copy_no_cms.mjs +2 -2
  17. package/modules/government/db.js +29 -0
  18. package/modules/government/docket.js +61 -4
  19. package/modules/government/routes/government.js +2 -0
  20. package/modules/hall-ui/public/ai-studio.html +1 -1
  21. package/modules/hall-ui/public/atlas.html +1 -1
  22. package/modules/hall-ui/public/blockers.html +1 -1
  23. package/modules/hall-ui/public/board-room.html +1 -1
  24. package/modules/hall-ui/public/builders.js +3 -79
  25. package/modules/hall-ui/public/city.html +1 -1
  26. package/modules/hall-ui/public/collab.html +1 -1
  27. package/modules/hall-ui/public/commands.html +1 -1
  28. package/modules/hall-ui/public/copy-desk.html +1 -1
  29. package/modules/hall-ui/public/craft-mode.js +1 -1
  30. package/modules/hall-ui/public/deploy.html +1 -1
  31. package/modules/hall-ui/public/diagrams.html +1 -1
  32. package/modules/hall-ui/public/drachmae.html +1 -1
  33. package/modules/hall-ui/public/fleet.html +1 -1
  34. package/modules/hall-ui/public/gate.html +1 -1
  35. package/modules/hall-ui/public/goal-map.html +1 -1
  36. package/modules/hall-ui/public/goals.html +1 -1
  37. package/modules/hall-ui/public/government.html +1 -1
  38. package/modules/hall-ui/public/hall-render.js +68 -167
  39. package/modules/hall-ui/public/hall-widgets.js +4 -45
  40. package/modules/hall-ui/public/home-city-draw.js +583 -0
  41. package/modules/hall-ui/public/home-city.css +102 -0
  42. package/modules/hall-ui/public/home-city.js +339 -0
  43. package/modules/hall-ui/public/home-gallery.css +129 -0
  44. package/modules/hall-ui/public/home-gallery.js +349 -0
  45. package/modules/hall-ui/public/home-studio.css +72 -0
  46. package/modules/hall-ui/public/home-studio.js +203 -0
  47. package/modules/hall-ui/public/idea.html +1 -1
  48. package/modules/hall-ui/public/ideas.html +1 -1
  49. package/modules/hall-ui/public/index.html +9 -7
  50. package/modules/hall-ui/public/index.states.json +73 -8
  51. package/modules/hall-ui/public/market-watch-item.html +1 -1
  52. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  53. package/modules/hall-ui/public/market-watch-settings.js +6 -1
  54. package/modules/hall-ui/public/market-watch.html +141 -54
  55. package/modules/hall-ui/public/market-watch.js +54 -0
  56. package/modules/hall-ui/public/market-watch.states.json +2 -1
  57. package/modules/hall-ui/public/modules.html +1 -1
  58. package/modules/hall-ui/public/my-agents.html +1 -1
  59. package/modules/hall-ui/public/primer.html +1 -1
  60. package/modules/hall-ui/public/profile.html +1 -1
  61. package/modules/hall-ui/public/project-settings.html +1 -1
  62. package/modules/hall-ui/public/ranks.html +1 -1
  63. package/modules/hall-ui/public/roadmap.html +1 -1
  64. package/modules/hall-ui/public/roster.html +1 -1
  65. package/modules/hall-ui/public/sessions.html +1 -1
  66. package/modules/hall-ui/public/settings.html +1 -1
  67. package/modules/hall-ui/public/shell.js +2 -3
  68. package/modules/hall-ui/public/street.html +1 -1
  69. package/modules/hall-ui/public/studio.html +9 -1
  70. package/modules/hall-ui/public/style.css +74 -27
  71. package/modules/hall-ui/public/task.html +1 -1
  72. package/modules/hall-ui/public/thinking.html +1 -1
  73. package/modules/hall-ui/public/tweak-editor.html +1 -1
  74. package/modules/hall-ui/public/watch.html +1 -1
  75. package/modules/hall-ui/public/work.html +1 -1
  76. package/modules/hall-ui/records/home-city.md +189 -0
  77. package/modules/hall-ui/records/loader.md +1 -1
  78. package/modules/hall-ui/records/market-watch-settings.md +16 -3
  79. package/modules/hall-ui/records/market-watch.md +3 -1
  80. package/modules/lifecycle/home-city.js +388 -0
  81. package/modules/lifecycle/home-gallery.js +320 -0
  82. package/modules/lifecycle/migrations/lifecycle_020_work_appreciations.sql +36 -0
  83. package/modules/lifecycle/module.json +2 -0
  84. package/modules/lifecycle/routes/home-city.js +29 -0
  85. package/modules/lifecycle/routes/home-gallery.js +37 -0
  86. package/modules/market-watch/routes/settings.js +1 -1
  87. package/package-lock.json +2 -2
  88. package/package.json +1 -1
  89. package/release-notes.json +38 -0
  90. package/scripts/bongos/leak-scan-allowlist.js +8 -0
  91. package/scripts/hall-preview/README.md +1 -0
  92. package/scripts/hall-preview/server.js +10 -0
  93. package/src/bongos/serve-internal.js +17 -3
  94. package/src/module-api.js +1 -1
  95. package/tests/claim_from_task_and_home.mjs +8 -43
  96. package/tests/hall_board_world.mjs +10 -12
  97. package/tests/hall_builders_page_pass.mjs +18 -27
  98. package/tests/hall_home_ships_bounded.mjs +4 -3
  99. package/tests/hall_loading.mjs +1 -1
  100. package/tests/hall_nav.mjs +11 -4
  101. package/tests/hall_page_gate_map.mjs +7 -4
  102. package/tests/home_bounded_reads.mjs +16 -31
  103. package/tests/home_city.mjs +580 -0
  104. package/tests/home_city_widget.mjs +260 -0
  105. package/tests/home_gallery.mjs +255 -0
  106. package/tests/home_gallery_widget.mjs +220 -0
  107. package/tests/home_studio.mjs +169 -0
  108. package/tests/home_studio_widget.mjs +157 -0
  109. package/tests/leaderboard_roles.mjs +3 -2
  110. package/tests/market_watch_pages.mjs +162 -0
  111. package/tests/nav_permission_atoms.mjs +2 -2
  112. package/modules/hall-ui/public/market-watch-settings.html +0 -115
  113. package/modules/hall-ui/public/market-watch-settings.states.json +0 -14
@@ -7,7 +7,7 @@
7
7
  "category": { "parent": "quality" },
8
8
  "default": true,
9
9
  "maintenance": { "status": "core-maintained" },
10
- "contributes": { "routes": ["copy-desk", "copy-desk-artist", "copy-desk-release"], "migrations": true, "skills": ["tweak"], "pollers": ["stuck-page-digest", "auto-close-rounds"] },
10
+ "contributes": { "routes": ["copy-desk", "copy-desk-artist", "copy-desk-release", "copy-desk-studio"], "migrations": true, "skills": ["tweak"], "pollers": ["stuck-page-digest", "auto-close-rounds"] },
11
11
  "provides": ["copy-desk.artistStats", "copy-desk.stuckPageTweaksFor"],
12
12
  "consumes": ["lifecycle"]
13
13
  }
@@ -0,0 +1,103 @@
1
+ // modules/copy-desk/routes/copy-desk-studio.js — the artist's studio, its copy-desk
2
+ // half (task 1005061, Home v16 step 4; mock docs/design/mocks/hall/home/v16-studio.html).
3
+ //
4
+ // GET /copy-desk/studio — one bounded read for three of the studio's shelves:
5
+ //
6
+ // easel the page round the caller holds (the Tweak/CopyWrite Flow's `resume`), or,
7
+ // with nothing held, the page the flow would recommend next.
8
+ // waiting "Waiting for new inspiration": pages whose words changed since an artist
9
+ // last designed them (the recommendation's `changed` tier, drift from the
10
+ // page readings), most changed lines first, at most MAX_WAITING, with the
11
+ // whole count.
12
+ // words "Words to tune": the newest open copy-desk flags, at most MAX_WORDS.
13
+ //
14
+ // Everything is composed by the flow's own reader (page-status.js composeNext over
15
+ // the inventory, the readings, the ledger's page rounds and the page asks), so the
16
+ // studio and the Studio page can never disagree about a page. Read only: it writes no
17
+ // row and returns no replacement wording (copy_no_cms.mjs holds every route file here
18
+ // to that). The other two shelves, the works the caller pinned and signed, are the
19
+ // lifecycle module's (GET /home/studio/works).
20
+ //
21
+ // Apart from routes/copy-desk.js because that file sits at the 1,500-line ratchet.
22
+
23
+ 'use strict';
24
+
25
+ const express = require('express');
26
+ const api = require('../../../src/module-api');
27
+ const flags = require('../flags');
28
+ const pageData = require('../page-data');
29
+ const pageStatus = require('../page-status');
30
+
31
+ const log = api.logger('copy-desk');
32
+
33
+ const MAX_WAITING = 4;
34
+ const MAX_WORDS = 3;
35
+ const SURFACE_NAMES = Object.freeze({ landing: 'The public site', builders: 'The builders’ hall', status: 'The status page' });
36
+
37
+ function editorPath(pageId) { return `/tweak-editor?page=${encodeURIComponent(pageId)}`; }
38
+
39
+ // The studio's shape from the flow's answer and the newest open flags. Pure.
40
+ function composeStudio({ next, flagRows }) {
41
+ const n = next || {};
42
+ const all = [n.recommended, ...(n.alternatives || [])].filter(Boolean);
43
+ const changed = all.filter((c) => c.tier === 'changed' && c.changed_lines > 0)
44
+ .sort((a, b) => b.changed_lines - a.changed_lines);
45
+ const r = n.resume;
46
+ const easel = r
47
+ ? { held: true, page_id: r.page_id, title: r.title, place: SURFACE_NAMES[r.surface] || null, round: r.round || null, open: editorPath(r.page_id) }
48
+ : (n.recommended
49
+ ? { held: false, page_id: n.recommended.page_id, title: n.recommended.title, place: SURFACE_NAMES[n.recommended.surface] || null, why: n.recommended.why || null, open: editorPath(n.recommended.page_id) }
50
+ : null);
51
+ return {
52
+ easel,
53
+ waiting: {
54
+ total: changed.length,
55
+ pages: changed.slice(0, MAX_WAITING).map((c) => ({
56
+ page_id: c.page_id, title: c.title, place: SURFACE_NAMES[c.surface] || null, changed_lines: c.changed_lines, open: editorPath(c.page_id),
57
+ })),
58
+ },
59
+ words: (flagRows || []).slice(0, MAX_WORDS).map((f) => ({
60
+ id: String(f.id),
61
+ text: f.text_at_flag || null,
62
+ reason: f.reason || '',
63
+ place: SURFACE_NAMES[f.surface] || null,
64
+ })),
65
+ };
66
+ }
67
+
68
+ module.exports = function buildCopyDeskStudioRouter() {
69
+ const router = express.Router();
70
+
71
+ // rank: any-builder — GET /copy-desk/studio, the caller's easel, the pages waiting
72
+ // for an artist and the newest open flags. A work list, like /copy-desk/next and
73
+ // /copy-desk/queue, which any signed-in builder may already read.
74
+ router.get('/copy-desk/studio', api.requireBuilder, async (req, res) => {
75
+ try {
76
+ const inv = pageData.loadInventory();
77
+ const lifecycle = api.resolveOptional && api.resolveOptional('lifecycle');
78
+ const readsOk = inv.ok && !!lifecycle && typeof lifecycle.listPageTweakTasks === 'function';
79
+ const readings = pageData.loadReadings();
80
+ const [tasks, asks, flagRows] = await Promise.all([
81
+ readsOk ? lifecycle.listPageTweakTasks() : Promise.resolve([]),
82
+ readsOk ? flags.pageAskSummary() : Promise.resolve([]),
83
+ flags.listFlags({ status: 'open', scopes: flags.FLAG_SCOPES, limit: MAX_WORDS }),
84
+ ]);
85
+ // Every candidate, not the flow's four alternatives: the waiting shelf is a filter
86
+ // over all of them. Bounded by the inventory's pages, never the corpus.
87
+ const next = readsOk ? pageStatus.composeNext({
88
+ inventory: inv.data, readings: readings.ok ? readings.data : null, tasks, asks, builderId: req.builder.id,
89
+ alternatives: (inv.data.pages || []).length,
90
+ }) : null;
91
+ res.set('Cache-Control', 'private, no-store');
92
+ res.json(composeStudio({ next, flagRows }));
93
+ } catch (err) {
94
+ log.error({ err }, 'GET /copy-desk/studio failed');
95
+ if (!res.headersSent) res.fail('copy_studio_failed', 500);
96
+ }
97
+ });
98
+
99
+ return router;
100
+ };
101
+ module.exports.composeStudio = composeStudio;
102
+ module.exports.MAX_WAITING = MAX_WAITING;
103
+ module.exports.MAX_WORDS = MAX_WORDS;
@@ -74,8 +74,8 @@ const MODULE_FILES = ['registry.js', 'flags.js', 'proposals.js', 'pages.js', 'pa
74
74
 
75
75
  console.log('copy-desk: NO LIVE CMS');
76
76
 
77
- await test('the route files are exactly the three the module declares', () => {
78
- assert.deepEqual(ROUTE_FILES, ['routes/copy-desk-artist.js', 'routes/copy-desk-release.js', 'routes/copy-desk.js'],
77
+ await test('the route files are exactly the four the module declares', () => {
78
+ assert.deepEqual(ROUTE_FILES, ['routes/copy-desk-artist.js', 'routes/copy-desk-release.js', 'routes/copy-desk-studio.js', 'routes/copy-desk.js'],
79
79
  'a new route file is a new place a write could hide: add it here, and to the write-surface scan below');
80
80
  });
81
81
 
@@ -902,6 +902,34 @@ async function listAccessHolders() {
902
902
  return rows;
903
903
  }
904
904
 
905
+ // THE GOAL OF A DOCKET MATTER (task 1005056): which goal each goal-shaped matter
906
+ // belongs to, so Home's project city can put the docket's "yours" over the right
907
+ // tower. A blocker belongs to every goal whose tasks it holds (task_blockers →
908
+ // tasks.goal_id); a criterion and a goal join request carry their goal. The
909
+ // tables are core's; this is a read across the wall in SQL, never a require.
910
+ // → [{ matter_id: 'blockers:12', goal_id: '1000095' }, …]; ids are numeric or skipped.
911
+ async function goalsOfMatters({ blockers = [], criteria = [], joinRequests = [] } = {}) {
912
+ const ids = (list) => (Array.isArray(list) ? list : []).map(String).filter((v) => /^\d{1,18}$/.test(v));
913
+ const [b, c, j] = [ids(blockers), ids(criteria), ids(joinRequests)];
914
+ if (!b.length && !c.length && !j.length) return [];
915
+ const { rows } = await pool.query(
916
+ `SELECT 'blockers:' || tb.blocker_id AS matter_id, t.goal_id::text AS goal_id
917
+ FROM task_blockers tb
918
+ JOIN tasks t ON t.id = tb.task_id
919
+ WHERE tb.blocker_id = ANY($1::bigint[]) AND t.goal_id IS NOT NULL
920
+ UNION
921
+ SELECT 'criteria_uat:' || c.id, c.goal_id::text
922
+ FROM done_when_criteria c
923
+ WHERE c.id = ANY($2::bigint[]) AND c.goal_id IS NOT NULL
924
+ UNION
925
+ SELECT 'goal_join_requests:' || r.id, r.goal_id::text
926
+ FROM goal_membership_requests r
927
+ WHERE r.id = ANY($3::bigint[])`,
928
+ [b, c, j],
929
+ );
930
+ return rows;
931
+ }
932
+
905
933
  // Record that a builder holds an access. Idempotent: an existing row is kept as it
906
934
  // was (its original granted_at/by). → true when a row was added.
907
935
  async function grantAccess(accessKey, builderId, grantedBy) {
@@ -1342,6 +1370,7 @@ module.exports = {
1342
1370
  listLeads,
1343
1371
  listActiveArchons,
1344
1372
  listAccessHolders,
1373
+ goalsOfMatters,
1345
1374
  grantAccess,
1346
1375
  revokeAccess,
1347
1376
  getActiveBuilder,
@@ -932,14 +932,69 @@ async function blockersByPerson(builder, { contributions = registeredContributio
932
932
  return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
933
933
  }
934
934
  await attachAsks(matters, asksDb.asksFor, timeoutMs);
935
+ const seats = await seatsFor(matters);
936
+ if (!seats) return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
937
+ const grouped = waiting.groupByPerson(matters, seats);
938
+ return { available: true, ...grouped, yours: waiting.forViewer(grouped, builder.id) };
939
+ }
940
+
941
+ // The seats a set of matters routes against (loadSeats), reading a board's holders
942
+ // only when some matter names a board seat. null when the seats are unreadable.
943
+ async function seatsFor(matters) {
935
944
  const boardSeat = matters.some((m) => /^board:/.test(routing.seatOf(m)));
936
945
  const government = boardSeat
937
946
  ? await loadGovernment({ decisions: await board.decisionRulesView().catch(() => null) })
938
947
  : { holders: {} };
939
- const seats = await loadSeats(government);
940
- if (!seats) return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
941
- const grouped = waiting.groupByPerson(matters, seats);
942
- return { available: true, ...grouped, yours: waiting.forViewer(grouped, builder.id) };
948
+ return loadSeats(government);
949
+ }
950
+
951
+ // WHAT IS YOURS, PER GOAL (task 1005056). Home's project city puts a salmon dot
952
+ // over a working area for each matter waiting on the viewer; this answers it the
953
+ // docket's way, so the city and the docket can never disagree. The goal-shaped
954
+ // sources are read exactly as buildDocket reads them (each under its own gate),
955
+ // carry their stored asks, and are routed against the seats with routing.js — a
956
+ // matter counts when its route is `yours`. Like docketFor, a viewer without the
957
+ // docket's own atom is routed nothing. Each matter is then placed on its goal(s)
958
+ // (db.goalsOfMatters). A source that fails is left out, as the docket leaves it
959
+ // unavailable; unreadable seats say `available: false` rather than "none".
960
+ // → { available: true, goals: { [goal_id]: count } } | { available: false, reason }
961
+ const GOAL_SOURCES = Object.freeze(['blockers', 'criteria_uat', 'goal_join_requests']);
962
+ async function yoursByGoal(builder, {
963
+ contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS,
964
+ permissions = null, readAsks = asksDb.asksFor, readSeats = seatsFor, goalsOf = db.goalsOfMatters,
965
+ } = {}) {
966
+ if (!builder || builder.id == null) return { available: true, goals: {} };
967
+ const held = new Set(permissions || await resolver.resolveBuilderPermissions(builder.id));
968
+ if (!held.has(GOVERNMENT_GATE.permission)) return { available: true, goals: {} };
969
+ const ctx = Object.freeze({ builderId: String(builder.id), rank: builder.rank || null, has: (p) => held.has(p) });
970
+ const matters = [];
971
+ await Promise.all(GOAL_SOURCES.map(async (source) => {
972
+ const c = contributions.sources.get(source);
973
+ if (!c || !gateAllows(c.gate, ctx)) return;
974
+ const decl = EXPECTED_SOURCES.find((d) => d.source === source);
975
+ try {
976
+ const raw = await withTimeout(Promise.resolve(c.read(ctx, { now })), timeoutMs);
977
+ matters.push(...(Array.isArray(raw) ? raw : []).map((m) => normalizeMatter(m, { source, building: c.building || decl.building })).filter(Boolean));
978
+ } catch (err) {
979
+ log.warn({ source, err: err && err.message }, 'yours by goal: a source could not be read');
980
+ }
981
+ }));
982
+ const needs = matters.filter((m) => m.state === 'needs');
983
+ if (!needs.length) return { available: true, goals: {} };
984
+ await attachAsks(needs, readAsks, timeoutMs);
985
+ const seats = await readSeats(needs);
986
+ if (!seats) return { available: false, reason: 'unavailable' };
987
+ routing.routeAll(needs, ctx.builderId, seats, now);
988
+ const mine = needs.filter((m) => m.route && m.route.yours);
989
+ if (!mine.length) return { available: true, goals: {} };
990
+ const tail = (m) => m.id.slice(m.source.length + 1);
991
+ const of = (source) => mine.filter((m) => m.source === source).map(tail);
992
+ const rows = await goalsOf({ blockers: of('blockers'), criteria: of('criteria_uat'), joinRequests: of('goal_join_requests') });
993
+ const goals = {};
994
+ for (const r of rows || []) {
995
+ if (r && r.goal_id != null) goals[String(r.goal_id)] = (goals[String(r.goal_id)] || 0) + 1;
996
+ }
997
+ return { available: true, goals };
943
998
  }
944
999
 
945
1000
  // One matter with its stored ask and the seats, for passing it on to `toSeat`
@@ -1024,5 +1079,7 @@ module.exports = {
1024
1079
  findMatterFor,
1025
1080
  loadSeats,
1026
1081
  blockersByPerson,
1082
+ yoursByGoal,
1083
+ GOAL_SOURCES,
1027
1084
  matterWithSeats,
1028
1085
  };
@@ -171,6 +171,8 @@ const governmentPort = {
171
171
  askRequiredMessage: asks.ASK_REQUIRED_MESSAGE,
172
172
  saveAsk: (...a) => asksDb.saveAsk(...a),
173
173
  asksFor: (...a) => asksDb.asksFor(...a),
174
+ // task 1005056: per goal, the docket matters routed to this viewer (Home's city)
175
+ yoursByGoal: (viewer) => docket.yoursByGoal(viewer),
174
176
  };
175
177
 
176
178
  // An ask arrives bare, or wrapped as { ask } (the shape POST /blockers takes it in).
@@ -89,7 +89,7 @@
89
89
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
90
90
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
91
91
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
92
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
92
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
93
93
  <script src="/builders/palette.js" defer></script>
94
94
  <script src="/builders/tweak-editor-lib.js?v=2026-09-28-tw23"></script>
95
95
  <script src="/builders/studio-lib.js?v=2026-10-09-day"></script>
@@ -92,7 +92,7 @@
92
92
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
93
93
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
94
94
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
95
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
95
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
96
96
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
97
97
  <script src="/builders/palette.js" defer></script>
98
98
  <script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
@@ -61,7 +61,7 @@
61
61
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
62
62
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
63
63
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
64
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
64
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
65
65
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
66
66
  <script src="/builders/palette.js" defer></script>
67
67
  <!-- the branding pack's currency label, loaded BEFORE blockers.js. -->
@@ -63,7 +63,7 @@
63
63
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
64
64
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
65
65
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
66
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
66
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
67
67
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
68
68
  <script src="/builders/palette.js" defer></script>
69
69
  <script src="/builders/hall-kit.js"></script>
@@ -185,9 +185,6 @@
185
185
  // (same pattern as lastNeedsState). These are un-landed confirmed tasks that
186
186
  // GATE new claims and whose reward is pending until they land on ship.
187
187
  let lastRebaseState = null;
188
- // The public version progress, which the Home version tab draws (task 1004774).
189
- // Null until it lands.
190
- let lastProgressState = null;
191
188
 
192
189
  // Cached /me payload from the most recent loadAll() call. Used by the
193
190
  // welcome-chip reopen path (#408) so we can re-render the welcome panel
@@ -874,62 +871,6 @@
874
871
  if (btn) window.location.hash = `#/idea/${encodeURIComponent(btn.dataset.triageIdea)}`;
875
872
  });
876
873
 
877
- // ---- Home "Next up" slice (task 2184) -------------------------------------
878
-
879
- // Fill the top of the "Your work" card with the first five rows of the
880
- // claimable queue (server order: priority, then calibrated estimate) so Home
881
- // answers "what's my next action". Best-effort: a primer-gated 403, an auth
882
- // hiccup, or an empty queue hides the slice — Home never shows an error for
883
- // it. Re-runs with every loadAll (SSE nudges keep it current).
884
- async function loadNextUp() {
885
- const wrap = document.getElementById('home-next');
886
- const list = document.getElementById('home-next-list');
887
- const subhead = document.getElementById('home-recent-subh');
888
- if (!wrap || !list) return;
889
- try {
890
- // ?limit=2 — this card is TWO lines. It used to download the entire claimable
891
- // feed, every row with its full description, and `.slice(0, 2)` it: the same
892
- // shape goal 1000072's criterion names, and the one goal-rollup.js was carved to
893
- // fix on the Goals page (5,229,455 bytes to produce forty counters). The server
894
- // already orders by priority then calibrated estimate, so the first two rows ARE
895
- // the top two. `claimable_total` comes back so the foot can say how many are
896
- // waiting instead of leaving two rows to imply there are only two.
897
- // task 1004774: five rows, level with the five Recently shipped rows beside it.
898
- const resp = await getJSON(`${API}/tasks/claimable?limit=5`, { softAuth: true });
899
- // capped as well as asked for: a server that ignores ?limit must not grow the card.
900
- const rows = resp && Array.isArray(resp.claimable) ? resp.claimable.slice(0, 5) : [];
901
- if (!rows.length) { wrap.hidden = true; if (subhead) subhead.hidden = true; return; }
902
- const total = Number(resp.claimable_total);
903
- const cur = (window.__BRANDING__ && window.__BRANDING__.currency && window.__BRANDING__.currency.label) || 'credits';
904
- list.innerHTML = rows.map((t) => {
905
- const credits = window.OTBBranding?.taskCreditValue?.(t) ?? t.credits_reward;
906
- const mins = t.est_minutes ? ` · ~${escapeHtml(String(t.est_minutes))}m` : '';
907
- // task 1003459 — this row now claims directly (the extracted claim
908
- // action, task 1003458) rather than only linking to the record and
909
- // making the builder find the door there.
910
- return `<li class="next-up__item">
911
- <a class="next-up__title" href="/task/${encodeURIComponent(t.id)}">${escapeHtml(t.title || `task ${t.id}`)}</a>
912
- <span class="next-up__meta">#${escapeHtml(String(t.id))}${mins} · ${fmtNum(credits)} ${escapeHtml(cur)} est.</span>
913
- <button class="task__claim next-up__claim" type="button" data-task-id="${escapeHtml(String(t.id))}" aria-label="Claim task ${escapeHtml(String(t.id))}">Claim</button>
914
- </li>`;
915
- }).join('');
916
- // Name the size of the queue these two came off. Silent when the count is
917
- // missing or not bigger than what is shown — "See all 2" under two rows is
918
- // noise, and a made-up number is worse than none.
919
- const foot = document.getElementById('home-next-foot');
920
- if (foot) {
921
- foot.textContent = Number.isFinite(total) && total > rows.length
922
- ? `See all ${fmtNum(total)} ready work →`
923
- : 'See all ready work →';
924
- }
925
- wrap.hidden = false;
926
- if (subhead) subhead.hidden = false;
927
- } catch (_) {
928
- wrap.hidden = true;
929
- if (subhead) subhead.hidden = true;
930
- }
931
- }
932
-
933
874
  // ---- the Roll’s rows, for the Standing card -----------------------------
934
875
  //
935
876
  // task 1003441 moved the leaderboard TABLE to its own page (/roster,
@@ -1084,11 +1025,6 @@
1084
1025
  // /me because it spans all builders' work), loaded fire-and-forget so a
1085
1026
  // slow/failed feed can't hold up the rest of the hall.
1086
1027
  loadAcclaimFeed();
1087
- // task 2184: the Home "Next up" slice — top of the claimable queue, so the
1088
- // page answers "what's my next action" without a trip to the Work board.
1089
- // Fire-and-forget; hides itself when the feed is gated (primer 403) or empty.
1090
- loadNextUp();
1091
-
1092
1028
  // #408: build the sticky nav now that every section has settled its
1093
1029
  // hidden state (welcome → karma → onboarding → security/roster).
1094
1030
  // The renderNav call also wires the IntersectionObserver for active-
@@ -1108,21 +1044,15 @@
1108
1044
  // down the page — log and show an inline placeholder.
1109
1045
  // #726: the per-skill model picker (Voices) moved to /builders/settings,
1110
1046
  // so the hall no longer fetches /me/skill-prefs here.
1111
- const [achAll, ships, versionsResp, lb, progress] = await Promise.allSettled([
1047
+ const [achAll, ships, versionsResp, lb] = await Promise.allSettled([
1112
1048
  getJSON(`${API}/achievements`),
1113
1049
  // task 1004854: Home's newest few + a count — never the whole history,
1114
1050
  // which only the Accomplishments ledger fetches (hall-render.js loadShipLedger).
1115
1051
  getJSON(`${API}/me/recent-ships?limit=${HOME_SHIPS_LIMIT}`),
1116
1052
  getJSON(`${API}/versions`),
1117
1053
  getJSON(`${API}/public/leaderboard`), // task 1004437: the list with each builder's roles
1118
- getJSON(`${API}/public/progress`), // the Home version tab (task 1004774)
1119
1054
  ]);
1120
1055
 
1121
- if (progress.status === 'fulfilled') {
1122
- lastProgressState = progress.value;
1123
- renderGreeting();
1124
- }
1125
-
1126
1056
  if (achAll.status === 'fulfilled') {
1127
1057
  // Show top 10 by sort_order — already ordered server-side. (Set is locked at 8
1128
1058
  // for V2, so the slice is defensive.) Unlocked + locked-with-progress come
@@ -1227,11 +1157,6 @@
1227
1157
  // task 1004833: the shared vote control reports each landed vote to the feed.
1228
1158
  $('#acclaim-feed')?.addEventListener('otb-vote', onAcclaimVote);
1229
1159
 
1230
- // task 1003459 — the Claim button on each Next-up row; the action + the
1231
- // delegated listener live in hall-render.js (builders.js is at the
1232
- // 1,500-line ratchet, hall-ui/CLAUDE.md's "growth goes in the lib/renderer").
1233
- wireNextUpClaim();
1234
-
1235
1160
  $('#f-ships-window')?.addEventListener('change', onShipsFilterChange);
1236
1161
  $('#f-ships-version')?.addEventListener('change', onShipsFilterChange);
1237
1162
  $('#f-ships-sort')?.addEventListener('change', renderRecentShips);
@@ -1346,14 +1271,13 @@
1346
1271
  // reads what this page holds NOW rather than what it held at boot. Everything
1347
1272
  // else is stable for the life of the page. This call sits AFTER every
1348
1273
  // declaration it passes — moving it earlier is a temporal-dead-zone crash.
1349
- const { acclaimEvidenceHtml, analyticsState, lbState, mountHallWidgets, renderCurveBody, renderGreeting, renderLanding, renderSessionRefused, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, HOME_SHIPS_LIMIT, populateShipVersionFilter, setRecentShips, shipsPager, welcomeDismissalKey, wireNextUpClaim } = window.OTBHallRender({
1350
- API, RANK_ORDER, api, brandWorld, curLabel, emptyRow, fmtDate, getJSON, loadNextUp, mins, normalizeRank, pct, postJSON, rankLabel, rankWeight, signClasses, toast, usd,
1274
+ const { acclaimEvidenceHtml, analyticsState, lbState, mountHallWidgets, renderCurveBody, renderGreeting, renderLanding, renderSessionRefused, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, HOME_SHIPS_LIMIT, populateShipVersionFilter, setRecentShips, shipsPager, welcomeDismissalKey } = window.OTBHallRender({
1275
+ API, RANK_ORDER, api, brandWorld, curLabel, emptyRow, fmtDate, getJSON, mins, normalizeRank, pct, postJSON, rankLabel, rankWeight, signClasses, toast, usd,
1351
1276
  get instanceRepo() { return instanceRepo; },
1352
1277
  get lastCostState() { return lastCostState; },
1353
1278
  get lastKarmaState() { return lastKarmaState; },
1354
1279
  get lastMeState() { return lastMeState; },
1355
1280
  get lastNeedsState() { return lastNeedsState; },
1356
- get lastProgressState() { return lastProgressState; },
1357
1281
  get lastRebaseState() { return lastRebaseState; },
1358
1282
  get newcomerGated() { return newcomerGated; },
1359
1283
  });
@@ -108,7 +108,7 @@
108
108
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
109
109
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab" defer></script>
110
110
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch" defer></script>
111
- <script src="/builders/shell.js?v=2026-11-03-update-pill" defer></script>
111
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch" defer></script>
112
112
  <script src="/builders/palette.js" defer></script>
113
113
  <script src="/builders/gov-living.js?v=2026-10-09-positions1" defer></script>
114
114
  <script src="/builders/gov-feeds.js?v=2026-10-09-living3" defer></script>
@@ -162,7 +162,7 @@
162
162
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
163
163
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
164
164
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
165
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
165
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
166
166
  <script src="/builders/palette.js" defer></script>
167
167
  <script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
168
168
  <script src="/builders/collab.js?v=2026-09-24-archive"></script>
@@ -54,7 +54,7 @@
54
54
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
55
55
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
56
56
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
57
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
57
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
58
58
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
59
59
  <script src="/builders/palette.js" defer></script>
60
60
  <script src="/builders/hall-kit.js"></script>
@@ -180,7 +180,7 @@
180
180
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
181
181
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
182
182
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
183
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
183
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
184
184
  <script src="/builders/palette.js" defer></script>
185
185
  <script src="/builders/hall-kit.js"></script>
186
186
  <script src="/builders/copy-desk.js?v=2026-09-01-redesign"></script>
@@ -107,7 +107,7 @@
107
107
  artistry: ['studio', 'goals', 'thinking'],
108
108
  ideating: ['goal-map', 'thinking', 'goals', 'market-watch'],
109
109
  governing: ['city', 'blockers', 'board-room',
110
- 'watch', 'gate', 'sessions', 'project-settings', 'deploy', 'fleet', 'market-watch-settings', 'government'],
110
+ 'watch', 'gate', 'sessions', 'project-settings', 'deploy', 'fleet', 'government'],
111
111
  };
112
112
  // A mode can lead with ONE tab and fold the rest of its own under a dropdown
113
113
  // (owner, 2026-10-08: the city is Governing's tab, everything else under it).
@@ -81,7 +81,7 @@
81
81
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
82
82
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
83
83
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
84
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
84
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
85
85
  <script src="/builders/palette.js" defer></script>
86
86
  <script src="/builders/hall-kit.js"></script>
87
87
  <!-- the shared ask card the move is drawn with (task 1004829) -->
@@ -97,7 +97,7 @@
97
97
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
98
98
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
99
99
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
100
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
100
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
101
101
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
102
102
  <script src="/builders/palette.js" defer></script>
103
103
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
@@ -97,7 +97,7 @@
97
97
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
98
98
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
99
99
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
100
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
100
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
101
101
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
102
102
  <script src="/builders/palette.js" defer></script>
103
103
  <script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
@@ -60,7 +60,7 @@
60
60
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
61
61
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
62
62
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
63
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
63
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
64
64
  <script src="/builders/palette.js" defer></script>
65
65
  <script src="/builders/hall-kit.js"></script>
66
66
  <script src="/builders/fleet-costs.js"></script>
@@ -67,7 +67,7 @@
67
67
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
68
68
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
69
69
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
70
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
70
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
71
71
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
72
72
  <script src="/builders/palette.js" defer></script>
73
73
  <script src="/builders/hall-kit.js"></script>
@@ -140,7 +140,7 @@
140
140
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
141
141
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
142
142
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
143
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
143
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
144
144
  <script src="/builders/palette.js" defer></script>
145
145
 
146
146
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -244,7 +244,7 @@
244
244
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
245
245
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
246
246
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
247
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
247
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
248
248
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
249
249
  <script src="/builders/palette.js" defer></script>
250
250
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -157,7 +157,7 @@
157
157
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
158
158
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
159
159
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
160
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
160
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
161
161
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
162
162
  <script src="/builders/palette.js" defer></script>
163
163
  <script src="/builders/hall-kit.js"></script>