@bongos/core 1.18.3 → 1.18.19

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 (471) hide show
  1. package/.bongos-core.json +1576 -286
  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/migrate.sh +99 -14
  347. package/scripts/status-mirror-sync.js +114 -84
  348. package/src/bongos/api-errors.js +1 -0
  349. package/src/bongos/auth.js +447 -102
  350. package/src/bongos/db-kernel.js +27 -0
  351. package/src/bongos/db.js +39 -0
  352. package/src/bongos/module-scope-map.js +27 -1
  353. package/src/bongos/permission-path-check.js +136 -102
  354. package/src/bongos/repo-info.js +47 -11
  355. package/src/bongos/route-rank-check.js +429 -46
  356. package/src/bongos/routes/_helpers.js +12 -1
  357. package/src/bongos/routes/auth.js +219 -2
  358. package/src/bongos/routes/backup.js +58 -2
  359. package/src/bongos/routes/me.js +35 -1
  360. package/src/bongos/serve-internal.js +84 -15
  361. package/src/branding.js +32 -0
  362. package/src/instance-config.js +30 -0
  363. package/src/module-api.js +124 -1
  364. package/src/platform-server.js +86 -9
  365. package/tests/abandon_task.mjs +59 -3
  366. package/tests/api_cli_flags.mjs +53 -0
  367. package/tests/auth_page_gate.mjs +13 -0
  368. package/tests/backfill_sessions.mjs +127 -1
  369. package/tests/backup_cli.mjs +54 -0
  370. package/tests/bongos_box_connect.mjs +77 -0
  371. package/tests/box_access.mjs +130 -1
  372. package/tests/box_credential.mjs +125 -0
  373. package/tests/box_host_keys.mjs +24 -0
  374. package/tests/box_infra_resolve.mjs +149 -0
  375. package/tests/box_scope_session.mjs +6 -2
  376. package/tests/box_sync.mjs +55 -0
  377. package/tests/box_task_scope.mjs +99 -0
  378. package/tests/branding.mjs +12 -0
  379. package/tests/cf_ruleset_preflight.mjs +142 -0
  380. package/tests/cf_tunnel.mjs +37 -0
  381. package/tests/claim_gate_rebase.mjs +98 -0
  382. package/tests/clawback_confirmed_backlog.mjs +103 -0
  383. package/tests/cli_lib_formatters.mjs +20 -0
  384. package/tests/confirm_attribution.mjs +30 -46
  385. package/tests/credit_grant.mjs +198 -10
  386. package/tests/criterion_authoring_gate.mjs +272 -0
  387. package/tests/criterion_progress.mjs +33 -1
  388. package/tests/dev_lib.mjs +253 -0
  389. package/tests/devbox_artifact_gate.mjs +75 -0
  390. package/tests/devbox_dmg_backdrop.mjs +137 -0
  391. package/tests/devbox_icons.mjs +304 -0
  392. package/tests/devbox_installer_brand.mjs +232 -0
  393. package/tests/devbox_key_register.mjs +234 -0
  394. package/tests/docs_openapi_route.mjs +110 -0
  395. package/tests/doctor_hooks.mjs +141 -0
  396. package/tests/extract_json.mjs +140 -0
  397. package/tests/fitness.mjs +89 -0
  398. package/tests/gen_freshness.mjs +96 -0
  399. package/tests/goal_advisory.mjs +251 -0
  400. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  401. package/tests/goal_routes.mjs +59 -4
  402. package/tests/goal_tier.mjs +12 -3
  403. package/tests/governance_catalog.mjs +129 -0
  404. package/tests/governance_claim_eligibility.mjs +262 -0
  405. package/tests/governance_config.mjs +187 -0
  406. package/tests/governance_ownership_scope.mjs +410 -0
  407. package/tests/governance_page_gates.mjs +324 -0
  408. package/tests/governance_parity.mjs +336 -0
  409. package/tests/governance_principals.mjs +232 -0
  410. package/tests/governance_protected_surfaces.mjs +208 -0
  411. package/tests/governance_require_permission.mjs +223 -0
  412. package/tests/governance_resolver.mjs +84 -0
  413. package/tests/governance_role_sync.mjs +231 -0
  414. package/tests/governance_seed.mjs +127 -0
  415. package/tests/governance_session_scope.mjs +434 -0
  416. package/tests/governance_vanilla_boot.mjs +203 -0
  417. package/tests/grade_attribution.mjs +7 -7
  418. package/tests/hall_audit.mjs +223 -0
  419. package/tests/hall_nav.mjs +191 -0
  420. package/tests/hall_palette.mjs +206 -0
  421. package/tests/hall_signout.mjs +124 -0
  422. package/tests/hook_user_agents.mjs +10 -0
  423. package/tests/idea_triage.mjs +162 -0
  424. package/tests/idp_device_exchange.mjs +93 -0
  425. package/tests/init.mjs +51 -0
  426. package/tests/instance_config.mjs +65 -0
  427. package/tests/instance_mismatch_guard.mjs +23 -6
  428. package/tests/instance_release_config.mjs +104 -0
  429. package/tests/linkify_refs.mjs +15 -2
  430. package/tests/migrate_root_resolution.mjs +124 -1
  431. package/tests/module_contributions.mjs +55 -0
  432. package/tests/module_loader.mjs +10 -5
  433. package/tests/module_scope_active_claims.mjs +94 -0
  434. package/tests/needs_rebase.mjs +92 -0
  435. package/tests/one_active_claim_per_session.mjs +76 -0
  436. package/tests/override_attribution.mjs +10 -8
  437. package/tests/override_request_ownership.mjs +8 -4
  438. package/tests/parse_cookie_decode.mjs +42 -0
  439. package/tests/pay_on_land.mjs +129 -0
  440. package/tests/platform_boot.mjs +100 -1
  441. package/tests/post_login_landing.mjs +42 -0
  442. package/tests/preflight_checks.mjs +72 -0
  443. package/tests/provision.mjs +132 -7
  444. package/tests/publish_branch_route.mjs +53 -0
  445. package/tests/publish_reconciler.mjs +187 -2
  446. package/tests/repo_info_binding.mjs +248 -0
  447. package/tests/reward_gate.mjs +11 -2
  448. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  449. package/tests/reward_mode_surfacing.mjs +168 -0
  450. package/tests/route_permission_check.mjs +301 -0
  451. package/tests/route_rank_check.mjs +254 -12
  452. package/tests/route_validation.mjs +30 -0
  453. package/tests/sequence.mjs +267 -0
  454. package/tests/session_digest.mjs +44 -0
  455. package/tests/session_index.mjs +18 -6
  456. package/tests/session_records.mjs +39 -1
  457. package/tests/session_reward_on_land.mjs +381 -0
  458. package/tests/session_reward_reconcile.mjs +229 -0
  459. package/tests/ship_api_client_pathspec.mjs +100 -0
  460. package/tests/ship_base_flag.mjs +93 -0
  461. package/tests/ship_ci_deploy.mjs +80 -3
  462. package/tests/ship_upstream_deletion.mjs +57 -0
  463. package/tests/smoke_target.mjs +233 -0
  464. package/tests/start_env_guard.mjs +87 -0
  465. package/tests/start_rebase_warning.mjs +103 -0
  466. package/tests/status_mirror_sync.mjs +122 -0
  467. package/tests/stop_refcheck.mjs +19 -1
  468. package/tests/task_goal_link.mjs +15 -6
  469. package/tests/upgrade.mjs +139 -0
  470. package/tests/web_admission_status.mjs +162 -0
  471. package/tests/write_validation_gaps.mjs +110 -0
@@ -65,6 +65,35 @@ function defaultApiBase(b = safeBrand()) {
65
65
  return (typeof origin === 'string' && origin.trim()) || 'http://localhost:3000';
66
66
  }
67
67
 
68
+ // The API base a CLI command should actually talk to:
69
+ // <PREFIX>_API_BASE (env) → the PERSISTED session's api_base → defaultApiBase()
70
+ // The session beats the branding default because a builder authenticated against
71
+ // instance A must keep hitting A even when this checkout's own branding names B —
72
+ // which is exactly what broke re-auth (task 1002427). cliClient() had this order
73
+ // right; paste-token.js hardcoded the founder host and reauth.js used the bare
74
+ // branding default, so a token minted by one instance was verified against
75
+ // another and rejected 401. Every base resolution routes through here so the
76
+ // three cannot drift again.
77
+ //
78
+ // `session` is injected by callers that already loaded it (cliClient) and read
79
+ // from the session file otherwise; pass null to mean "no session" without a disk
80
+ // read. Never returns empty — an unusable api_base falls back to the default.
81
+ function resolveApiBase({ session, env = process.env, b = safeBrand() } = {}) {
82
+ const fromEnv = resolveEnv('API_BASE', { env, prefix: envPrefix(b) });
83
+ if (fromEnv) return fromEnv;
84
+ let s = session;
85
+ if (s === undefined) {
86
+ try {
87
+ const raw = readConfigFileSync('gds-session.json', { b });
88
+ s = raw ? JSON.parse(raw) : null;
89
+ } catch (_) {
90
+ s = null; // unreadable/corrupt session must never break base resolution
91
+ }
92
+ }
93
+ const fromSession = s && typeof s.api_base === 'string' && s.api_base.trim();
94
+ return fromSession || defaultApiBase(b);
95
+ }
96
+
68
97
  // Absolute path to the instance config dir — the WRITE target and first read.
69
98
  function configHome(b = safeBrand()) {
70
99
  return path.join(os.homedir(), '.config', configDirName(b));
@@ -202,6 +231,7 @@ module.exports = {
202
231
  configDirName,
203
232
  envPrefix,
204
233
  defaultApiBase,
234
+ resolveApiBase,
205
235
  configHome,
206
236
  configReadDirs,
207
237
  configPath,
package/src/module-api.js CHANGED
@@ -237,8 +237,131 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
237
237
  // was --skip-pin-verify, which the unattended auto-upgrade subscription cannot pass).
238
238
  // package-core now drops npm-stripped files from the manifest + pin so it describes exactly
239
239
  // what npm installs. Build-tool fix, no doorway export change → semver PATCH.
240
+ // 1.18.4 — ship the platform-identity hub module in the core package (task 1002235, goal 1000041).
241
+ // The module (ADR 0141: /sso/*, projects catalog, my-projects, scouting + its migrations) was
242
+ // built but never added to publish-manifest PUBLISH_ALLOWLIST, so it did not ship in
243
+ // @bongos/core — the hub's enabledModules() threw "unknown module platform-identity" and
244
+ // provision.js's federation load MODULE_NOT_FOUNDed. Reclassified IN (sibling of provisioning);
245
+ // default:false → shipped ≠ enabled, dormant on every instance until the hub turns it on. PATCH.
246
+ // 1.18.5 — configurable post-login landing (task 1002240, goal 1000041). A successful web sign-in
247
+ // that carried no explicit ?return= / same-apex Referer now lands on branding.landing.postLogin
248
+ // (auth.js postLoginLanding) instead of a hardcoded /builders. Portable default stays
249
+ // '/builders' (the hall always exists); env-overridable per instance via
250
+ // <PREFIX>_POST_LOGIN_LANDING. cloudbongos.com sets it to /gate/ so a visitor who signs in
251
+ // from the orbs returns to the orbs, never auto-redirected into the builders' hall. The value
252
+ // is validated through the open-redirect allowlist. Additive config read, no doorway change → PATCH.
253
+ // 1.18.6 — post-login ?return= fix (task 1002241). parseCookie() now URL-DECODES the cookie
254
+ // value, symmetric with Express res.cookie's percent-ENCODING. Before this, a return
255
+ // target like "/create" was stored as "%2Fcreate"; safeReturnTo() rejected the encoded
256
+ // form and every explicit ?return= fell through to the post-login landing (on
257
+ // cloudbongos.com, /gate/) — most visibly the onboarding "Connect GitHub" button, which
258
+ // bounced to the orbs instead of back to the wizard. Also un-breaks the #726 subdomain
259
+ // return. A bug fix in a shared route helper, no doorway change → PATCH.
260
+ // 1.18.7 — federated single-logout (task 1002238, goal 1000041, ADR 0144). Signing out of the hub now
261
+ // signs you out of every federated project too. Core gains: a `builder.logout` seam event
262
+ // (emitted by POST /auth/logout), a POST /auth/backchannel-logout endpoint (dormant unless the
263
+ // instance federates — idp set), and auth.js verifyHubLogoutToken. The hub (platform-identity)
264
+ // listens for builder.logout and fans a hub-signed EdDSA logout token out to each federated
265
+ // instance the builder entered, which verifies it (aud-scoped, purpose-tagged, ~120s TTL) and
266
+ // revokes that builder's local sessions. Identity/trust boundary intact (the token carries only
267
+ // a subject + purpose, never a rank). Additive event + route, no doorway change → PATCH.
268
+ // 1.18.8 — hall sign-out works on every page (task 1002245). The real sign-out (POST /auth/logout:
269
+ // revoke session + clear cookies + emit the `builder.logout` federated single-logout) was wired
270
+ // ONLY in the index script (builders.js); shell.js draws the sign-out control on EVERY hall page
271
+ // as a bare GET anchor to the POST-only endpoint, so on non-index pages the click 404'd — the
272
+ // session was never revoked and the hub never notified (the 1.18.7 SLO was bypassed). Centralized
273
+ // in shell.js (doLogout + a delegated click listener), with tests/hall_signout.mjs proving it.
274
+ // Also carries minor /create wizard polish (tasks 1002246/1002247). Client + shared-surface fix,
275
+ // no doorway change → PATCH.
276
+ // 1.18.9 — /create onboarding: the wizard's own "Sign in with GitHub" now requests repo scope up front
277
+ // (read:user + public_repo via &repos=1), so a visitor who signs in THROUGH the create flow grants
278
+ // everything in one GitHub screen and never hits the separate "Connect GitHub" step at Home —
279
+ // ensureRepoAccess()'s probe finds the stored token and shows the picker/creator directly. Also
280
+ // reworks the residual need-connect copy + button ("Allow repo access") so a user already signed
281
+ // in with identity-only scope isn't asked to "connect" as if signing in twice. Hall + CLI sign-in
282
+ // stay identity-only (ADR 0143 least-privilege default). Client-only (public-landing/create.html),
283
+ // no doorway change → PATCH. (task 1002259)
284
+ // 1.18.10 — reward-mode go-live: the configurable per-instance branding.reward.mode knob (economy;
285
+ // ADR 0146) lands together with the goal-1000037 pay-on-land migrations. Cloud Bongos selects
286
+ // "cost-plus-only" (instance config), so a shipped task pays only the cost-plus session reward,
287
+ // not the per-task author estimate; the estimate still rides the row for display + the no-zero
288
+ // claim gate. This deploy applies core_193 (tasks.needs_rebase columns) + core_194 (the one-time
289
+ // confirmed-backlog clawback). Default stays "cost-plus-and-estimate" → every other instance is
290
+ // unchanged; economy-only, no doorway change → PATCH. (task 1002270 + goal 1000037 go-live 1002273)
291
+ // 1.18.12 — Docs-surface correctness (task 1002374). User-facing "GDS"/"Game Development System" →
292
+ // "Bongos" (primer, OpenAPI spec title/description + reference header, the two CLI session
293
+ // messages the primer quotes). 04-architecture diagram REDRAWN for the standalone model —
294
+ // own repo + auto-pull deploy (ADR 0129) + vendored @bongos/core (ADR 0108), product-neutral;
295
+ // it had hardcoded the reference game monorepo (amazonprimea.com / GitHub-Actions deploy-prod.yml
296
+ // / "the live game"). Docs/prose + diagram generator only, no schema/doorway change → PATCH.
297
+ // 1.18.11 — Standalone instances get live-updating docs (ADR 0147, task 1002314). gen-session-index.js
298
+ // now resolves the INSTANCE root (was hardcoded to the core, so a standalone box indexed
299
+ // node_modules/@bongos/core instead of its own sessions); adds upgrade.regenerateNavDocs() +
300
+ // a regen-instance-docs.js CLI wired into init/upgrade/provision-deploy; INSTANCE_GITIGNORE
301
+ // ignores the whole-file nav docs. Docs-pipeline only, no schema/doorway change → PATCH.
302
+ // (go-live 1002318)
303
+ // 1.18.12 — docs-surface correctness: GDS→Bongos rename + standalone architecture redraw
304
+ // (task 1002374). Docs/prose + diagram generator only, no schema/doorway change → PATCH.
305
+ // 1.18.13 — `bongos dev`: one-command LOCAL launcher for a standalone instance (task 1002397,
306
+ // ADR 0149). New CLI verb + scripts/gds/dev.js + dev-lib.js so a freshly-created,
307
+ // localhost-configured project stands up locally in one step. No schema/doorway change → PATCH.
308
+ // 1.18.14 — module pollers actually start on a standalone instance (task 1002419). The loader's
309
+ // startModulePollers had ZERO callers once the game entrypoint server.js was retired, so
310
+ // EVERY module poller was dead — most visibly lifecycle/publish-reconciler, which is what
311
+ // drives confirmed→shipped, leaving merged tasks stranded at 'confirmed' indefinitely
312
+ // (12 rows, oldest 6.2 days). src/platform-server.js now starts them at boot behind the
313
+ // same isEnabled gate mountModuleRoutes uses, and stops them on SIGTERM/SIGINT. Wiring
314
+ // only — no schema/doorway change → PATCH. Any instance on ≤1.18.13 lands nothing until
315
+ // it takes this bump.
316
+ // 1.18.15 — re-release of the 1.18.14 module-poller fix (task 1002419) under a fresh number.
317
+ // A SEPARATE release had already published 1.18.14 for the task-scoped dev-box go-live
318
+ // (goal 1000051), so the poller-fix 1.18.14 could never reach an instance already pinned to
319
+ // the dev-box 1.18.14 (same version = npm no-op; the pull-deploy loop skips npm ci when the
320
+ // lockfile is unchanged). This bump carries the identical src/platform-server.js poller
321
+ // wiring forward on a number no instance has taken yet, so the pin can actually move. No
322
+ // code delta vs the 1.18.14 already on main — version constant only → PATCH. (task 1002421)
323
+ // 1.18.16 — go-live carrier for the hall version-UI work already merged to main since 1.18.15:
324
+ // the Roadmap "Command deck" redesign (task 1002426) and the sidebar reorg — nav order
325
+ // Roadmap→Goals→Tasks, the "Work" board relabelled "Tasks", goal-detail task parity
326
+ // (task 1002425, PR #17). Both landed on main with NO version bump, so no vendored-tarball
327
+ // instance could take them (the pull-deploy pin only moves on a fresh number). No new code
328
+ // delta beyond this constant → PATCH. (go-live task 1002431)
329
+ // 1.18.17 — Goals page reorganised into a task-readiness rollup (grouped
330
+ // Active/Blocked/Achieved; each goal rolls its tasks into ready/in-progress/
331
+ // blocked + a next claimable task) and the task-list API gained an opt-in
332
+ // `?include=deps` that enriches each task with blocked_by[]/blocks so the
333
+ // hall can paint readiness without an N+1 (db.attachTaskDepSummaries). New
334
+ // export is additive; the enrichment is opt-in → PATCH. (task 1002439)
335
+ // 1.18.18 — go-live carrier for 171 commits that landed on main between 2026-07-24
336
+ // and 2026-08-03 with no bump behind them, so no vendored-tarball instance
337
+ // could take any of them. Headline: the governance permission system
338
+ // (BV1.R95–R109 — resolver + kernel port, first-class principals, session
339
+ // scopes, requirePermission(), ownership-scoped grants, config/governance.json)
340
+ // and the route-rank/validation hardening that followed it; the ship.js
341
+ // Windows project-path fix so Windows sessions actually get paid (89f291c);
342
+ // the sequential chain runner (task 1002484); Metic task abandon/un-abandon
343
+ // (ADR 0152); goal-authored criteria + the orphan-task goal_advisory (ADR
344
+ // 0154); the nightly-backup 203/EXEC fix (task 1002479); and a broad hall
345
+ // pass (jump palette, sidebar regroup, browser-side idea triage, mobile
346
+ // overflow). This bump also carries the /version-vs-CORE_VERSION divergence
347
+ // check added under task 1002521 — the gap above went unnoticed for ten days
348
+ // precisely because nothing compared live against main. The doorway surface
349
+ // is byte-identical to 1.18.17 (governance is reached through the kernel
350
+ // port, not a new export), so despite the size this is → PATCH. (task 1002521)
351
+ // 1.18.19 — SINGLE-FIX patch cut from the core-v1.18.18 tag, not from main: core
352
+ // migrations were silently skipped on any checkout whose absolute path
353
+ // contains a space. `for f in $MIGRATIONS` word-split the newline-separated
354
+ // list on IFS, fragmenting every path; the `[ -e "$f" ]` guard then dropped
355
+ // all of them and schema_migrations stayed empty. The module loop is quoted,
356
+ // so module migrations ran against an empty schema and the operator saw a
357
+ // misleading `relation "achievements" does not exist` from modules/economy
358
+ // instead of the real cause. Now read with a newline-safe herestring, and a
359
+ // no-op guard makes the skip FATAL rather than silent. No doorway change, no
360
+ // schema change, one script + its test → PATCH. Deliberately does NOT carry
361
+ // the unreleased 1.19 tree on main, so a blocked instance can take the fix
362
+ // alone. (task 1002611)
240
363
  // ---------------------------------------------------------------------------
241
- const CORE_VERSION = '1.18.3'; // package-core pin honesty for vendored upgrades (task 1002229 / idea 1000543); see changelog above
364
+ const CORE_VERSION = '1.18.19'; // single-fix patch off the 1.18.18 tag: migrations skipped on a path with a space (task 1002611); see changelog above
242
365
 
243
366
  // A namespaced logger so a module's log lines are attributable + consistent.
244
367
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -29,10 +29,17 @@
29
29
  // by ABSENCE (the game middleware is simply never mounted), not by a runtime
30
30
  // check, exactly as preview-server.js enforces "no internal API" by absence.
31
31
  //
32
- // Background services (uptime poller, publish reconciler, Discord bot) are NOT
33
- // started here. They are not game code, but which of them a given instance runs
34
- // is an instance-policy decision that R54's feature-module system (task 1193)
35
- // will own; R51 keeps this entrypoint to the minimal "serve the surfaces" core.
32
+ // BACKGROUND SERVICES — started here, at the bottom of this file (task 1002419).
33
+ // R51 deferred "which background services an instance runs" to the feature-module
34
+ // system. That system shipped: a module DECLARES contributes.pollers in its
35
+ // manifest and the instance's enabled-module set decides what actually runs
36
+ // (ADR 0083 / 0093 §1). So the policy question is already answered — the enabled
37
+ // set IS the policy — and this entrypoint just has to honour it, exactly as it
38
+ // already honours contributes.routes (mountModuleRoutes) and contributes.
39
+ // webSurfaces (moduleWebSurfaces). Until this was wired, startModulePollers had
40
+ // ZERO callers post-split — its only caller was ever the retired game entrypoint
41
+ // server.js — so on a standalone instance EVERY module poller was dead: the
42
+ // confirmed→shipped sweep never fired and shipped tasks stranded at 'confirmed'.
36
43
 
37
44
  const http = require('http');
38
45
  const fs = require('fs');
@@ -40,6 +47,8 @@ const path = require('path');
40
47
  const express = require('express');
41
48
  const { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal } = require('./bongos/serve-internal');
42
49
  const { isApiShapedPath, apiNotFoundBody } = require('./bongos/api-path-404');
50
+ const moduleLoader = require('./module-loader/loader');
51
+ const { isModuleEnabled } = require('./modules');
43
52
 
44
53
  const PORT = Number(process.env.PORT) || 3000;
45
54
  const HOST = process.env.HOST || '127.0.0.1';
@@ -63,6 +72,19 @@ function landingPage(file) {
63
72
  return _landingPages.get(file);
64
73
  }
65
74
  function landingHtml() { return landingPage('index.html'); }
75
+ // The interactive landing gate (public-landing/gate/, ported from amazonprimea
76
+ // in task 1002407). When an instance ships it, it is the apex front door
77
+ // (task 1002408). Unlike the placeholder landing it is a self-contained baked
78
+ // design that reads no window.__BRANDING__, so it is served RAW, not injected.
79
+ // Memoized: absolute path to gate/index.html, or '' when the instance ships no gate.
80
+ let _gateIndexPath; // undefined = unresolved, '' = absent, string = present
81
+ function gateIndexPath() {
82
+ if (_gateIndexPath === undefined) {
83
+ const p = path.join(LANDING_DIR, 'gate', 'index.html');
84
+ _gateIndexPath = fs.existsSync(p) ? p : '';
85
+ }
86
+ return _gateIndexPath;
87
+ }
66
88
  function isApexHost(req) {
67
89
  return !isStatusHost(req) && !isBuildersHost(req);
68
90
  }
@@ -81,16 +103,38 @@ function buildPlatformApp() {
81
103
  // The whole internal surface — shared verbatim with server.js (ADR 0062 R51).
82
104
  mountInternalSurfaces(app);
83
105
 
84
- // Apex landing. On the apex host (not status./builders.), GET / serves the
85
- // branding-injected landing page, and other paths can resolve a landing asset
86
- // (e.g. the mascot image) from public-landing/. Both fall through to the 404
87
- // tail when there is no landing file or no matching asset.
106
+ // Apex landing. On the apex host (not status./builders.):
107
+ // * if this instance ships the interactive landing gate (public-landing/gate/),
108
+ // the gate is the front door — GET / 302-redirects to /gate/ (mirrors the
109
+ // amazonprimea Caddy rule this replaces, task 1002408). The redirect (not a
110
+ // direct serve at /) is required so the browser base URL becomes /gate/ and
111
+ // the gate's relative asset paths (sprites/…, ../world, ../assets) resolve.
112
+ // * otherwise GET / serves the branding-injected minimal placeholder.
113
+ // Both fall through to the 404 tail when neither is present, and to the landing
114
+ // asset handler below for non-/ paths (e.g. the mascot image).
88
115
  app.get('/', (req, res, next) => {
89
116
  if (!isApexHost(req)) return next();
117
+ if (gateIndexPath()) return res.redirect(302, '/gate/');
90
118
  const html = landingHtml();
91
119
  if (!html) return next();
92
120
  res.type('html').send(html);
93
121
  });
122
+ // /gate → /gate/ (so the gate's relative asset paths resolve against the dir),
123
+ // and /gate/ serves the gate's own index RAW — a self-contained baked design,
124
+ // so no window.__BRANDING__ injection. The gate's sub-assets (/gate/*, /world/*,
125
+ // /assets/*) are served by the express.static(LANDING_DIR) handler below with no
126
+ // extra rule, which also closes the amazonprimea Caddy gap where /assets/anim/idle/*
127
+ // (the buddha idle frames) fell through to the app instead of the static server.
128
+ // One handler for both forms — Express's non-strict routing matches /gate and
129
+ // /gate/ interchangeably, so branch on the real req.path: /gate 301→ /gate/
130
+ // (so relative asset paths resolve against the dir), /gate/ serves the index.
131
+ app.get(['/gate', '/gate/'], (req, res, next) => {
132
+ if (!isApexHost(req)) return next();
133
+ const p = gateIndexPath();
134
+ if (!p) return next();
135
+ if (!req.path.endsWith('/')) return res.redirect(301, '/gate/');
136
+ return res.type('html').sendFile(p);
137
+ });
94
138
  // The branded project-creation experience (goal 35, task 2081) — a sibling
95
139
  // landing page at a clean /create URL, branding-injected like the landing.
96
140
  app.get('/create', (req, res, next) => {
@@ -122,6 +166,28 @@ function buildPlatformApp() {
122
166
  return app;
123
167
  }
124
168
 
169
+ // Start each ENABLED module's background pollers through the loader — module-
170
+ // agnostic: it requires modules/<key>/pollers/<name>.js and fires start(). Same
171
+ // `{ isEnabled: isModuleEnabled }` gate mountModuleRoutes uses, so a module that
172
+ // is off contributes no poller, exactly as it contributes no route. Non-blocking
173
+ // + failure-isolated by the loader, and every poller is independently idempotent,
174
+ // unref'd and self-gating (PUBLISH_RECONCILER_DISABLED, ROUTINE_TIMERS_DISABLED,
175
+ // DISCORD_BOT_ENABLED), so a poller problem can never take the platform down.
176
+ //
177
+ // Safe to start unconditionally — no cross-process lease needed — because this
178
+ // entrypoint runs as a SINGLE process (docker-entrypoint.sh `exec`s it; there is
179
+ // no cluster/replica topology). The one poller with a hard single-runner
180
+ // requirement, the Discord gateway, is double-gated off anyway: its module is
181
+ // default:false AND it needs DISCORD_BOT_ENABLED=true. If an instance ever runs
182
+ // more than one web process, this is the line that needs a lease
183
+ // (modules/lifecycle/merge-lock.js has the pattern).
184
+ //
185
+ // Deliberately OUTSIDE buildPlatformApp(): importing this module (tests) must
186
+ // bind no port and start no timers. Exported so a test can drive it directly.
187
+ function startBackgroundServices() {
188
+ return moduleLoader.startModulePollers({}, { isEnabled: isModuleEnabled });
189
+ }
190
+
125
191
  // Start only when run as the entry (`node src/platform-server.js`). Importing the
126
192
  // module (the smoke test) must NOT bind a port.
127
193
  if (require.main === module) {
@@ -129,7 +195,18 @@ if (require.main === module) {
129
195
  const httpServer = http.createServer(app);
130
196
  httpServer.listen(PORT, HOST, () => {
131
197
  console.log(`Cloud Bongos platform (GDS-only, no game; ADR 0062 R51) listening on ${HOST}:${PORT}`);
198
+ startBackgroundServices();
132
199
  });
200
+
201
+ // Graceful shutdown — stop each module poller (e.g. close the Discord gateway
202
+ // socket cleanly on a systemd restart / Ctrl-C so no zombie session is left for
203
+ // Discord to reap). stopModulePollers awaits each stop and isolates errors, so
204
+ // a stuck poller never blocks the exit.
205
+ for (const sig of ['SIGTERM', 'SIGINT']) {
206
+ process.on(sig, () => {
207
+ moduleLoader.stopModulePollers().finally(() => process.exit(0));
208
+ });
209
+ }
133
210
  }
134
211
 
135
- module.exports = { buildPlatformApp };
212
+ module.exports = { buildPlatformApp, startBackgroundServices };
@@ -30,7 +30,7 @@ process.env.NODE_ENV = 'test';
30
30
 
31
31
  const require = createRequire(import.meta.url);
32
32
  const db = require('../modules/lifecycle/db.js');
33
- const { abandonTask } = db;
33
+ const { abandonTask, restoreTask } = db;
34
34
 
35
35
  const { test, summary } = makeRunner();
36
36
 
@@ -39,7 +39,8 @@ const { test, summary } = makeRunner();
39
39
  // notFound → the FOR UPDATE select matches no row
40
40
  // status → the row's current status
41
41
  // hasActiveClaim → the claims lookup returns an unreleased claim
42
- function fakePool({ status = 'ready', notFound = false, hasActiveClaim = false }) {
42
+ // updatedStatus → what the UPDATE ... RETURNING reports (restore returns 'backlog')
43
+ function fakePool({ status = 'ready', notFound = false, hasActiveClaim = false, updatedStatus = 'abandoned' }) {
43
44
  return makeSqlAwareClient((s, params) => {
44
45
  if (/^SELECT id, title, status FROM tasks WHERE id = \$1 FOR UPDATE/.test(s)) {
45
46
  return { rows: notFound ? [] : [{ id: params[0], title: 'fixture task', status }] };
@@ -48,7 +49,7 @@ function fakePool({ status = 'ready', notFound = false, hasActiveClaim = false }
48
49
  return { rows: hasActiveClaim ? [{ id: 999 }] : [] };
49
50
  }
50
51
  if (/^UPDATE tasks/.test(s)) {
51
- return { rows: [{ id: params[0], title: 'fixture task', status: 'abandoned' }] };
52
+ return { rows: [{ id: params[0], title: 'fixture task', status: updatedStatus }] };
52
53
  }
53
54
  return { rows: [] }; // BEGIN / COMMIT / ROLLBACK
54
55
  });
@@ -134,5 +135,60 @@ await test('abandon: a missing task throws TASK_NOT_FOUND and rolls back', async
134
135
  assert.ok(find(pool._client.queries, /^ROLLBACK/), 'must ROLLBACK');
135
136
  });
136
137
 
138
+ // =========================================================================
139
+ // restoreTask (ADR 0152) — the undo that had to exist before abandon could be
140
+ // handed to Metics. Same txn shape as abandonTask, inverted.
141
+
142
+ await test('restore: an abandoned task flips to backlog, COMMITs, reports previousStatus', async () => {
143
+ const pool = fakePool({ status: 'abandoned', updatedStatus: 'backlog' });
144
+ const out = await restoreTask({ taskId: TASK }, { pool });
145
+
146
+ assert.equal(out.taskStatus, 'backlog', 'restores to backlog, never straight to ready');
147
+ assert.equal(out.previousStatus, 'abandoned');
148
+ assert.ok(find(pool._client.queries, /^UPDATE tasks/), 'the tasks UPDATE must run');
149
+ assert.ok(find(pool._client.queries, /^COMMIT/), 'must COMMIT');
150
+ assert.ok(!find(pool._client.queries, /^ROLLBACK/), 'no ROLLBACK on the happy path');
151
+ });
152
+
153
+ await test("restore: clears only an 'Abandoned: …' value_summary, leaving any other note", async () => {
154
+ const pool = fakePool({ status: 'abandoned', updatedStatus: 'backlog' });
155
+ await restoreTask({ taskId: TASK }, { pool });
156
+ const upd = find(pool._client.queries, /^UPDATE tasks/);
157
+ const sql = String(upd.sql).replace(/\s+/g, ' ');
158
+ assert.match(sql, /status = 'backlog'/);
159
+ assert.match(sql, /value_summary LIKE 'Abandoned: %' THEN NULL ELSE value_summary END/,
160
+ 'the retire stamp is cleared conditionally — an unrelated summary survives');
161
+ assert.equal(upd.params.length, 1, 'only the task id is bound');
162
+ });
163
+
164
+ await test('restore: a task that is not abandoned is refused and rolls back', async () => {
165
+ for (const status of ['ready', 'backlog', 'shipped']) {
166
+ const pool = fakePool({ status });
167
+ await assert.rejects(
168
+ () => restoreTask({ taskId: TASK }, { pool }),
169
+ (err) => err.code === 'TASK_NOT_ABANDONED',
170
+ `${status} must not be restorable`,
171
+ );
172
+ assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0, `must not UPDATE a ${status} task`);
173
+ assert.ok(find(pool._client.queries, /^ROLLBACK/), 'must ROLLBACK');
174
+ }
175
+ });
176
+
177
+ await test('restore: a missing task throws TASK_NOT_FOUND and rolls back', async () => {
178
+ const pool = fakePool({ notFound: true });
179
+ await assert.rejects(
180
+ () => restoreTask({ taskId: 424242 }, { pool }),
181
+ (err) => err.code === 'TASK_NOT_FOUND',
182
+ );
183
+ assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0);
184
+ assert.ok(find(pool._client.queries, /^ROLLBACK/), 'must ROLLBACK');
185
+ });
186
+
187
+ await test('restore: never consults claims (an abandoned task has none by construction)', async () => {
188
+ const pool = fakePool({ status: 'abandoned', updatedStatus: 'backlog' });
189
+ await restoreTask({ taskId: TASK }, { pool });
190
+ assert.equal(all(pool._client.queries, /SELECT id FROM claims/).length, 0);
191
+ });
192
+
137
193
  // -------------------------------------------------------------------------
138
194
  summary();
@@ -0,0 +1,53 @@
1
+ // tests/api_cli_flags.mjs
2
+ //
3
+ // Unit test for two PURE helpers exported from scripts/gds/cli-lib.js in support
4
+ // of scripts/gds/api.js's CLI flags (task 1002475):
5
+ // - stripBom(s) → drop a leading UTF-8 BOM (U+FEFF); pass clean input through
6
+ // - resolveQuiet(args, env) → true when --quiet or GDS_QUIET should suppress
7
+ // api.js's stderr status banner
8
+ // Extracted out of api.js's own top-level IIFE (which has no exports and would
9
+ // run on require()) so this behavior is unit-testable without spawning a
10
+ // process or a network call. Deterministic, no DB/network/filesystem/live
11
+ // server. Run: node tests/api_cli_flags.mjs
12
+
13
+ import { strict as assert } from 'node:assert';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const cli = require('../scripts/gds/cli-lib.js');
18
+
19
+ let passed = 0;
20
+ let failed = 0;
21
+ function test(name, fn) {
22
+ try { fn(); passed++; console.log(` ok ${name}`); }
23
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
24
+ }
25
+
26
+ test('stripBom: removes a leading U+FEFF', () => {
27
+ assert.equal(cli.stripBom('{"a":1}'), '{"a":1}');
28
+ });
29
+
30
+ test('stripBom: clean input (no BOM) passes through untouched', () => {
31
+ assert.equal(cli.stripBom('{"a":1}'), '{"a":1}');
32
+ assert.equal(cli.stripBom(''), '');
33
+ });
34
+
35
+ test('stripBom: non-string input passes through untouched (edge)', () => {
36
+ assert.equal(cli.stripBom(null), null);
37
+ assert.equal(cli.stripBom(undefined), undefined);
38
+ });
39
+
40
+ test('resolveQuiet: --quiet flag present → true', () => {
41
+ assert.equal(cli.resolveQuiet(['GET', '/api/gds/me', '--quiet'], {}), true);
42
+ });
43
+
44
+ test('resolveQuiet: GDS_QUIET env var set → true', () => {
45
+ assert.equal(cli.resolveQuiet(['GET', '/api/gds/me'], { GDS_QUIET: '1' }), true);
46
+ });
47
+
48
+ test('resolveQuiet: neither flag nor env → false (default off)', () => {
49
+ assert.equal(cli.resolveQuiet(['GET', '/api/gds/me'], {}), false);
50
+ });
51
+
52
+ console.log(`\napi.js CLI-flag helpers (task 1002475) tests: ${passed} passed, ${failed} failed`);
53
+ process.exit(failed > 0 ? 1 : 0);
@@ -29,6 +29,17 @@ db.lookupSession = async () => {
29
29
  };
30
30
  const auth = require('../src/bongos/auth.js');
31
31
 
32
+ // BV1.R107: the page gate now also resolves a permission (page.view.public) through
33
+ // the `governance` kernel port, so an authenticated request needs the port stubbed —
34
+ // exactly as tests/governance_require_permission.mjs stubs it for requirePermission.
35
+ // The audience is unchanged; tests/governance_page_gates.mjs is the parity proof.
36
+ const seams = require('../src/module-seams.js');
37
+ const catalog = require('../modules/governance/catalog.js');
38
+ seams._reset();
39
+ seams.registerProvider('governance', {
40
+ resolveBuilderPermissions: async () => catalog.seedForRank('xenos'),
41
+ });
42
+
32
43
  // Minimal res double capturing redirect target + status.
33
44
  function fakeRes() {
34
45
  return {
@@ -88,5 +99,7 @@ await t('session lookup error → fails CLOSED (redirect, next() NOT called)', a
88
99
  'must redirect to sign-in on a lookup error (fail closed)');
89
100
  });
90
101
 
102
+ seams._reset(); // leave the seam registry as we found it (the governance_* suites' pattern)
103
+
91
104
  console.log(`\n${passed} passed, ${failed} failed`);
92
105
  process.exit(failed ? 1 : 0);
@@ -11,9 +11,14 @@
11
11
 
12
12
  import { strict as assert } from 'node:assert';
13
13
  import { createRequire } from 'node:module';
14
+ import fs from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
14
17
 
15
18
  const require = createRequire(import.meta.url);
16
19
  const bf = require('../scripts/gds/backfill-sessions.js');
20
+ const { encodeProjectPath } = require('../scripts/gds/ship.js');
21
+ const { looksLikeGraderTranscript } = require('../scripts/gds/session-cost.js');
17
22
 
18
23
  let passed = 0;
19
24
  let failed = 0;
@@ -58,7 +63,7 @@ test('extracts a release marker', () => {
58
63
  test('does NOT false-positive on CLAUDE.md markdown #N links', () => {
59
64
  // The exact shape that saturates CLAUDE.md / the injected context.
60
65
  const claudeMd = asst(
61
- 'See [#435](https://amazonprimea.com/builders#/task/435) and ADR 0024 ([#395](x), [#410](y)). ' +
66
+ 'See [#435](https://example.com/builders#/task/435) and ADR 0024 ([#395](x), [#410](y)). ' +
62
67
  'Phase 2.2 [#421] shipped. (#607) mirrors to Discord.'
63
68
  );
64
69
  assert.deepEqual(bf.extractTaskIds([claudeMd]), []);
@@ -97,5 +102,126 @@ test('inScope matches every historical project-path family', () => {
97
102
  assert.equal(bf.inScope('-Users-x-Desktop-SomeOtherProject'), false);
98
103
  });
99
104
 
105
+ // --- regressions: the tool was a silent no-op on any non-legacy checkout -----
106
+
107
+ test('inScope matches THIS checkout, not just the four legacy names', () => {
108
+ // The bug: PROJECT_TOKENS was a hardcoded list of the owner's old project
109
+ // names, so every other clone skipped every folder and reported "0 transcripts"
110
+ // while exiting 0. The scope token must be derived from the repo itself.
111
+ const derived = encodeProjectPath(path.resolve('.'));
112
+ assert.ok(bf.inScope(derived), 'the current checkout must be in scope');
113
+ assert.ok(
114
+ bf.inScope(`${derived}--claude-worktrees-task-1002503`),
115
+ 'a per-task worktree dir must be in scope too'
116
+ );
117
+ });
118
+
119
+ test('a Windows cwd encodes to the dir name Claude Code actually creates', () => {
120
+ // Claude Code replaces every non-alphanumeric char, so a Windows cwd (drive
121
+ // colon, backslashes, spaces) encodes to one long dashed name. This is the
122
+ // shape the old [/.] encoder no-op'd on, which is why no transcript was found.
123
+ assert.equal(
124
+ encodeProjectPath('C:\\Users\\rocke\\OneDrive\\Desktop\\Cloud Bongos\\cloud-bongos'),
125
+ 'C--Users-rocke-OneDrive-Desktop-Cloud-Bongos-cloud-bongos'
126
+ );
127
+ // And the matching loop accepts it when it IS the derived token — asserted
128
+ // against inScope itself, machine-independently, via the real first token.
129
+ const derived = bf.PROJECT_TOKENS[0];
130
+ assert.ok(bf.inScope(derived), 'the derived token must match itself through inScope');
131
+ assert.equal(bf.inScope('C--Users-someone-else-Desktop-OtherProject'), false);
132
+ });
133
+
134
+ test('buildUploadPayload sends model_usage at the TOP level', () => {
135
+ // The route reads body.model_usage and never looks inside metrics; dropping it
136
+ // silently under-pays via the opus cache_read fallback. Guard the field set.
137
+ const digest = {
138
+ totalTokens: 135734889,
139
+ durationSeconds: 6407,
140
+ startedAt: '2026-07-26T21:18:16.356Z',
141
+ endedAt: '2026-07-26T23:05:03.560Z',
142
+ detail: [{ i: 0, role: 'assistant' }],
143
+ metrics: { assistant_turns: 1, model_usage: { 'claude-opus-5': { input: 1, output: 2 } } },
144
+ };
145
+ const payload = bf.buildUploadPayload({ sessionId: 'abc', taskIds: [1002471], digest });
146
+ assert.deepEqual(payload.model_usage, { 'claude-opus-5': { input: 1, output: 2 } });
147
+ assert.equal(payload.session_id, 'abc');
148
+ assert.deepEqual(payload.task_ids, [1002471]);
149
+ assert.equal(payload.total_tokens, 135734889);
150
+ });
151
+
152
+ test('buildUploadPayload defaults model_usage to {} rather than undefined', () => {
153
+ // An unpriceable digest must still upload (the server prices from total_tokens);
154
+ // an undefined field would fail the route's object validator.
155
+ const payload = bf.buildUploadPayload({ sessionId: 'x', taskIds: [], digest: { metrics: {} } });
156
+ assert.deepEqual(payload.model_usage, {});
157
+ });
158
+
159
+ test('grader detection matches a NEW worker persona, not just the known four', () => {
160
+ // A closed name list would let a future grader worker be uploaded as a builder
161
+ // session and earn credit for spend the ship path never pays.
162
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backfill-grader2-'));
163
+ const write = (name, text) => {
164
+ const f = path.join(dir, `${name}.jsonl`);
165
+ fs.writeFileSync(f, JSON.stringify({ type: 'user', message: { role: 'user', content: text } }) + '\n');
166
+ return f;
167
+ };
168
+ try {
169
+ for (const [persona, joiner] of [['Narc', '— a specialist'], ['Quality', 'specialist — an'],
170
+ ['Efficiency Monger', '— a specialist'], ['Cartographer', '— a specialist']]) {
171
+ const f = write(persona.replace(/\s/g, ''), `You are the **${persona}** ${joiner} adversarial subagent for the project.`);
172
+ assert.ok(looksLikeGraderTranscript(f), `${persona} must be detected as a grader worker`);
173
+ }
174
+ // A real session that merely mentions the panel must NOT be skipped.
175
+ const real = write('real', 'The adversarial subagent panel flagged my diff — help me fix it.');
176
+ assert.equal(looksLikeGraderTranscript(real), false, 'a builder session must not be skipped');
177
+ } finally {
178
+ fs.rmSync(dir, { recursive: true, force: true });
179
+ }
180
+ });
181
+
182
+ test('extracts a 7-digit task id from a ship.js invocation', () => {
183
+ // {1,4} matched nothing on a live instance, so no modern session could ever pay.
184
+ const ids = bf.extractTaskIds([toolUse('Bash', { command: 'node scripts/gds/ship.js 1002471' })]);
185
+ assert.deepEqual(ids, [1002471]);
186
+ });
187
+
188
+ test('extracts 7-digit ids from claim/release/Claimed markers', () => {
189
+ assert.deepEqual(bf.extractTaskIds([asst('Running /builder-claim 1002503 now')]), [1002503]);
190
+ assert.deepEqual(bf.extractTaskIds([asst('builder-release 1002446 — handing it back')]), [1002446]);
191
+ assert.deepEqual(bf.extractTaskIds([asst('Claimed task #1002475.')]), [1002475]);
192
+ });
193
+
194
+ test('7-digit ids survive extractTaskIds (the n < 10000 clamp is gone)', () => {
195
+ // Widening TASK_MARKERS alone was not enough: the clamp dropped them again.
196
+ const entries = [
197
+ toolUse('Bash', { command: 'node scripts/gds/ship.js 1002471' }),
198
+ asst('Claimed #284.'), // legacy 3-digit ids must still work
199
+ ];
200
+ assert.deepEqual(bf.extractTaskIds(entries), [284, 1002471]);
201
+ });
202
+
203
+ test('grader worker transcripts are detectable and would otherwise be paid', () => {
204
+ // The grader panel writes each worker as its OWN transcript in the same project
205
+ // dir. A worker quotes the diff, so it carries real ship markers — uploading one
206
+ // would book a cost-plus reward the live ship path never pays.
207
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backfill-grader-'));
208
+ const file = path.join(dir, 'aaaaaaaa-0000-0000-0000-000000000000.jsonl');
209
+ const graderPrompt =
210
+ 'You are the **Narc** — a specialist adversarial subagent for this project. ' +
211
+ 'Review the diff shipped by node scripts/gds/ship.js 1002471 --summary "x".';
212
+ try {
213
+ fs.writeFileSync(file, JSON.stringify({ type: 'user', message: { role: 'user', content: graderPrompt } }) + '\n');
214
+ assert.ok(looksLikeGraderTranscript(file), 'the grader worker prompt must be detected');
215
+ // Load-bearing: without the skip, this transcript yields a real shipped id.
216
+ assert.deepEqual(bf.extractTaskIds([asst(graderPrompt)]), [1002471]);
217
+
218
+ const normal = path.join(dir, 'bbbbbbbb-0000-0000-0000-000000000000.jsonl');
219
+ fs.writeFileSync(normal, JSON.stringify({ type: 'user', message: { role: 'user', content: 'fix the backfill script' } }) + '\n');
220
+ assert.equal(looksLikeGraderTranscript(normal), false, 'a real builder session must NOT be skipped');
221
+ } finally {
222
+ fs.rmSync(dir, { recursive: true, force: true });
223
+ }
224
+ });
225
+
100
226
  console.log(`\nbackfill_sessions: ${passed} passed, ${failed} failed`);
101
227
  process.exit(failed ? 1 : 0);