@bongos/core 1.18.3 → 1.18.18

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 (469) hide show
  1. package/.bongos-core.json +1575 -285
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +4 -2
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +6 -1
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/status-mirror-sync.js +114 -84
  347. package/src/bongos/api-errors.js +1 -0
  348. package/src/bongos/auth.js +447 -102
  349. package/src/bongos/db-kernel.js +27 -0
  350. package/src/bongos/db.js +39 -0
  351. package/src/bongos/module-scope-map.js +27 -1
  352. package/src/bongos/permission-path-check.js +136 -102
  353. package/src/bongos/repo-info.js +47 -11
  354. package/src/bongos/route-rank-check.js +429 -46
  355. package/src/bongos/routes/_helpers.js +12 -1
  356. package/src/bongos/routes/auth.js +219 -2
  357. package/src/bongos/routes/backup.js +58 -2
  358. package/src/bongos/routes/me.js +35 -1
  359. package/src/bongos/serve-internal.js +84 -15
  360. package/src/branding.js +32 -0
  361. package/src/instance-config.js +30 -0
  362. package/src/module-api.js +112 -1
  363. package/src/platform-server.js +86 -9
  364. package/tests/abandon_task.mjs +59 -3
  365. package/tests/api_cli_flags.mjs +53 -0
  366. package/tests/auth_page_gate.mjs +13 -0
  367. package/tests/backfill_sessions.mjs +127 -1
  368. package/tests/backup_cli.mjs +54 -0
  369. package/tests/bongos_box_connect.mjs +77 -0
  370. package/tests/box_access.mjs +130 -1
  371. package/tests/box_credential.mjs +125 -0
  372. package/tests/box_host_keys.mjs +24 -0
  373. package/tests/box_infra_resolve.mjs +149 -0
  374. package/tests/box_scope_session.mjs +6 -2
  375. package/tests/box_sync.mjs +55 -0
  376. package/tests/box_task_scope.mjs +99 -0
  377. package/tests/branding.mjs +12 -0
  378. package/tests/cf_ruleset_preflight.mjs +142 -0
  379. package/tests/cf_tunnel.mjs +37 -0
  380. package/tests/claim_gate_rebase.mjs +98 -0
  381. package/tests/clawback_confirmed_backlog.mjs +103 -0
  382. package/tests/cli_lib_formatters.mjs +20 -0
  383. package/tests/confirm_attribution.mjs +30 -46
  384. package/tests/credit_grant.mjs +198 -10
  385. package/tests/criterion_authoring_gate.mjs +272 -0
  386. package/tests/criterion_progress.mjs +33 -1
  387. package/tests/dev_lib.mjs +253 -0
  388. package/tests/devbox_artifact_gate.mjs +75 -0
  389. package/tests/devbox_dmg_backdrop.mjs +137 -0
  390. package/tests/devbox_icons.mjs +304 -0
  391. package/tests/devbox_installer_brand.mjs +232 -0
  392. package/tests/devbox_key_register.mjs +234 -0
  393. package/tests/docs_openapi_route.mjs +110 -0
  394. package/tests/doctor_hooks.mjs +141 -0
  395. package/tests/extract_json.mjs +140 -0
  396. package/tests/fitness.mjs +89 -0
  397. package/tests/gen_freshness.mjs +96 -0
  398. package/tests/goal_advisory.mjs +251 -0
  399. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  400. package/tests/goal_routes.mjs +59 -4
  401. package/tests/goal_tier.mjs +12 -3
  402. package/tests/governance_catalog.mjs +129 -0
  403. package/tests/governance_claim_eligibility.mjs +262 -0
  404. package/tests/governance_config.mjs +187 -0
  405. package/tests/governance_ownership_scope.mjs +410 -0
  406. package/tests/governance_page_gates.mjs +324 -0
  407. package/tests/governance_parity.mjs +336 -0
  408. package/tests/governance_principals.mjs +232 -0
  409. package/tests/governance_protected_surfaces.mjs +208 -0
  410. package/tests/governance_require_permission.mjs +223 -0
  411. package/tests/governance_resolver.mjs +84 -0
  412. package/tests/governance_role_sync.mjs +231 -0
  413. package/tests/governance_seed.mjs +127 -0
  414. package/tests/governance_session_scope.mjs +434 -0
  415. package/tests/governance_vanilla_boot.mjs +203 -0
  416. package/tests/grade_attribution.mjs +7 -7
  417. package/tests/hall_audit.mjs +223 -0
  418. package/tests/hall_nav.mjs +191 -0
  419. package/tests/hall_palette.mjs +206 -0
  420. package/tests/hall_signout.mjs +124 -0
  421. package/tests/hook_user_agents.mjs +10 -0
  422. package/tests/idea_triage.mjs +162 -0
  423. package/tests/idp_device_exchange.mjs +93 -0
  424. package/tests/init.mjs +51 -0
  425. package/tests/instance_config.mjs +65 -0
  426. package/tests/instance_mismatch_guard.mjs +23 -6
  427. package/tests/instance_release_config.mjs +104 -0
  428. package/tests/linkify_refs.mjs +15 -2
  429. package/tests/module_contributions.mjs +55 -0
  430. package/tests/module_loader.mjs +10 -5
  431. package/tests/module_scope_active_claims.mjs +94 -0
  432. package/tests/needs_rebase.mjs +92 -0
  433. package/tests/one_active_claim_per_session.mjs +76 -0
  434. package/tests/override_attribution.mjs +10 -8
  435. package/tests/override_request_ownership.mjs +8 -4
  436. package/tests/parse_cookie_decode.mjs +42 -0
  437. package/tests/pay_on_land.mjs +129 -0
  438. package/tests/platform_boot.mjs +100 -1
  439. package/tests/post_login_landing.mjs +42 -0
  440. package/tests/preflight_checks.mjs +72 -0
  441. package/tests/provision.mjs +132 -7
  442. package/tests/publish_branch_route.mjs +53 -0
  443. package/tests/publish_reconciler.mjs +187 -2
  444. package/tests/repo_info_binding.mjs +248 -0
  445. package/tests/reward_gate.mjs +11 -2
  446. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  447. package/tests/reward_mode_surfacing.mjs +168 -0
  448. package/tests/route_permission_check.mjs +301 -0
  449. package/tests/route_rank_check.mjs +254 -12
  450. package/tests/route_validation.mjs +30 -0
  451. package/tests/sequence.mjs +267 -0
  452. package/tests/session_digest.mjs +44 -0
  453. package/tests/session_index.mjs +18 -6
  454. package/tests/session_records.mjs +39 -1
  455. package/tests/session_reward_on_land.mjs +381 -0
  456. package/tests/session_reward_reconcile.mjs +229 -0
  457. package/tests/ship_api_client_pathspec.mjs +100 -0
  458. package/tests/ship_base_flag.mjs +93 -0
  459. package/tests/ship_ci_deploy.mjs +80 -3
  460. package/tests/ship_upstream_deletion.mjs +57 -0
  461. package/tests/smoke_target.mjs +233 -0
  462. package/tests/start_env_guard.mjs +87 -0
  463. package/tests/start_rebase_warning.mjs +103 -0
  464. package/tests/status_mirror_sync.mjs +122 -0
  465. package/tests/stop_refcheck.mjs +19 -1
  466. package/tests/task_goal_link.mjs +15 -6
  467. package/tests/upgrade.mjs +139 -0
  468. package/tests/web_admission_status.mjs +162 -0
  469. package/tests/write_validation_gaps.mjs +110 -0
@@ -109,31 +109,9 @@
109
109
  return `${prefix}--${v > 0 ? 'up' : v < 0 ? 'down' : 'flat'}`;
110
110
  }
111
111
 
112
- // renderPager — the works-wrought / roster twin pager. Clamps state.page into
113
- // range (mutating it), hides the pager when there is ≤1 page, writes the
114
- // "start–end of total" range label, and toggles prev/next. Returns { start } so
115
- // the caller can slice. NOTE: the leaderboard pager deliberately does NOT use
116
- // this — it never clamps and keeps its range/buttons always visible.
117
- function renderPager(state, total, pageSize, { pagerEl, rangeEl, prevBtn, nextBtn }) {
118
- const pageCount = Math.max(1, Math.ceil(total / pageSize));
119
- if (state.page >= pageCount) state.page = pageCount - 1;
120
- if (state.page < 0) state.page = 0;
121
- const start = state.page * pageSize;
122
- if (pagerEl) {
123
- if (pageCount > 1) {
124
- pagerEl.hidden = false;
125
- if (rangeEl) {
126
- const end = Math.min(start + pageSize, total);
127
- rangeEl.textContent = `${start + 1}–${end} of ${total}`;
128
- }
129
- if (prevBtn) prevBtn.disabled = state.page === 0;
130
- if (nextBtn) nextBtn.disabled = state.page >= pageCount - 1;
131
- } else {
132
- pagerEl.hidden = true;
133
- }
134
- }
135
- return { start };
136
- }
112
+ // (task 2174: the hand-rolled renderPager the ships + roster tables shared is
113
+ // gone — both now use the OTBKit pager, which clamps, persists per-page and
114
+ // paints the bar from one implementation.)
137
115
 
138
116
  // ---- repo info (the clone URL the welcome walkthrough hands a newcomer) ---
139
117
  //
@@ -248,6 +226,11 @@
248
226
  // Standing-card "Action needed" banner survives re-renders that don't carry
249
227
  // needs (same pattern as lastCostState).
250
228
  let lastNeedsState = null;
229
+ // ADR 0120 part 5: cache the most recent /me `needs_rebase_tasks` so the
230
+ // Standing-card "Rebase owed" banner survives re-renders that don't carry it
231
+ // (same pattern as lastNeedsState). These are un-landed confirmed tasks that
232
+ // GATE new claims and whose reward is pending until they land on ship.
233
+ let lastRebaseState = null;
251
234
 
252
235
  // Cached /me payload from the most recent loadAll() call. Used by the
253
236
  // welcome-chip reopen path (#408) so we can re-render the welcome panel
@@ -300,30 +283,59 @@
300
283
  // source of truth is now the hall-widget registry (window.OTB, dom-utils.js):
301
284
  // hall-widgets.js registers the core widgets above (loads before this script),
302
285
  // a module's client script registers its own (gated by loader.js + moduleOn).
303
- // SECTIONS() derives the nav rows from the registry, preserving order + the
304
- // per-widget gates (welcomeChip / archonOnly) carried on the registry entry.
305
- const SECTIONS = () => window.OTB.hallWidgets().map((wgt) => ({
306
- id: wgt.id,
307
- label: wgt.label,
308
- view: wgt.view || 'home',
309
- welcomeChip: !!wgt.welcomeChip,
310
- archonOnly: !!wgt.archonOnly,
311
- }));
286
+ // The rail (renderNav) reads window.OTB.hallWidgets() straight, applying the
287
+ // per-widget gates (archonOnly) carried on the registry entry.
312
288
 
313
289
  // task 1723: which container a widget mounts into. A widget may declare
314
- // `zone` (a fine-grained Home slot) or `view` (home/standing/achievements/
315
- // leaderboard); both default to the Home main zone so pre-redesign module
316
- // widgets still land somewhere visible. Every container lives INSIDE <main>,
317
- // so the `main .scroll` sweeps (newcomer gate, landing hide-all) keep working.
290
+ // `zone` (a fine-grained Home slot), `group` (a Standing rail section, task
291
+ // 2174) or `view` (home/standing/leaderboard); all default to the Home main
292
+ // zone so pre-redesign module widgets still land somewhere visible. Every
293
+ // container lives INSIDE <main>, so the `main .scroll` sweeps (newcomer gate,
294
+ // landing hide-all) keep working.
318
295
  const VIEW_CONTAINERS = {
319
296
  home: 'zone-home-main',
320
297
  standing: 'zone-standing',
321
- achievements: 'zone-achievements',
322
298
  leaderboard: 'zone-leaderboard',
323
299
  };
300
+
301
+ // task 2174: Standing is an identity band + four rail sections grouped by the
302
+ // question they answer. This array is the rail's order and its labels; each
303
+ // entry gets a #standing-<key> anchor container inside #zone-standing that the
304
+ // group's widgets mount into. The Profile card declares no group, so it stays
305
+ // above them as the band.
306
+ const STANDING_GROUPS = [
307
+ { key: 'work', label: 'Work' },
308
+ { key: 'karma', label: 'Karma' },
309
+ { key: 'progress', label: 'Progress' },
310
+ { key: 'achievements', label: 'Achievements' },
311
+ ];
312
+ const standingGroupId = (key) => `standing-${key}`;
313
+
314
+ // Create ALL four group containers the first time any group is asked for, so
315
+ // the rail's order is STANDING_GROUPS' — not whichever group's first widget
316
+ // happens to register earliest. Creating them on demand (rather than up front)
317
+ // is what keeps the Profile band on top: an ungrouped Standing widget appends
318
+ // straight to #zone-standing, so anything registered before the first grouped
319
+ // widget lands above the groups. Idempotent.
320
+ function ensureStandingGroups() {
321
+ const zone = document.getElementById('zone-standing');
322
+ if (!zone || document.getElementById(standingGroupId(STANDING_GROUPS[0].key))) return;
323
+ for (const g of STANDING_GROUPS) {
324
+ const el = document.createElement('div');
325
+ el.id = standingGroupId(g.key);
326
+ el.className = 'standing-group';
327
+ zone.appendChild(el);
328
+ }
329
+ }
330
+
324
331
  function widgetHost(wgt) {
325
332
  const zone = wgt.zone ? document.getElementById(`zone-${wgt.zone}`) : null;
326
333
  if (zone) return zone;
334
+ if (wgt.group) {
335
+ ensureStandingGroups();
336
+ const group = document.getElementById(standingGroupId(wgt.group));
337
+ if (group) return group;
338
+ }
327
339
  const viewId = VIEW_CONTAINERS[wgt.view || 'home'] || VIEW_CONTAINERS.home;
328
340
  return document.getElementById(viewId) || document.querySelector('main');
329
341
  }
@@ -409,10 +421,11 @@
409
421
  window.OTBBranding?.applyCurrencyLabels?.(main);
410
422
  }
411
423
 
412
- // The IntersectionObserver instance survives across re-renders (it's
413
- // disconnected and re-created when nav contents change). Stored on
414
- // module scope so nav re-renders can dispose of the previous one.
415
- let navObserver = null;
424
+ // task 2174: the Standing rail is an OTBKit sectionRail in scrollspy mode —
425
+ // the kit owns the IntersectionObserver that used to live here as
426
+ // setupNavObserver(). Built lazily on the first renderNav and refreshed after,
427
+ // so the same instance survives re-renders.
428
+ let standingRail = null;
416
429
 
417
430
  // task 1723: the old sticky chip strip is gone. renderNav now settles the
418
431
  // three nav surfaces the shell owns or hosts:
@@ -420,8 +433,10 @@
420
433
  // the server enforces every page + endpoint regardless, ADR 0016);
421
434
  // 2. the Home re-open affordance for a dismissed welcome banner (the old
422
435
  // welcome chip's only job);
423
- // 3. the Standing view's in-page section rail (#standing-rail), rebuilt from
424
- // the registry's standing widgets that are currently shown.
436
+ // 3. the Standing view's in-page section rail (#standing-rail) — task 2174:
437
+ // one entry per GROUP that currently has something to show, not one per
438
+ // widget. A group with every member hidden (a newcomer, an empty karma
439
+ // feed) hides itself and drops off the rail.
425
440
  // Keeps its (builder, onboarding) signature — all call sites pass them.
426
441
  function renderNav(builder, onboarding) {
427
442
  const isArchon = (builder?.rank || '').toLowerCase() === 'archon';
@@ -446,25 +461,30 @@
446
461
  reopen.hidden = !(welcomeEl && welcomeEl.hidden && !graduated);
447
462
  }
448
463
 
449
- // Standing section rail: one link per visible standing panel.
464
+ // Standing section rail: one link per group with a visible member.
450
465
  const rail = document.getElementById('standing-rail');
451
466
  if (rail) {
452
- const items = SECTIONS().filter((s) => {
453
- if (s.view !== 'standing') return false;
454
- if (s.archonOnly && !isArchon) return false;
455
- const el = document.getElementById(s.id);
456
- return !!el && !el.hidden;
457
- });
458
- rail.innerHTML = items.map((s) =>
459
- `<a class="rail-link" href="#${escapeHtml(s.id)}" data-section="${escapeHtml(s.id)}">${escapeHtml(s.label)}</a>`
460
- ).join('');
461
- rail.hidden = items.length < 2;
467
+ const shown = new Set(
468
+ window.OTB.hallWidgets()
469
+ .filter((wgt) => {
470
+ if (!wgt.group) return false;
471
+ if (wgt.archonOnly && !isArchon) return false;
472
+ const el = document.getElementById(wgt.id);
473
+ return !!el && !el.hidden;
474
+ })
475
+ .map((wgt) => wgt.group),
476
+ );
477
+ // An empty group would otherwise leave a gap in the panel column.
478
+ for (const g of STANDING_GROUPS) {
479
+ const el = document.getElementById(standingGroupId(g.key));
480
+ if (el) el.hidden = !shown.has(g.key);
481
+ }
482
+ const items = STANDING_GROUPS
483
+ .filter((g) => shown.has(g.key))
484
+ .map((g) => ({ id: standingGroupId(g.key), label: g.label }));
485
+ if (standingRail) standingRail.refresh(items);
486
+ else standingRail = window.OTBKit.makeSectionRail({ mount: rail, items, mode: 'scrollspy' });
462
487
  }
463
-
464
- // Rebuild the observer to track whichever sections are currently in
465
- // the rail. Doing this on each renderNav call covers both initial load
466
- // and post-dismiss/re-open re-renders.
467
- setupNavObserver();
468
488
  }
469
489
 
470
490
  // #748: newcomer gate. A Xenos sees a deliberately spare hall — their
@@ -502,56 +522,6 @@
502
522
  // gates. This function keeps only the widget-section sweep.
503
523
  }
504
524
 
505
- function setupNavObserver() {
506
- if (navObserver) {
507
- navObserver.disconnect();
508
- navObserver = null;
509
- }
510
- const list = document.getElementById('standing-rail');
511
- if (!list) return;
512
-
513
- const chipsBySection = new Map();
514
- list.querySelectorAll('a[data-section]').forEach((a) => {
515
- chipsBySection.set(a.dataset.section, a);
516
- });
517
- if (chipsBySection.size === 0) return;
518
-
519
- const sections = Array.from(chipsBySection.keys())
520
- .map((id) => document.getElementById(id))
521
- .filter((el) => el && !el.hidden);
522
-
523
- if (sections.length === 0) return;
524
-
525
- // rootMargin trims the active band to roughly the upper third of the
526
- // viewport (top 40% ignored, bottom 55% ignored — a ~5% horizontal
527
- // strip about a third of the way down the page). The active flip
528
- // happens when a section headline crosses that band, not when the
529
- // section first peeks in. Matches the rationale in the task spec.
530
- navObserver = new IntersectionObserver((entries) => {
531
- for (const entry of entries) {
532
- if (entry.isIntersecting) {
533
- const id = entry.target.id;
534
- chipsBySection.forEach((chip, sectionId) => {
535
- chip.classList.toggle('rail-link--active', sectionId === id);
536
- });
537
- // Auto-scroll the active chip into view on mobile so the user
538
- // can see what section they're in without having to scroll the
539
- // nav strip themselves.
540
- const active = chipsBySection.get(id);
541
- if (active && window.matchMedia('(max-width: 640px)').matches) {
542
- active.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
543
- }
544
- break;
545
- }
546
- }
547
- }, {
548
- rootMargin: '-40% 0px -55% 0px',
549
- threshold: 0,
550
- });
551
-
552
- sections.forEach((sec) => navObserver.observe(sec));
553
- }
554
-
555
525
  function reopenWelcomeFromNav(e) {
556
526
  e.preventDefault();
557
527
  const id = window.__buildersSelfId;
@@ -712,6 +682,7 @@
712
682
  const karma = lastKarmaState;
713
683
  const cost = lastCostState;
714
684
  const needs = lastNeedsState;
685
+ const rebase = lastRebaseState;
715
686
  const avatar = builder.avatar_url
716
687
  ? `<img class="profile__avatar" src="${escapeHtml(builder.avatar_url)}" alt="">`
717
688
  : `<div class="profile__avatar" aria-hidden="true"></div>`;
@@ -770,7 +741,20 @@
770
741
  </div>`
771
742
  : '';
772
743
 
744
+ // ADR 0120 part 5: the "Rebase owed" banner. `needs_rebase_tasks` are un-landed
745
+ // confirmed tasks the publish-reconciler flagged — they GATE new claims (part 3)
746
+ // and their per-task reward is PENDING until they land on ship (part 1). Rendered
747
+ // above the other banners since it blocks the queue; reads the SAME /me field the
748
+ // CLI (/builder-start) does. `task N` form, never a bare #N (the ref rule).
749
+ const rebaseTasks = Array.isArray(rebase) ? rebase.filter((t) => t && t.id != null) : [];
750
+ const rebaseBanner = rebaseTasks.length
751
+ ? `<div class="profile__rebase" role="status">
752
+ <strong>Rebase owed — new claims paused</strong> — ${rebaseTasks.length} confirmed ${rebaseTasks.length === 1 ? 'task' : 'tasks'} can't land yet, so ${rebaseTasks.length === 1 ? 'its' : 'their'} reward is pending (it lands on ship). Rebase + re-ship or release ${rebaseTasks.length === 1 ? 'it' : 'them'}: ${rebaseTasks.map((t) => 'task ' + escapeHtml(String(t.id))).join(', ')}.
753
+ </div>`
754
+ : '';
755
+
773
756
  $('#profile-body').innerHTML = `
757
+ ${rebaseBanner}
774
758
  ${needsBanner}
775
759
  ${budgetBanner}
776
760
  ${avatar}
@@ -824,6 +808,7 @@
824
808
  const homeStanding = document.getElementById('home-standing-body');
825
809
  if (homeStanding) {
826
810
  homeStanding.innerHTML = `
811
+ ${rebaseBanner}
827
812
  ${needsBanner}
828
813
  ${budgetBanner}
829
814
  <div class="profile profile--compact">
@@ -1329,11 +1314,10 @@
1329
1314
  // filtered, sorted, and paged entirely client-side. Mirrors the status site's
1330
1315
  // Lately Inscribed pattern (modules/status-ui/public/status.js renderShippedPage), with
1331
1316
  // an additional version filter since a builder's history spans versions.
1332
- const SHIPS_PAGE_SIZE = 10;
1333
- const shipState = {
1334
- all: [],
1335
- page: 0,
1336
- };
1317
+ const shipState = { all: [] };
1318
+ // task 2174: paging moved to the OTBKit pager (per-page select + persistence);
1319
+ // 10 stays the default page size this table has always used.
1320
+ const shipsPager = window.OTBKit.makePager('ships', 'otb.standing.shipsPerPage', { defaultPerPage: 10 });
1337
1321
 
1338
1322
  function filterShips(ships) {
1339
1323
  const win = $('#f-ships-window')?.value || 'all';
@@ -1380,9 +1364,6 @@
1380
1364
  const tbody = $('#recent-ships tbody');
1381
1365
  const countEl = $('#ships-count');
1382
1366
  const pagerEl = $('#ships-pager');
1383
- const rangeEl = $('#ships-range');
1384
- const prevBtn = $('#ships-prev');
1385
- const nextBtn = $('#ships-next');
1386
1367
 
1387
1368
  const filtered = filterShips(shipState.all);
1388
1369
  const mode = $('#f-ships-sort')?.value || 'recent';
@@ -1426,8 +1407,7 @@
1426
1407
  return;
1427
1408
  }
1428
1409
 
1429
- const { start } = renderPager(shipState, sorted.length, SHIPS_PAGE_SIZE, { pagerEl, rangeEl, prevBtn, nextBtn });
1430
- const slice = sorted.slice(start, start + SHIPS_PAGE_SIZE);
1410
+ const slice = window.OTBKit.paginate(sorted, shipsPager);
1431
1411
 
1432
1412
  tbody.innerHTML = slice.map((s) => `
1433
1413
  <tr>
@@ -1442,11 +1422,11 @@
1442
1422
  `).join('');
1443
1423
  }
1444
1424
 
1445
- // Re-render when filters change. Reset to page 0 so the new filter starts at
1425
+ // Re-render when filters change. Reset to page 1 so the new filter starts at
1446
1426
  // the top — otherwise switching to a smaller window can leave you on page 9
1447
1427
  // of a 2-page result.
1448
1428
  function onShipsFilterChange() {
1449
- shipState.page = 0;
1429
+ shipsPager.page = 1;
1450
1430
  renderRecentShips();
1451
1431
  }
1452
1432
 
@@ -1723,12 +1703,17 @@
1723
1703
  // isn't a peer's work.
1724
1704
 
1725
1705
  let acclaimCanVote = false;
1706
+ // task 1002446: this was the hall's last unpaged list. The endpoint returns the
1707
+ // whole feed in one call, so the rows are held here and the kit's client-side
1708
+ // pager slices them — a page change re-renders without a refetch.
1709
+ const acclaimState = { tasks: [] };
1710
+ const acclaimPager = window.OTBKit.makePager('acclaim', 'otb.standing.acclaimPerPage', { defaultPerPage: 10 });
1726
1711
 
1727
1712
  function renderAcclaimFeed(data) {
1728
1713
  const scroll = $('#acclaim-scroll');
1729
- const tbody = $('#acclaim-feed tbody');
1730
- if (!scroll || !tbody) return;
1714
+ if (!scroll) return;
1731
1715
  const tasks = (data && Array.isArray(data.tasks)) ? data.tasks : [];
1716
+ acclaimState.tasks = tasks;
1732
1717
  acclaimCanVote = !!(data && data.can_vote);
1733
1718
  if (tasks.length === 0) {
1734
1719
  // Nothing to acclaim yet (e.g. the only confirmed work so far is your
@@ -1738,7 +1723,18 @@
1738
1723
  }
1739
1724
  // #748: a gated newcomer never shows the acclaim feed.
1740
1725
  scroll.hidden = newcomerGated;
1741
- tbody.innerHTML = tasks.map((t) => {
1726
+ // task 2174: this feed lands after loadAll built the rail, and it can be the
1727
+ // only visible member of the Karma group (an empty karma feed hides itself).
1728
+ // Re-run renderNav so the group — and its rail entry — appear.
1729
+ if (lastMeState?.builder) renderNav(lastMeState.builder, lastMeState.onboarding || null);
1730
+ renderAcclaimRows();
1731
+ }
1732
+
1733
+ function renderAcclaimRows() {
1734
+ const tbody = $('#acclaim-feed tbody');
1735
+ if (!tbody) return;
1736
+ const slice = window.OTBKit.paginate(acclaimState.tasks, acclaimPager);
1737
+ tbody.innerHTML = slice.map((t) => {
1742
1738
  const by = escapeHtml(t.display_name || (t.github_login ? `@${t.github_login}` : `builder #${t.shipped_by}`));
1743
1739
  const net = Number(t.net_votes) || 0;
1744
1740
  const netCls = signClasses(net, 'acclaim__net');
@@ -1797,6 +1793,12 @@
1797
1793
  buttons.forEach((b) => { b.disabled = true; });
1798
1794
  try {
1799
1795
  const res = await postJSON(`${API}/tasks/${encodeURIComponent(taskId)}/vote`, { vote });
1796
+ // task 1002446: the row is now paged, so the vote has to land in the held
1797
+ // rows too — otherwise flipping to page 2 and back re-renders the stale
1798
+ // server tally over the one just cast.
1799
+ acclaimState.tasks = acclaimState.tasks.map((t) => (Number(t.id) === taskIdNum
1800
+ ? { ...t, net_votes: res.net_votes, my_vote: res.my_vote }
1801
+ : t));
1800
1802
  // Reflect the authoritative net + which button is active.
1801
1803
  const netEl = $(`[data-acclaim-net="${taskId}"]`);
1802
1804
  if (netEl) {
@@ -1823,32 +1825,10 @@
1823
1825
  // the hall's old Claim section + its renderTasks/copy-prompt machinery were
1824
1826
  // removed. work.js carries the equivalent rendering for that page.
1825
1827
 
1826
- // ---- red-team rewards (bounty table, V3.R65 #284) ------------------------
1827
-
1828
- // Severity tiers ship low → critical; the server already returns them in that
1829
- // order (src/bongos/security.js SEVERITIES), but we cap the row classes here so
1830
- // the colouring is deterministic regardless of payload ordering.
1831
- function renderBountyTable(rows) {
1832
- const tbody = $('#bounty-table tbody');
1833
- if (!tbody) return;
1834
- if (!Array.isArray(rows) || !rows.length) {
1835
- tbody.innerHTML = emptyRow(3, 'The reward schedule could not be loaded.');
1836
- return;
1837
- }
1838
- tbody.innerHTML = rows.map((r) => {
1839
- const sev = escapeHtml(r.severity || '');
1840
- const overridden = r.overridden
1841
- ? ` <span style="color:var(--ink-faint);font-size:0.82rem">(tuned)</span>`
1842
- : '';
1843
- return `
1844
- <tr>
1845
- <td style="text-transform:capitalize;font-weight:600">${sev}</td>
1846
- <td class="ledger__drachmae">${fmtNum(r.drachmae)}${overridden}</td>
1847
- <td style="color:var(--ink-soft)">${escapeHtml(r.blurb || '')}</td>
1848
- </tr>
1849
- `;
1850
- }).join('');
1851
- }
1828
+ // (task 2174: the red-team bounty rate card — V3.R65 #284 — left Standing for
1829
+ // /drachmae. It is a published reward SCHEDULE, i.e. reference material that
1830
+ // reads the same for every builder, not part of anyone's personal standing.
1831
+ // drachmae.js now fetches /public/bounty-table and renders it there.)
1852
1832
 
1853
1833
  // ---- idea-inbox triage cue (task 1471) -----------------------------------
1854
1834
 
@@ -1872,13 +1852,18 @@
1872
1852
  const kind = i.kind
1873
1853
  ? `<span class="triage-inbox__kind">${escapeHtml(i.kind)}</span>`
1874
1854
  : '';
1855
+ // task 1002447: an explicit verb, matching the Accept/Decline affordance
1856
+ // the goal slice puts on its own rows. It opens the record overlay, where
1857
+ // promote/merge/discard live — the forms need more room than a summary
1858
+ // row has, and the placement doctrine keeps this card a summary.
1875
1859
  return `<li class="triage-inbox__item">`
1876
1860
  + `<a class="triage-inbox__link" href="#/idea/${encodeURIComponent(i.id)}">${title}</a>`
1877
1861
  + kind
1862
+ + `<button class="btn-ghost" type="button" data-triage-idea="${escapeHtml(String(i.id))}">Triage</button>`
1878
1863
  + `</li>`;
1879
1864
  }).join('');
1880
1865
  const more = openCount > 5
1881
- ? `<p class="ginbox__pending">…${fmtNum(openCount - 5)} more open ideas — open any row to triage in place.</p>`
1866
+ ? `<p class="ginbox__pending">…${fmtNum(openCount - 5)} more open ideas — triage clears them one at a time.</p>`
1882
1867
  : '';
1883
1868
  box.innerHTML = `<h3 class="ginbox__sec-h">Ideas awaiting triage <span class="count-pill">${fmtNum(openCount)}</span></h3>`
1884
1869
  + `<ul class="triage-inbox">${rows}</ul>${more}`;
@@ -1886,6 +1871,30 @@
1886
1871
  window.OTBAttention?.refresh();
1887
1872
  }
1888
1873
 
1874
+ // task 1002447: refetch the slice so a verdict advances the queue in place —
1875
+ // the next open ideas move up into the five visible rows instead of the card
1876
+ // sitting stale until a reload. Metic+ only, exactly as the initial load is:
1877
+ // loadAll sets the flag from the /me it already has, so both paths read one
1878
+ // gate and a non-triager never issues an /inbox request at all.
1879
+ let canTriage = false;
1880
+ function loadTriageInbox() {
1881
+ if (!canTriage) return;
1882
+ getJSON(`${API}/inbox`)
1883
+ .then((data) => renderTriageInbox(data))
1884
+ .catch(() => { /* nice-to-have; never break the hall */ });
1885
+ }
1886
+
1887
+ // task-detail.js announces a landed verdict from the record overlay; the row
1888
+ // it acted on is one of ours.
1889
+ window.addEventListener('otb:idea-triaged', loadTriageInbox);
1890
+
1891
+ // The row's explicit verb: open the record overlay, where the three triage
1892
+ // forms live. Delegated so it survives every re-render of the slice.
1893
+ document.addEventListener('click', (e) => {
1894
+ const btn = e.target.closest('[data-triage-idea]');
1895
+ if (btn) window.location.hash = `#/idea/${encodeURIComponent(btn.dataset.triageIdea)}`;
1896
+ });
1897
+
1889
1898
  // ---- Home "Next up" slice (task 2184) -------------------------------------
1890
1899
 
1891
1900
  // Fill the top of the "Your work" card with the first two rows of the
@@ -1950,12 +1959,12 @@
1950
1959
 
1951
1960
  const lbState = {
1952
1961
  rows: [],
1953
- page: 0,
1954
- pageSize: 25, // task 1750: top-100 Roll — 25/page keeps it to a few pages
1955
-
1956
1962
  sortBy: 'drachmae', // 'rank' | 'drachmae' | 'karma' | 'tokens'
1957
1963
  sortDir: 'desc', // 'desc' | 'asc'
1958
1964
  };
1965
+ // task 2174: OTBKit pager. 25/page is what task 1750 set for the top-100 Roll;
1966
+ // the per-page select now lets a builder widen it and the choice persists.
1967
+ const lbPager = window.OTBKit.makePager('lb', 'otb.standing.leaderboardPerPage', { defaultPerPage: 25 });
1959
1968
 
1960
1969
  // Stable sort. Server returns rows in drachmae-DESC order, which is also the
1961
1970
  // default view; for other modes (rank, karma, drachmae-ASC), re-sort here.
@@ -2051,9 +2060,9 @@
2051
2060
 
2052
2061
  function renderLeaderboard() {
2053
2062
  const rows = sortedLeaderboardRows();
2054
- const start = lbState.page * lbState.pageSize;
2055
- const end = start + lbState.pageSize;
2056
- const slice = rows.slice(start, end);
2063
+ // paginate() clamps the page, slices, and paints the whole #lb-* bar.
2064
+ const slice = window.OTBKit.paginate(rows, lbPager);
2065
+ const start = (lbPager.page - 1) * lbPager.perPage;
2057
2066
  const tbody = $('#leaderboard tbody');
2058
2067
 
2059
2068
  if (!slice.length) {
@@ -2094,12 +2103,6 @@
2094
2103
  : 'none';
2095
2104
  head.setAttribute('aria-sort', ariaSort);
2096
2105
  }
2097
-
2098
- $('#lb-range').textContent = rows.length
2099
- ? `${start + 1}–${Math.min(end, rows.length)} of ${rows.length}`
2100
- : '0';
2101
- $('#lb-prev').disabled = lbState.page === 0;
2102
- $('#lb-next').disabled = end >= rows.length;
2103
2106
  }
2104
2107
 
2105
2108
  // Cycle: pick column → desc → asc → back to default (drachmae desc).
@@ -2120,7 +2123,7 @@
2120
2123
  lbState.sortBy = key;
2121
2124
  lbState.sortDir = 'desc';
2122
2125
  }
2123
- lbState.page = 0;
2126
+ lbPager.page = 1;
2124
2127
  renderLeaderboard();
2125
2128
  }
2126
2129
 
@@ -2188,6 +2191,9 @@
2188
2191
  lastKarmaState = me.karma || lastKarmaState;
2189
2192
  lastCostState = me.cost || lastCostState;
2190
2193
  lastNeedsState = me.needs || lastNeedsState;
2194
+ // ADR 0120 part 5: owed-rebase list is authoritative on every /me (an empty
2195
+ // array means "nothing owed now"), so take it verbatim when present.
2196
+ if (Array.isArray(me.needs_rebase_tasks)) lastRebaseState = me.needs_rebase_tasks;
2191
2197
  renderOnboarding(me.onboarding || null, me.discord);
2192
2198
  // If they graduated while watching, hide the welcome panel + rebuild the nav
2193
2199
  // (drops the Welcome/Path chips) — the same transition loadAll would render.
@@ -2218,6 +2224,7 @@
2218
2224
  lastKarmaState = me.karma || null;
2219
2225
  lastCostState = me.cost || null;
2220
2226
  lastNeedsState = me.needs || null; // task 1013 / ADR 0073: action-needed banner
2227
+ lastRebaseState = Array.isArray(me.needs_rebase_tasks) ? me.needs_rebase_tasks : null; // ADR 0120 part 5: owed-rebase banner
2221
2228
  // #408: stash the whole /me payload so reopenWelcomeFromNav can re-render
2222
2229
  // the welcome panel without a fresh round-trip.
2223
2230
  lastMeState = me;
@@ -2275,22 +2282,18 @@
2275
2282
  // task 1471: idea-inbox triage cue — Metic+ only (triage is a Metic+ action).
2276
2283
  // Fetched fire-and-forget so a slow/large inbox can't hold up the hall; the
2277
2284
  // widget is hiddenByDefault, so a non-triager never loads or sees idea data.
2278
- if (['metic', 'archon'].includes(normalizeRank(me.builder?.rank))) {
2279
- getJSON(`${API}/inbox`)
2280
- .then((data) => renderTriageInbox(data))
2281
- .catch(() => { /* nice-to-have; never break the hall */ });
2282
- }
2285
+ canTriage = ['metic', 'archon'].includes(normalizeRank(me.builder?.rank));
2286
+ loadTriageInbox();
2283
2287
 
2284
2288
  // Then the rest, in parallel. Any individual section failure shouldn't take
2285
2289
  // down the page — log and show an inline placeholder.
2286
2290
  // #726: the per-skill model picker (Voices) moved to /builders/settings,
2287
2291
  // so the hall no longer fetches /me/skill-prefs here.
2288
- const [achAll, ships, versionsResp, lb, bounty] = await Promise.allSettled([
2292
+ const [achAll, ships, versionsResp, lb] = await Promise.allSettled([
2289
2293
  getJSON(`${API}/achievements`),
2290
2294
  getJSON(`${API}/me/recent-ships?limit=all`),
2291
2295
  getJSON(`${API}/versions`),
2292
2296
  getJSON(`${API}/leaderboard`),
2293
- getJSON(`${API}/public/bounty-table`),
2294
2297
  ]);
2295
2298
 
2296
2299
  if (achAll.status === 'fulfilled') {
@@ -2323,7 +2326,7 @@
2323
2326
 
2324
2327
  if (lb.status === 'fulfilled') {
2325
2328
  lbState.rows = lb.value.leaderboard || [];
2326
- lbState.page = 0;
2329
+ lbPager.page = 1;
2327
2330
  renderLeaderboard();
2328
2331
  // #757: the Standing card painted before the Roll loaded, so re-render it
2329
2332
  // now that lbState.rows exists — this is when its "#N on the Roll" stat
@@ -2332,13 +2335,6 @@
2332
2335
  } else {
2333
2336
  $('#leaderboard tbody').innerHTML = emptyRow(5, 'The leaderboard could not be loaded.');
2334
2337
  }
2335
-
2336
- if (bounty.status === 'fulfilled') {
2337
- renderBountyTable(bounty.value.table || []);
2338
- } else {
2339
- $('#bounty-table tbody').innerHTML = emptyRow(3, 'The reward schedule could not be loaded.');
2340
- }
2341
-
2342
2338
  }
2343
2339
 
2344
2340
  function wireEvents() {
@@ -2408,22 +2404,11 @@
2408
2404
  $('#f-ships-window')?.addEventListener('change', onShipsFilterChange);
2409
2405
  $('#f-ships-version')?.addEventListener('change', onShipsFilterChange);
2410
2406
  $('#f-ships-sort')?.addEventListener('change', renderRecentShips);
2411
- $('#ships-prev')?.addEventListener('click', () => {
2412
- if (shipState.page > 0) { shipState.page--; renderRecentShips(); }
2413
- });
2414
- $('#ships-next')?.addEventListener('click', () => {
2415
- shipState.page++;
2416
- renderRecentShips();
2417
- });
2418
-
2419
-
2420
- $('#lb-prev').addEventListener('click', () => {
2421
- if (lbState.page > 0) { lbState.page--; renderLeaderboard(); }
2422
- });
2423
- $('#lb-next').addEventListener('click', () => {
2424
- const max = Math.ceil(lbState.rows.length / lbState.pageSize) - 1;
2425
- if (lbState.page < max) { lbState.page++; renderLeaderboard(); }
2426
- });
2407
+ // task 2174: the kit wires prev/next/per-page for both tables against the
2408
+ // #ships-*/#lb-* ids its own pagerBarHtml() stamped in hall-widgets.js.
2409
+ window.OTBKit.wirePager(shipsPager, renderRecentShips);
2410
+ window.OTBKit.wirePager(lbPager, renderLeaderboard);
2411
+ window.OTBKit.wirePager(acclaimPager, renderAcclaimRows);
2427
2412
 
2428
2413
  // Sortable leaderboard headers — clickable + keyboard-operable. Each
2429
2414
  // sortable column registers under LB_SORT_HEADERS; the click handler
@@ -2442,28 +2427,11 @@
2442
2427
  });
2443
2428
  }
2444
2429
 
2445
- // Logout link: the /api/bongos/auth/logout endpoint is POST + requires the
2446
- // session cookie. Convert the anchor click into a fetch then bounce home.
2447
- $('#logout-link').addEventListener('click', async (e) => {
2448
- e.preventDefault();
2449
- try {
2450
- await api.request('POST', `${API}/auth/logout`);
2451
- } catch (_) { /* even if it fails we drop the cookie below */ }
2452
- // Best-effort cookie clear on the client side too. Mirror the SERVER
2453
- // logout (src/bongos/routes/auth.js): expire BOTH the current `gds_session`
2454
- // and the legacy `pms_session`, each in BOTH the host-only form and the
2455
- // apex-scoped `Domain=.amazonprimea.com` form (#726 — the session cookie
2456
- // is set apex-scoped on production hosts so it crosses subdomains; an
2457
- // expire only matches the cookie whose Domain attribute it names, so the
2458
- // host-only Max-Age=0 alone leaves the apex cookie live). Clearing only
2459
- // `pms_session` host-only — the prior behaviour — left a real
2460
- // `gds_session` cookie behind on this offline fallback path.
2461
- for (const name of ['gds_session', 'pms_session']) {
2462
- document.cookie = `${name}=; Max-Age=0; path=/`;
2463
- document.cookie = `${name}=; Max-Age=0; path=/; Domain=.${location.hostname.split('.').slice(-2).join('.')}`;
2464
- }
2465
- window.location.assign('/');
2466
- });
2430
+ // Sign-out is wired centrally in shell.js (doLogout + a delegated click
2431
+ // listener on the sign-out anchors), so it runs the real POST -> cookie-clear
2432
+ // -> bounce-home on EVERY hall page, not only this index. It used to live
2433
+ // here alone, which is exactly why sign-out silently no-op'd everywhere else
2434
+ // (a bare GET to the POST-only endpoint; task 1002245). Nothing to wire here.
2467
2435
  }
2468
2436
 
2469
2437
  // ---- Live updates (#569 / ADR 0030 → R55 / ADR 0069) -------------------
@@ -107,6 +107,8 @@
107
107
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
108
108
  time so the chrome exists before the deferred page scripts execute. -->
109
109
  <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
110
+ <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
111
+ <script src="/builders/palette.js" defer></script>
110
112
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
111
113
  <script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
112
114
  </body>