@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
@@ -1,29 +1,30 @@
1
- // roadmap.js — the Roadmap page (task 1757, owner request from the task-1723
2
- // review): every declared version's progress, with its goals nested
3
- // underneath (status, criteria, inline join/leave). Carved out of the Work
4
- // Board's "Versions, by progress" section (work.js) so the board stays about
5
- // claimable work and this page owns the bigger picture. Server gate is
6
- // SIGNEDIN_BUILDERS_PAGE_RE in serve-internal.js.
1
+ // roadmap.js — the Roadmap page, "Command deck" layout (task 1002426, owner-
2
+ // approved redesign from the mockup review; supersedes the flat progress-bar
3
+ // stack introduced in task 1757).
7
4
  //
8
- // Reads: GET /me (viewer id + rank — rank gates the task-1760 drag handle so
9
- // only an Archon sees it, though the server is the real enforcement), GET
10
- // /public/progress (version lifecycle rollup — public, never gated), GET
11
- // /goals?version=X + GET /goals/:id (per-version goals + members/criteria).
12
- // Writes: POST /goals/:id/join | /leave, POST /goals/reorder (Archon-only,
13
- // task 1760 — persists a version's goal priority order via drag-to-reorder).
5
+ // Shape: a program-level hero (whole-build rollup) → versions GROUPED BY their
6
+ // real status (planning=Staged · building=In flight · frozen=Parked ·
7
+ // shipped=Shipped), with the dominant in-flight version featured full-width. The
8
+ // goal layer is SUMMARIZED per version — a status bar (achieved / in progress /
9
+ // not started) plus only the goals active right now — instead of nesting every
10
+ // goal + criterion inline (BONGOS-V1 alone carries ~30 goals; the old flat list
11
+ // was the big-picture killer). The full per-version goal list, inline join/leave,
12
+ // and Archon drag-to-reorder now live on the Goals page, which "See all N goals"
13
+ // links to.
14
+ //
15
+ // Reads (all GET): /public/progress (lifecycle rollup + counts + %, public),
16
+ // /public/versions (track + started/shipped dates, public), and — for every
17
+ // non-shipped version only — /goals?version=X + /goals/:id (per-goal criteria to
18
+ // split in-progress from not-started). Shipped versions render as a pill strip
19
+ // with no goal fetch. No writes. Server page gate: SIGNEDIN_BUILDERS_PAGE_RE.
14
20
  (() => {
15
21
  'use strict';
16
22
 
17
23
  const API = '/api/bongos';
18
- // Generated typed client (task 1996 / R09), served as window.BongosClient by
19
- // /lib/bongos-client.js. baseUrl:'' passes the full same-origin paths this file
20
- // builds through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
21
24
  const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
22
25
  const { escapeHtml, $, fmtNum } = window.OTB;
23
26
  const toast = (msg) => window.OTB.toast(msg, { duration: 2200, clear: true });
24
27
 
25
- const viewer = { id: null, isArchon: false };
26
-
27
28
  async function getJSON(path) {
28
29
  const res = await api.request('GET', path);
29
30
  if (res.status === 401) {
@@ -34,342 +35,354 @@
34
35
  if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
35
36
  return data;
36
37
  }
37
- async function postJSON(path, body) {
38
- const res = await api.request('POST', path, { body: body || {} });
39
- if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw new Error('redirecting to auth'); }
40
- const data = res.data || {};
41
- if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
42
- return data;
43
- }
44
38
 
45
- // ---- version + goal rendering (moved from work.js, task 1757) ------------
39
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
40
+ const TRACK = {
41
+ product: { label: 'Product', cls: 'product' },
42
+ internal: { label: 'Platform', cls: 'internal' },
43
+ default: { label: 'Dogfood', cls: 'dogfood' },
44
+ };
45
+ const PHASE = {
46
+ building: { label: 'In flight', cls: 'building' },
47
+ planning: { label: 'Staged', cls: 'planning' },
48
+ frozen: { label: 'Parked', cls: 'frozen' },
49
+ };
50
+ // The instance's Goals page (root-relative — resolves on the builders host).
51
+ const GOALS_URL = '/goals';
52
+ const goalDeepLink = (id) => `/goals#/goal/${encodeURIComponent(String(id))}`;
46
53
 
47
- function goalBadge(status) {
48
- const cls = status === 'achieved' ? 'goal-badge--achieved'
49
- : (status === 'archived' ? 'goal-badge--archived' : 'goal-badge--open');
50
- return `<span class="goal-badge ${cls}">${escapeHtml(status)}</span>`;
51
- }
54
+ const pctOf = (n, d) => (d > 0 ? (n / d) * 100 : 0);
55
+ const ym = (s) => { if (!s) return ''; const d = new Date(s); return Number.isNaN(d.getTime()) ? '' : `${MON[d.getMonth()]} ${d.getFullYear()}`; };
56
+ const trackOf = (v) => TRACK[v.track] || TRACK.internal;
52
57
 
53
- function critListHtml(criteria) {
54
- return `<ul class="vbar__crit-list">${criteria.map((c) => {
55
- const { label, detail } = window.OTB.splitCriterion(c.criterion_md || '');
56
- const text = detail
57
- ? `<details class="vbar__crit-d"><summary class="vbar__crit-text">${escapeHtml(label)}</summary><div class="vbar__crit-full">${escapeHtml(detail)}</div></details>`
58
- : `<span class="vbar__crit-text">${escapeHtml(label)}</span>`;
59
- return `<li class="vbar__crit${c.satisfied ? ' vbar__crit--done' : ''}"><span class="vbar__crit-mark" aria-hidden="true">${c.satisfied ? '✓' : '○'}</span>${text}</li>`;
60
- }).join('')}</ul>`;
58
+ const chip = (v) => { const t = trackOf(v); return `<span class="rm-chip rm-chip--${t.cls}"><span class="rm-sq"></span>${t.label}</span>`; };
59
+ const phase = (v) => { const p = PHASE[v.status]; return p ? `<span class="rm-phase rm-phase--${p.cls}"><span class="rm-pd"></span>${p.label}</span>` : ''; };
60
+
61
+ function ring(v) {
62
+ const done = v.pct >= 100;
63
+ return `<div class="rm-ring" style="--p:${v.pct};--rc:${done ? 'var(--ok)' : 'var(--accent)'}" role="img" aria-label="${v.pct}% of tasks complete"><b>${v.pct}<i>%</i></b></div>`;
64
+ }
65
+ function lbar(v) {
66
+ const t = v.total, l = v.life, r = Math.max(0, t - (l.sh + l.co + l.cm));
67
+ return `<div class="rm-lbar" role="img" aria-label="${l.sh} shipped, ${l.co} confirmed, ${l.cm} completed, ${r} remaining of ${t}">
68
+ <span class="rm-lbar__s rm-lbar__s--sh" style="width:${pctOf(l.sh, t)}%"></span>
69
+ <span class="rm-lbar__s rm-lbar__s--co" style="width:${pctOf(l.co, t)}%"></span>
70
+ <span class="rm-lbar__s rm-lbar__s--cm" style="width:${pctOf(l.cm, t)}%"></span></div>`;
71
+ }
72
+ function gbar(g) {
73
+ return `<div class="rm-gbar" role="img" aria-label="${g.ach} achieved, ${g.prog} in progress, ${g.plan} not started of ${g.total} goals">
74
+ <span class="rm-gbar__s rm-gbar__s--ach" style="width:${pctOf(g.ach, g.total)}%"></span>
75
+ <span class="rm-gbar__s rm-gbar__s--prog" style="width:${pctOf(g.prog, g.total)}%"></span>
76
+ <span class="rm-gbar__s rm-gbar__s--plan" style="width:${pctOf(g.plan, g.total)}%"></span></div>`;
61
77
  }
62
78
 
63
- // One goal under a version: expandable to its criteria, with inline Join/Leave.
64
- // d = { goal, members, criteria }. `priority` is this goal's 1-based rank
65
- // within its version's list (the server's sort_order order, task 1760).
66
- function goalItemHtml(d, priority) {
67
- const g = d.goal;
68
- const crit = Array.isArray(d.criteria) ? d.criteria : [];
69
- const sat = crit.filter((c) => c.satisfied).length;
70
- const achieved = g.status === 'achieved';
71
- const joined = (d.members || []).some((m) => String(m.builder_id) === String(viewer.id));
72
- // task 1761: an open goal blocked by a not-yet-achieved prerequisite goal is
73
- // greyed + flagged (the tooltip names the goals it waits on).
74
- const unmetDeps = (Array.isArray(d.dependencies) ? d.dependencies : []).filter((dep) => !dep.satisfied);
75
- const blocked = g.status === 'open' && unmetDeps.length > 0;
76
- const blockedBadge = blocked
77
- ? `<span class="goal-badge goal-badge--blocked" title="Blocked until achieved: ${escapeHtml(unmetDeps.map((u) => u.title || ('goal ' + u.id)).join(', '))}">🔒 blocked</span>`
78
- : '';
79
- // Expanded by default when mid-flight (some but not all criteria met).
80
- const expanded = crit.length && sat > 0 && sat < crit.length;
81
- const critBlock = crit.length ? critListHtml(crit) : '<p class="vgoal__nocrit">No criteria yet.</p>';
82
- const action = joined
83
- ? `<button type="button" class="vgoal-action vgoal-leave" data-act="leave" data-id="${escapeHtml(String(g.id))}">Leave</button>`
84
- : (g.status === 'open'
85
- ? `<button type="button" class="vgoal-action vgoal-join" data-act="join" data-id="${escapeHtml(String(g.id))}">Join</button>`
86
- : '');
87
- // Priority is always visible ("sees order"); the drag handle is Archon-only
88
- // ("can't change it" for everyone else) — the server-side rank gate on
89
- // POST /goals/reorder is the real enforcement, this is just the affordance.
90
- const handle = viewer.isArchon
91
- ? `<span class="vgoal__handle" draggable="true" title="Drag to reprioritize within ${escapeHtml(g.version_id || 'this version')}" aria-hidden="true">⠿</span>`
92
- : '';
93
- // A custom disclosure (not <details>/<summary>): the criteria toggle is its own
94
- // button and Join/Leave is a plain sibling button — so neither click target is
95
- // nested in a <summary>, where the native details-toggle makes buttons flaky.
96
- return `
97
- <div class="vgoal${achieved ? ' vgoal--achieved' : ''}${blocked ? ' vgoal--blocked' : ''}" data-goal-id="${escapeHtml(String(g.id))}" data-version-id="${escapeHtml(String(g.version_id || ''))}"${viewer.isArchon ? ' draggable="true"' : ''}>
98
- <div class="vgoal__head">
99
- ${handle}
100
- <span class="vgoal__priority" title="Priority within this version">${priority}</span>
101
- <button type="button" class="vgoal__toggle" aria-expanded="${expanded ? 'true' : 'false'}">
102
- <span class="vgoal__caret${expanded ? ' vgoal__caret--open' : ''}" aria-hidden="true">▸</span>
103
- </button>
104
- <a class="vgoal__title vgoal__title--link" href="/goals#/goal/${escapeHtml(String(g.id))}" title="Open goal #${escapeHtml(String(g.id))} detail">${escapeHtml(g.title || 'Untitled goal')}</a>
105
- ${goalBadge(g.status)}
106
- ${blockedBadge}
107
- <span class="vgoal__prog">${sat}/${crit.length}</span>
108
- ${action}
109
- </div>
110
- <div class="vgoal__body"${expanded ? '' : ' hidden'}>
111
- <span class="vgoal__err" data-err="${escapeHtml(String(g.id))}" hidden></span>
112
- ${critBlock}
113
- </div>
79
+ const arrowSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg>';
80
+
81
+ // The goals block that sits inside an in-flight / featured card. `active` are
82
+ // the in-progress goals (sat>0), already sorted; we show up to 4 and let
83
+ // "See all" carry the rest.
84
+ function goalsBlock(v) {
85
+ const g = v.goals;
86
+ if (!g || g.total === 0) {
87
+ return `<div class="rm-goals__frame"><div class="rm-goals__h"><span class="t">Goals</span></div>
88
+ <p class="rm-gnone">No goals declared yet — this version is tracked by its ${fmtNum(v.criteria)} done-when criteria.</p></div>`;
89
+ }
90
+ const active = g.active.slice(0, 4);
91
+ const rows = active.map((x) => {
92
+ const ready = x.total > 0 && x.sat >= x.total;
93
+ return `<div class="rm-goal">
94
+ <span class="rm-goal__dot"></span>
95
+ <a class="rm-goal__t" href="${goalDeepLink(x.id)}" title="Open goal #${escapeHtml(String(x.id))}">${escapeHtml(x.title || 'Untitled goal')}</a>
96
+ ${ready ? '<span class="rm-goal__ready">criteria met</span>' : ''}
97
+ <span class="rm-goal__meter"><i style="width:${pctOf(x.sat, x.total)}%"></i></span>
98
+ <span class="rm-goal__n">${x.sat}/${x.total}</span>
114
99
  </div>`;
100
+ }).join('');
101
+ return `<div class="rm-goals__frame">
102
+ <div class="rm-goals__h"><span class="t">Goals</span><span class="n">${g.total} total</span></div>
103
+ ${gbar(g)}
104
+ <div class="rm-gdist">
105
+ <span><i class="rm-ach"></i><b>${g.ach}</b> achieved</span>
106
+ <span><i class="rm-prog"></i><b>${g.prog}</b> in progress</span>
107
+ <span><i class="rm-plan"></i><b>${g.plan}</b> not started</span>
108
+ </div>
109
+ ${active.length ? `<div class="rm-ginp"><div class="rm-ginp__cap">Active right now</div>${rows}</div>`
110
+ : '<p class="rm-gnone" style="margin-top:12px">No goal has criteria met yet — work is in the task queue.</p>'}
111
+ <a class="rm-gmore" href="${GOALS_URL}">See all ${g.total} goals ${arrowSvg}</a>
112
+ </div>`;
115
113
  }
116
114
 
117
- function renderGoals(details, desc) {
118
- const goals = Array.isArray(details) ? details : [];
119
- const description = (desc || '').trim();
120
- if (!goals.length && !description) return '';
121
- const achieved = goals.filter((d) => d.goal.status === 'achieved').length;
122
- const summary = goals.length ? `${achieved} of ${goals.length} goals achieved` : 'The work, in brief';
123
- const descBlock = description ? `<p class="vbar__desc">${escapeHtml(description)}</p>` : '';
124
- const list = goals.length ? `<div class="vgoal-list">${goals.map((d, i) => goalItemHtml(d, i + 1)).join('')}</div>` : '';
125
- // Auto-open when mid-flight (some but not all goals achieved).
126
- const open = goals.length && achieved > 0 && achieved < goals.length ? ' open' : '';
127
- return `
128
- <details class="vbar__criteria"${open}>
129
- <summary>${summary}</summary>
130
- <div class="vbar__details-body">${descBlock}${list}</div>
131
- </details>`;
115
+ // ---- card renderers -------------------------------------------------------
116
+
117
+ function featCard(v) {
118
+ return `<article class="rm-card rm-card--feat">
119
+ <div class="rm-featgrid">
120
+ <div class="rm-featgrid__l">
121
+ <div class="rm-feat-top">
122
+ <div class="rt">
123
+ <div class="rm-row">${chip(v)}${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span></div>
124
+ <h3 class="rm-name">${escapeHtml(v.name)}</h3>
125
+ <p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
126
+ </div>
127
+ ${ring(v)}
128
+ </div>
129
+ <div class="rm-cardprog">
130
+ <div class="rm-count"><span><b>${fmtNum(v.shipped)}</b> of <b>${fmtNum(v.total)}</b> tasks</span><span>${fmtNum(v.criteria)} criteria · since ${ym(v.start)}</span></div>
131
+ ${lbar(v)}
132
+ </div>
133
+ </div>
134
+ <div class="rm-goals">${goalsBlock(v)}</div>
135
+ </div>
136
+ </article>`;
132
137
  }
133
138
 
134
- // One version's progress card. Pulled out so the same markup serves both the
135
- // live, in-flight versions (rendered inline) and the shipped ones (folded away
136
- // below). `done=true` adds the "shipped" badge and a softened look.
137
- function versionBarHtml(v, goalsByVersion, done) {
138
- const total = Number(v.total_count) || 0;
139
- const cShipped = Number(v.lifecycle_shipped_count) || 0;
140
- const cConfirmed = Number(v.lifecycle_confirmed_count) || 0;
141
- const cCompleted = Number(v.lifecycle_completed_count) || 0;
142
- const pctOf = (n) => (total > 0 ? (n / total) * 100 : 0);
143
- const pct = Number(v.percent_complete) || 0;
144
- const tail = (cConfirmed + cCompleted > 0)
145
- ? ` · ${cShipped} shipped, ${cConfirmed} confirmed, ${cCompleted} completed`
146
- : '';
147
- const detailsBlock = renderGoals(goalsByVersion.get(v.version_id), v.done_when);
148
- const badge = done
149
- ? ' <span class="vbar__done-badge" title="shipped — no work remains">shipped</span>'
150
- : '';
151
- return `
152
- <div class="vbar${done ? ' vbar--done' : ''}">
153
- <div class="vbar__top">
154
- <span class="vbar__name">${escapeHtml(v.version_id)} · ${escapeHtml(v.name || '')}${badge}
155
- </span>
156
- <span class="vbar__count">${fmtNum(v.shipped_count)} of ${fmtNum(v.total_count)} shipped</span>
139
+ function flightCard(v) {
140
+ return `<article class="rm-card">
141
+ <div class="rm-feat-top">
142
+ <div class="rt">
143
+ <div class="rm-row">${chip(v)}${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span></div>
144
+ <h3 class="rm-name">${escapeHtml(v.name)}</h3>
145
+ <p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
157
146
  </div>
158
- <div class="vbar__bar">
159
- <div class="vbar__seg vbar__seg--shipped" style="width:${pctOf(cShipped)}%" title="${cShipped} shipped (live)"></div>
160
- <div class="vbar__seg vbar__seg--confirmed" style="width:${pctOf(cConfirmed)}%" title="${cConfirmed} confirmed (awaiting merge)"></div>
161
- <div class="vbar__seg vbar__seg--completed" style="width:${pctOf(cCompleted)}%" title="${cCompleted} completed (awaiting verification)"></div>
147
+ ${ring(v)}
148
+ </div>
149
+ <div class="rm-cardprog">
150
+ <div class="rm-count"><span><b>${fmtNum(v.shipped)}</b> of <b>${fmtNum(v.total)}</b> tasks</span><span>${fmtNum(v.criteria)} criteria · since ${ym(v.start)}</span></div>
151
+ ${lbar(v)}
152
+ </div>
153
+ <div style="margin-top:13px">${goalsBlock(v)}</div>
154
+ </article>`;
155
+ }
156
+
157
+ // Staged (planning) card — goals-forward. No task progress yet, so it leads
158
+ // with the seeded plan instead of a lifecycle bar.
159
+ function stagedCard(v) {
160
+ const g = v.goals;
161
+ const seeded = (g && g.all ? g.all : []).slice(0, 4);
162
+ const rows = seeded.map((x) => `
163
+ <div class="rm-goal"><span class="rm-goal__dot" style="background:var(--rm-plan)"></span>
164
+ <a class="rm-goal__t" href="${goalDeepLink(x.id)}">${escapeHtml(x.title || 'Untitled goal')}</a>
165
+ <span class="rm-goal__seed">seeded</span></div>`).join('');
166
+ const totalGoals = g ? g.total : 0;
167
+ const goalsFrame = totalGoals === 0
168
+ ? `<div class="rm-goals__frame"><div class="rm-goals__h"><span class="t">Goals</span></div>
169
+ <p class="rm-gnone">No goals seeded yet.</p></div>`
170
+ : `<div class="rm-goals__frame">
171
+ <div class="rm-goals__h"><span class="t">Goals</span><span class="n">${totalGoals} seeded</span></div>
172
+ ${gbar(g)}
173
+ <div class="rm-gdist">
174
+ <span><i class="rm-ach"></i><b>${g.ach}</b> achieved</span>
175
+ <span><i class="rm-prog"></i><b>${g.prog}</b> in progress</span>
176
+ <span><i class="rm-plan"></i><b>${g.plan}</b> seeded</span>
177
+ </div>
178
+ <div class="rm-ginp"><div class="rm-ginp__cap">The seeded plan</div>${rows}</div>
179
+ <a class="rm-gmore" href="${GOALS_URL}">See all ${totalGoals} goals ${arrowSvg}</a>
180
+ </div>`;
181
+ return `<article class="rm-card rm-card--staged" style="grid-column:1 / -1">
182
+ <div class="rm-featgrid">
183
+ <div class="rm-featgrid__l">
184
+ <div class="rm-row">${chip(v)}${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span></div>
185
+ <h3 class="rm-name">${escapeHtml(v.name)}</h3>
186
+ <p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
187
+ <div class="rm-staged-state">
188
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v4M9 6l3-3 3 3"/><rect x="4" y="9" width="16" height="12" rx="2"/></svg>
189
+ <div><div class="s1">Goals seeded — awaiting kickoff</div>
190
+ <div class="s2"><b>${totalGoals}</b> goals · <b>${fmtNum(v.criteria)}</b> criteria planned · no tasks yet</div></div>
191
+ </div>
162
192
  </div>
163
- <div class="vbar__pct">${pct}% complete · weighted by priority${tail}</div>
164
- ${detailsBlock}
193
+ <div class="rm-goals">${goalsFrame}</div>
165
194
  </div>
166
- `;
195
+ </article>`;
167
196
  }
168
197
 
169
- function renderVersionBars(progress, goalsByVersion) {
170
- const root = $('#roadmap-versions');
171
- if (!root) return;
172
- const rows = Array.isArray(progress) ? progress : [];
173
- const gbv = goalsByVersion instanceof Map ? goalsByVersion : new Map();
174
- if (!rows.length) {
175
- root.innerHTML = `<div class="profile__placeholder">No versions declared yet.</div>`;
176
- return;
177
- }
178
- // A version with status 'shipped' is history — its bar reads 100% and it
179
- // only crowds out the live work. Render the open versions inline; fold the
180
- // finished ones into one collapsed reveal the reader can expand.
181
- const live = rows.filter((v) => v.status !== 'shipped');
182
- const done = rows.filter((v) => v.status === 'shipped');
198
+ function parkedCard(v) {
199
+ return `<article class="rm-card rm-card--parked" style="grid-column:1 / -1">
200
+ <div class="rm-feat-top">
201
+ <div class="rt">
202
+ <div class="rm-row">${chip(v)}${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span></div>
203
+ <h3 class="rm-name">${escapeHtml(v.name)}</h3>
204
+ <p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
205
+ </div>
206
+ ${ring(v)}
207
+ </div>
208
+ <div class="rm-cardprog">
209
+ <div class="rm-count"><span><b>${fmtNum(v.shipped)}</b> of <b>${fmtNum(v.total)}</b> tasks</span><span>${fmtNum(v.criteria)} criteria · started ${ym(v.start)}</span></div>
210
+ ${lbar(v)}
211
+ </div>
212
+ <div class="rm-parknote">
213
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="6" height="16" rx="1"/><rect x="14" y="4" width="6" height="16" rx="1"/></svg>
214
+ <div>Work is complete but the version is held — no tasks are being claimed here right now.</div>
215
+ </div>
216
+ </article>`;
217
+ }
183
218
 
184
- const liveHtml = live.length
185
- ? live.map((v) => versionBarHtml(v, gbv, false)).join('')
186
- : `<div class="profile__placeholder">Every declared version has shipped — nothing is in flight right now.</div>`;
219
+ function sectionHtml(dotCls, title, count, note, inner) {
220
+ return `<section class="rm-sec">
221
+ <div class="rm-sec__h"><span class="rm-sec__dot rm-sec__dot--${dotCls}"></span><span class="rm-sec__t">${title}</span><span class="rm-sec__n">${count}</span><span class="rm-sec__rule"></span></div>
222
+ ${note ? `<p class="rm-gnone" style="margin:-4px 2px 13px">${note}</p>` : ''}
223
+ ${inner}
224
+ </section>`;
225
+ }
187
226
 
188
- let doneHtml = '';
189
- if (done.length) {
190
- const n = done.length;
191
- const noun = n === 1 ? 'version' : 'versions';
192
- doneHtml = `
193
- <details class="vbar__done-fold">
194
- <summary>${n} ${noun} already shipped — collapsed to keep the focus on live work. <span class="vbar__done-cta">Show ${n === 1 ? 'it' : 'them'}.</span></summary>
195
- <div class="vbars vbar__done-body">
196
- ${done.map((v) => versionBarHtml(v, gbv, true)).join('')}
227
+ function heroHtml(flight) {
228
+ const A = flight.reduce((a, v) => ({
229
+ total: a.total + v.total, shipped: a.shipped + v.shipped,
230
+ sh: a.sh + v.life.sh, co: a.co + v.life.co, cm: a.cm + v.life.cm,
231
+ }), { total: 0, shipped: 0, sh: 0, co: 0, cm: 0 });
232
+ const G = flight.reduce((a, v) => ({
233
+ total: a.total + (v.goals ? v.goals.total : 0), ach: a.ach + (v.goals ? v.goals.ach : 0),
234
+ prog: a.prog + (v.goals ? v.goals.prog : 0), plan: a.plan + (v.goals ? v.goals.plan : 0),
235
+ }), { total: 0, ach: 0, prog: 0, plan: 0 });
236
+ const heroPct = Math.round(pctOf(A.shipped, A.total));
237
+ const heroRem = A.total - A.shipped;
238
+ const goalsRow = G.total > 0 ? `
239
+ <div class="rm-hgoals">
240
+ <span class="glab">Goals</span>
241
+ <div class="rm-gbar">
242
+ <span class="rm-gbar__s rm-gbar__s--ach" style="width:${pctOf(G.ach, G.total)}%"></span>
243
+ <span class="rm-gbar__s rm-gbar__s--prog" style="width:${pctOf(G.prog, G.total)}%"></span>
244
+ <span class="rm-gbar__s rm-gbar__s--plan" style="width:${pctOf(G.plan, G.total)}%"></span>
245
+ </div>
246
+ <span class="gleg"><b>${G.ach}</b> achieved · <b>${G.prog}</b> in progress · <b>${G.plan}</b> not started <span style="color:var(--ink-faint)">of ${G.total}</span></span>
247
+ </div>` : '';
248
+ return `<div class="rm-hero">
249
+ <div class="rm-hero__top">
250
+ <div>
251
+ <div class="rm-big"><span class="n">${heroPct}</span><span class="u">%</span></div>
252
+ <div class="rm-lab">of all in-flight tasks shipped</div>
253
+ </div>
254
+ <div>
255
+ <div class="rm-lbar rm-hbar" role="img" aria-label="${A.sh} shipped, ${A.co} confirmed, ${A.cm} completed, ${heroRem} remaining of ${A.total}">
256
+ <span class="rm-lbar__s rm-lbar__s--sh" style="width:${pctOf(A.sh, A.total)}%"></span>
257
+ <span class="rm-lbar__s rm-lbar__s--co" style="width:${pctOf(A.co, A.total)}%"></span>
258
+ <span class="rm-lbar__s rm-lbar__s--cm" style="width:${pctOf(A.cm, A.total)}%"></span>
197
259
  </div>
198
- </details>`;
199
- }
200
-
201
- root.innerHTML = liveHtml + doneHtml;
260
+ <div class="rm-stats">
261
+ <div class="rm-stat"><span class="v">${flight.length}</span><span class="k">versions in flight</span></div>
262
+ <div class="rm-stat"><span class="v sh">${fmtNum(A.sh)}</span><span class="k">shipped (live)</span></div>
263
+ <div class="rm-stat"><span class="v co">${fmtNum(A.co)}</span><span class="k">confirmed</span></div>
264
+ <div class="rm-stat"><span class="v cm">${fmtNum(A.cm)}</span><span class="k">completed</span></div>
265
+ <div class="rm-stat"><span class="v">${fmtNum(heroRem)}</span><span class="k">tasks remaining</span></div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ ${goalsRow}
270
+ </div>`;
202
271
  }
203
272
 
204
- // Fetch a version's goals, each enriched with its members + criteria (so the
205
- // page can show goals under the version, expand to criteria, and offer join/leave).
206
- // GET /goals?version lists them; GET /goals/:id carries members + criteria. Returns
207
- // [] on any failure so a hiccup never blanks the bars.
208
- async function fetchGoals(versionId) {
273
+ // ---- data ------------------------------------------------------------------
274
+
275
+ // Enrich a version with its goal summary. `list` gives titles + status +
276
+ // sort_order (1 request); `/goals/:id` gives per-goal criteria (needed to split
277
+ // in-progress from not-started + to fill the "active now" list).
278
+ async function goalSummary(versionId) {
209
279
  try {
210
280
  const list = await getJSON(`${API}/goals?version=${encodeURIComponent(versionId)}`);
211
281
  const goals = (list.goals || []).filter((g) => g.status !== 'archived');
212
- const details = await Promise.all(goals.map((g) =>
213
- getJSON(`${API}/goals/${encodeURIComponent(g.id)}`).catch(() => null)));
214
- return details.filter(Boolean);
282
+ if (!goals.length) return { total: 0, ach: 0, prog: 0, plan: 0, active: [], all: [] };
283
+ const details = await Promise.all(goals.map((g) => getJSON(`${API}/goals/${encodeURIComponent(g.id)}`).catch(() => null)));
284
+ const enriched = goals.map((g, i) => {
285
+ const d = details[i];
286
+ const crit = d && Array.isArray(d.criteria) ? d.criteria : [];
287
+ const sat = crit.filter((c) => c.satisfied).length;
288
+ return { id: g.id, title: g.title, status: g.status, sort: Number(g.sort_order) || 0, sat, total: crit.length };
289
+ }).sort((a, b) => a.sort - b.sort);
290
+ const ach = enriched.filter((g) => g.status === 'achieved').length;
291
+ const active = enriched.filter((g) => g.status !== 'achieved' && g.sat > 0);
292
+ const plan = enriched.filter((g) => g.status !== 'achieved' && g.sat === 0).length;
293
+ return { total: enriched.length, ach, prog: active.length, plan, active, all: enriched };
215
294
  } catch (_) {
216
- return [];
295
+ return { total: 0, ach: 0, prog: 0, plan: 0, active: [], all: [] };
217
296
  }
218
297
  }
219
298
 
220
- // Load (or reload) the version progress bars + their goals. Extracted so a
221
- // join/leave can re-render the section. Public progress read; goals per version.
222
- async function loadVersionBars() {
223
- try {
224
- const prog = await getJSON(`${API}/public/progress`);
225
- const versions = prog.progress || [];
226
- const goalsByVersion = new Map();
227
- await Promise.all(versions.map(async (v) => {
228
- goalsByVersion.set(v.version_id, await fetchGoals(v.version_id));
229
- }));
230
- renderVersionBars(versions, goalsByVersion);
231
- } catch (err) {
232
- const root = $('#roadmap-versions');
233
- if (root) root.innerHTML = `<div class="profile__placeholder">Version progress could not be loaded right now.</div>`;
234
- }
299
+ // Merge /public/progress (counts, %, lifecycle) with /public/versions (track,
300
+ // dates) into one version model the renderers consume.
301
+ function mergeVersions(progress, versions) {
302
+ const meta = new Map();
303
+ (versions || []).forEach((v) => meta.set(v.id, v));
304
+ return (progress || []).map((p) => {
305
+ const m = meta.get(p.version_id) || {};
306
+ return {
307
+ id: p.version_id,
308
+ name: p.name || m.name || p.version_id,
309
+ track: m.track || 'internal',
310
+ status: p.status || m.status || 'building',
311
+ desc: (p.done_when || m.done_when || '').trim(),
312
+ shipped: Number(p.shipped_count) || 0,
313
+ total: Number(p.total_count) || 0,
314
+ pct: Number(p.percent_complete) || 0,
315
+ criteria: Number(p.criteria_count) || 0,
316
+ life: {
317
+ sh: Number(p.lifecycle_shipped_count) || 0,
318
+ co: Number(p.lifecycle_confirmed_count) || 0,
319
+ cm: Number(p.lifecycle_completed_count) || 0,
320
+ },
321
+ start: m.started_at || null,
322
+ ship: m.shipped_at || null,
323
+ };
324
+ });
235
325
  }
236
326
 
237
- // Join or leave a goal, then re-render the version bars so the goal moves
238
- // between "joined" (Leave) and joinable (Join). Errors surface inline.
239
- async function doGoalMembership(goalId, act, btn) {
240
- if (btn.disabled) return;
241
- btn.disabled = true;
242
- const goalEl = btn.closest('.vgoal');
243
- const errEl = goalEl ? goalEl.querySelector('.vgoal__err') : null;
244
- if (errEl) { errEl.hidden = true; errEl.textContent = ''; }
245
- try {
246
- await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/${act}`);
247
- toast(act === 'join' ? 'Joined the goal.' : 'Left the goal.');
248
- // Re-render just this goal's row in place — join/leave changes only this goal's
249
- // membership, so repainting (and collapsing) the whole version section would be
250
- // both wasteful and jarring. Re-fetch the one goal for its fresh member list; the
251
- // delegated #roadmap-versions handler catches clicks on the replacement button.
252
- const detail = await getJSON(`${API}/goals/${encodeURIComponent(goalId)}`).catch(() => null);
253
- if (detail && goalEl) {
254
- // Join/leave never changes order — carry the row's current priority
255
- // number over rather than re-deriving it (a single row has no view of
256
- // its siblings to recompute a rank from).
257
- const priority = goalEl.querySelector('.vgoal__priority')?.textContent || '';
258
- goalEl.outerHTML = goalItemHtml(detail, priority);
259
- } else if (goalEl) {
260
- // Couldn't refetch — flip the button optimistically so the UI isn't stuck.
261
- btn.outerHTML = act === 'join'
262
- ? `<button type="button" class="vgoal-action vgoal-leave" data-act="leave" data-id="${escapeHtml(String(goalId))}">Leave</button>`
263
- : `<button type="button" class="vgoal-action vgoal-join" data-act="join" data-id="${escapeHtml(String(goalId))}">Join</button>`;
264
- }
265
- } catch (err) {
266
- const code = err && err.data ? err.data.error : null;
267
- const msg = code === 'archon_approval_required' ? 'This goal needs an Archon\'s approval to join.'
268
- : code === 'goal_not_open' ? 'This goal is no longer open to join.'
269
- : code === 'not_a_member' ? 'You are not a member of this goal.'
270
- : (err && err.data && err.data.message) || `Could not ${act} the goal.`;
271
- if (errEl) { errEl.textContent = msg; errEl.hidden = false; }
272
- btn.disabled = false;
273
- }
274
- }
327
+ async function render() {
328
+ const root = $('#rm-root');
329
+ const [progress, versions] = await Promise.all([
330
+ getJSON(`${API}/public/progress`).then((d) => d.progress || []),
331
+ getJSON(`${API}/public/versions`).then((d) => d.versions || []).catch(() => []),
332
+ ]);
333
+ const all = mergeVersions(progress, versions);
275
334
 
276
- // ---- drag-to-reorder (task 1760, Archon-only) ------------------------------
277
- // Native HTML5 drag-and-drop, scoped to one version's .vgoal-list (a goal
278
- // never crosses versions here — dragging never removes it from its list, it
279
- // only reorders siblings). On drop, persist the WHOLE list's new order via
280
- // POST /goals/reorder; the server is the real gate (route-rank archon) — this
281
- // is only reached by viewer.isArchon builders, matching the affordance.
282
- let dragEl = null;
335
+ // Goal summaries only for versions we actually show goals on (everything but
336
+ // shipped) — a real perf win over fetching goals for the whole history.
337
+ const needGoals = all.filter((v) => v.status !== 'shipped');
338
+ await Promise.all(needGoals.map(async (v) => { v.goals = await goalSummary(v.id); }));
283
339
 
284
- function wireDragReorder() {
285
- const root = $('#roadmap-versions');
286
- if (!root) return;
287
- root.addEventListener('dragstart', (e) => {
288
- const el = e.target.closest('.vgoal');
289
- if (!el || !viewer.isArchon) return;
290
- dragEl = el;
291
- el.classList.add('vgoal--dragging');
292
- e.dataTransfer.effectAllowed = 'move';
293
- });
294
- root.addEventListener('dragover', (e) => {
295
- if (!dragEl) return;
296
- const over = e.target.closest('.vgoal');
297
- const list = dragEl.closest('.vgoal-list');
298
- if (!over || !list || over === dragEl || !list.contains(over)) return;
299
- e.preventDefault();
300
- const rect = over.getBoundingClientRect();
301
- const after = (e.clientY - rect.top) > rect.height / 2;
302
- list.insertBefore(dragEl, after ? over.nextSibling : over);
303
- });
304
- root.addEventListener('drop', (e) => {
305
- if (dragEl) e.preventDefault();
306
- });
307
- root.addEventListener('dragend', async () => {
308
- if (!dragEl) return;
309
- const el = dragEl;
310
- const list = el.closest('.vgoal-list');
311
- el.classList.remove('vgoal--dragging');
312
- dragEl = null;
313
- if (!list) return;
314
- const versionId = el.dataset.versionId;
315
- const rows = [...list.querySelectorAll('.vgoal')];
316
- rows.forEach((row, i) => {
317
- const num = row.querySelector('.vgoal__priority');
318
- if (num) num.textContent = String(i + 1);
319
- });
320
- const orderedIds = rows.map((row) => Number(row.dataset.goalId));
321
- try {
322
- await postJSON(`${API}/goals/reorder`, { version_id: versionId, ordered_ids: orderedIds });
323
- toast('Priority order saved.');
324
- } catch (err) {
325
- toast((err && err.data && err.data.message) || 'Could not save the new order — reloading.');
326
- await loadVersionBars();
327
- }
328
- });
329
- }
340
+ const flight = all.filter((v) => v.status === 'building').sort((a, b) => b.total - a.total);
341
+ const staged = all.filter((v) => v.status === 'planning').sort((a, b) => b.total - a.total);
342
+ const parked = all.filter((v) => v.status === 'frozen').sort((a, b) => b.pct - a.pct);
343
+ const shipped = all.filter((v) => v.status === 'shipped')
344
+ .sort((a, b) => (new Date(b.ship || 0)) - (new Date(a.ship || 0)));
330
345
 
331
- // ---- wiring + bootstrap ---------------------------------------------------
346
+ let html = '';
332
347
 
333
- function wireEvents() {
334
- // A criteria toggle (custom disclosure) and inline join/leave. Plain
335
- // buttons — no <summary> nesting.
336
- $('#roadmap-versions')?.addEventListener('click', (e) => {
337
- const toggle = e.target.closest('.vgoal__toggle');
338
- if (toggle) {
339
- const goalEl = toggle.closest('.vgoal');
340
- const body = goalEl ? goalEl.querySelector('.vgoal__body') : null;
341
- const nowOpen = toggle.getAttribute('aria-expanded') !== 'true';
342
- toggle.setAttribute('aria-expanded', String(nowOpen));
343
- toggle.querySelector('.vgoal__caret')?.classList.toggle('vgoal__caret--open', nowOpen);
344
- if (body) body.hidden = !nowOpen;
345
- return;
346
- }
347
- const act = e.target.closest('.vgoal-action');
348
- if (act) doGoalMembership(act.dataset.id, act.dataset.act, act);
349
- });
350
- wireDragReorder();
348
+ if (flight.length) {
349
+ html += heroHtml(flight);
350
+ const feat = flight[0]; // dominant in-flight version (most tasks)
351
+ const rest = flight.slice(1);
352
+ const inner = featCard(feat) + (rest.length ? `<div class="rm-grid" style="margin-top:16px">${rest.map(flightCard).join('')}</div>` : '');
353
+ html += sectionHtml('flight', 'In flight', flight.length, '', inner);
354
+ } else {
355
+ html += `<div class="rm-hero"><div class="rm-lab" style="max-width:none">No version is in flight right now.</div></div>`;
356
+ }
357
+
358
+ if (staged.length) {
359
+ html += sectionHtml('staged', 'Staged', staged.length, 'Seeded with goals, not yet started.', `<div class="rm-grid">${staged.map(stagedCard).join('')}</div>`);
360
+ }
361
+ if (parked.length) {
362
+ html += sectionHtml('parked', 'Parked', parked.length, '', `<div class="rm-grid">${parked.map(parkedCard).join('')}</div>`);
363
+ }
364
+ if (shipped.length) {
365
+ const pills = shipped.map((v) => `<a class="rm-shippill" href="${GOALS_URL}" title="${escapeHtml(v.name)}"><span class="ok">✓</span><span class="nm">${escapeHtml(v.id)}</span><span class="mt">${fmtNum(v.shipped)} tasks · ${ym(v.ship)}</span></a>`).join('');
366
+ html += sectionHtml('shipped', 'Shipped', shipped.length, '', `<div class="rm-shipstrip">${pills}</div>`);
367
+ }
368
+
369
+ if (!all.length) html = `<div class="profile__placeholder">No versions declared yet.</div>`;
370
+ root.innerHTML = html;
351
371
  }
352
372
 
353
373
  async function boot() {
354
- // /me settles the viewer id + rank (so a joined goal shows "Leave" not
355
- // "Join", and only an Archon sees the drag handle). Best-effort: a stale/
356
- // expired session still renders the public progress.
357
- try {
358
- const me = await getJSON(`${API}/me`).catch(() => null);
359
- viewer.id = me?.builder?.id ?? null;
360
- viewer.isArchon = (me?.builder?.rank || '').toLowerCase() === 'archon';
361
- } catch (_) { /* cosmetic only */ }
362
-
363
374
  const sub = $('#roadmap-sub');
364
- if (sub) sub.textContent = 'Every declared version, its progress, and the goals building toward it.';
365
-
366
- await loadVersionBars();
375
+ if (sub) sub.textContent = 'How the whole build is doing — the program at a glance, then each version. Shipped work stays out of the way.';
376
+ try {
377
+ await render();
378
+ } catch (err) {
379
+ if (err && err.message === 'redirecting to auth') return;
380
+ console.error('[roadmap] load failed:', err);
381
+ const root = $('#rm-root');
382
+ if (root) root.innerHTML = `<div class="profile__placeholder">The roadmap could not be loaded right now. Reload to try again.</div>`;
383
+ toast('The roadmap failed to load. Reload to try again.');
384
+ }
367
385
  }
368
386
 
369
- wireEvents();
370
- boot().catch((err) => {
371
- if (err && err.message === 'redirecting to auth') return;
372
- console.error('[roadmap] load failed:', err);
373
- toast('The roadmap failed to load. Reload to try again.');
374
- });
387
+ boot();
375
388
  })();