@bongos/core 1.18.0 → 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 (553) hide show
  1. package/.bongos-core.json +1740 -640
  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 +5 -3
  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 +11 -2
  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/0142-project-context-is-prose-only.md +41 -0
  27. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  28. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  29. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  30. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  31. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  32. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  33. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  34. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  35. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  36. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  37. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  38. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  39. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  40. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  41. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  42. package/docs/adr/README.md +15 -2
  43. package/docs/api/openapi.json +2648 -483
  44. package/docs/api-reference.md +73 -13
  45. package/docs/architecture.md +4 -2
  46. package/docs/branding-contract.md +3 -1
  47. package/docs/canonical-permissions.md +21 -8
  48. package/docs/file-map.md +15 -7
  49. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  50. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  51. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  52. package/docs/onboarding/diagrams/assertions.json +4 -7
  53. package/docs/onboarding/drachmae-and-karma.md +3 -3
  54. package/docs/onboarding/primer.md +17 -16
  55. package/docs/onboarding/slash-commands.md +124 -0
  56. package/docs/recipes/builder-box-lifecycle.md +23 -0
  57. package/docs/recipes/devbox-app-release.md +14 -4
  58. package/docs/recipes/instance-first-run.md +16 -0
  59. package/docs/recipes/local-dev.md +81 -0
  60. package/docs/recipes/ops-gotchas.md +19 -1
  61. package/docs/recipes/overnight-builder.md +1 -1
  62. package/docs/recipes/packaging-the-core.md +13 -0
  63. package/docs/recipes/parallel-goal-waves.md +174 -0
  64. package/docs/recipes/ship-and-regrade.md +2 -2
  65. package/docs/recipes/standalone-live-docs.md +48 -0
  66. package/docs/recipes/upgrading-the-core.md +5 -2
  67. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  68. package/migrations/003_pms.sql +6 -7
  69. package/migrations/006_pms_v2.sql +3 -31
  70. package/migrations/039_newcomer_friendly_column.sql +5 -0
  71. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  72. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  73. package/migrations/core_193_task_needs_rebase.sql +28 -0
  74. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  75. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  76. package/migrations/core_196_session_reward_topup.sql +69 -0
  77. package/modules/autonomy/pollers/routine-timers.js +9 -8
  78. package/modules/builder-settings/wandering-prefs.js +61 -18
  79. package/modules/dev-box/app/README.md +73 -0
  80. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  81. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  82. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  83. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  84. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  85. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  86. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  87. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  88. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  89. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  90. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  91. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  92. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  93. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  94. package/modules/dev-box/app/build-resources/icon.png +0 -0
  95. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  96. package/modules/dev-box/app/electron-builder.yml +95 -0
  97. package/modules/dev-box/app/package-lock.json +5291 -0
  98. package/modules/dev-box/app/package.json +30 -0
  99. package/modules/dev-box/app/renderer/app.js +319 -0
  100. package/modules/dev-box/app/renderer/index.html +45 -0
  101. package/modules/dev-box/app/renderer/styles.css +246 -0
  102. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  103. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  104. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  105. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  106. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  107. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  108. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  109. package/modules/dev-box/app/src/box-controller.js +287 -0
  110. package/modules/dev-box/app/src/brand.generated.json +13 -0
  111. package/modules/dev-box/app/src/brand.js +16 -0
  112. package/modules/dev-box/app/src/config.js +19 -0
  113. package/modules/dev-box/app/src/gds-client.js +90 -0
  114. package/modules/dev-box/app/src/main.js +610 -0
  115. package/modules/dev-box/app/src/pairing.js +57 -0
  116. package/modules/dev-box/app/src/preload.js +29 -0
  117. package/modules/dev-box/app/src/session-store.js +58 -0
  118. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  119. package/modules/dev-box/app/src/task-picker.js +70 -0
  120. package/modules/dev-box/app/src/tray.js +99 -0
  121. package/modules/dev-box/app/src/updater.js +55 -0
  122. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  123. package/modules/dev-box/app/test/unit.mjs +526 -0
  124. package/modules/dev-box/box-access.js +121 -4
  125. package/modules/dev-box/boxes.js +20 -0
  126. package/modules/dev-box/module.json +1 -1
  127. package/modules/dev-box/routes/box.js +87 -3
  128. package/modules/economy/credits.js +116 -25
  129. package/modules/economy/reward.js +8 -0
  130. package/modules/economy/streak.js +2 -1
  131. package/modules/governance/CLAUDE.md +15 -0
  132. package/modules/governance/catalog.js +419 -0
  133. package/modules/governance/config.js +174 -0
  134. package/modules/governance/db.js +125 -0
  135. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  136. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  137. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  138. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  139. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  140. package/modules/governance/module.json +10 -0
  141. package/modules/governance/protected-surfaces.js +88 -0
  142. package/modules/governance/protected-surfaces.json +80 -0
  143. package/modules/governance/rank-roles.js +83 -0
  144. package/modules/governance/resolver.js +221 -0
  145. package/modules/governance/routes/governance.js +89 -0
  146. package/modules/governance/routes/role-sync.js +49 -0
  147. package/modules/governance/session-scopes.js +127 -0
  148. package/modules/governance/session-scopes.json +38 -0
  149. package/modules/hall-ui/public/atlas.css +117 -0
  150. package/modules/hall-ui/public/atlas.html +3 -114
  151. package/modules/hall-ui/public/brand-apply.js +34 -6
  152. package/modules/hall-ui/public/builders.js +187 -219
  153. package/modules/hall-ui/public/diagrams.html +2 -0
  154. package/modules/hall-ui/public/dom-utils.js +27 -0
  155. package/modules/hall-ui/public/drachmae.html +2 -0
  156. package/modules/hall-ui/public/drachmae.js +54 -1
  157. package/modules/hall-ui/public/gate.css +98 -0
  158. package/modules/hall-ui/public/gate.html +3 -95
  159. package/modules/hall-ui/public/goal-inbox.js +4 -1
  160. package/modules/hall-ui/public/goals-page.js +626 -114
  161. package/modules/hall-ui/public/goals.css +79 -1
  162. package/modules/hall-ui/public/goals.html +38 -11
  163. package/modules/hall-ui/public/hall-widgets.js +62 -52
  164. package/modules/hall-ui/public/harbor.css +123 -0
  165. package/modules/hall-ui/public/harbor.html +3 -120
  166. package/modules/hall-ui/public/index.html +11 -4
  167. package/modules/hall-ui/public/instances-panel.js +4 -1
  168. package/modules/hall-ui/public/modules.css +63 -0
  169. package/modules/hall-ui/public/modules.html +3 -60
  170. package/modules/hall-ui/public/my-projects.html +78 -0
  171. package/modules/hall-ui/public/my-projects.js +142 -0
  172. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  173. package/modules/hall-ui/public/not-ready.html +2 -0
  174. package/modules/hall-ui/public/pair.css +73 -0
  175. package/modules/hall-ui/public/pair.html +3 -70
  176. package/modules/hall-ui/public/palette.js +302 -0
  177. package/modules/hall-ui/public/primer.css +13 -0
  178. package/modules/hall-ui/public/primer.html +2 -0
  179. package/modules/hall-ui/public/profile.html +2 -0
  180. package/modules/hall-ui/public/ranks.html +2 -0
  181. package/modules/hall-ui/public/roadmap.css +179 -0
  182. package/modules/hall-ui/public/roadmap.html +9 -172
  183. package/modules/hall-ui/public/roadmap.js +329 -316
  184. package/modules/hall-ui/public/scouting.html +45 -0
  185. package/modules/hall-ui/public/scouting.js +73 -0
  186. package/modules/hall-ui/public/sessions.css +16 -0
  187. package/modules/hall-ui/public/sessions.html +3 -13
  188. package/modules/hall-ui/public/settings.html +2 -0
  189. package/modules/hall-ui/public/settings.js +4 -4
  190. package/modules/hall-ui/public/shell.js +143 -18
  191. package/modules/hall-ui/public/style.css +231 -39
  192. package/modules/hall-ui/public/task-detail.js +347 -26
  193. package/modules/hall-ui/public/task.html +6 -1
  194. package/modules/hall-ui/public/watch.html +2 -0
  195. package/modules/hall-ui/public/work.html +8 -6
  196. package/modules/hall-ui/public/work.js +1 -1
  197. package/modules/ideas/inbox.js +61 -11
  198. package/modules/ideas/routes/inbox.js +23 -0
  199. package/modules/lifecycle/claim-eligibility.js +72 -0
  200. package/modules/lifecycle/db.js +519 -139
  201. package/modules/lifecycle/done-when.js +18 -3
  202. package/modules/lifecycle/github-push.js +19 -2
  203. package/modules/lifecycle/goal-advisory.js +114 -0
  204. package/modules/lifecycle/goal-authz.js +62 -0
  205. package/modules/lifecycle/lifecycle.js +9 -0
  206. package/modules/lifecycle/publish-reconciler.js +109 -6
  207. package/modules/lifecycle/routes/claims.js +68 -23
  208. package/modules/lifecycle/routes/done-when.js +110 -15
  209. package/modules/lifecycle/routes/goals.js +8 -38
  210. package/modules/lifecycle/routes/tasks.js +38 -6
  211. package/modules/lifecycle/ship-card.js +6 -0
  212. package/modules/onboarding/onboarding-state.js +1 -1
  213. package/modules/platform-identity/CLAUDE.md +15 -0
  214. package/modules/platform-identity/hub-device.js +147 -0
  215. package/modules/platform-identity/hub-keys.js +151 -0
  216. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  217. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  218. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  219. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  220. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  221. package/modules/platform-identity/module.json +18 -0
  222. package/modules/platform-identity/platform-identity.js +531 -0
  223. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  224. package/modules/platform-identity/routes/my-projects.js +140 -0
  225. package/modules/platform-identity/routes/projects.js +107 -0
  226. package/modules/platform-identity/routes/scouting.js +35 -0
  227. package/modules/platform-identity/routes/sso.js +402 -0
  228. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  229. package/modules/platform-identity/ui/.gitkeep +0 -0
  230. package/modules/security/routes/reports.js +10 -0
  231. package/modules/sessions/db.js +166 -21
  232. package/modules/sessions/module.json +1 -1
  233. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  234. package/modules/sessions/routes/sessions.js +33 -0
  235. package/modules/status-ui/public/explore.html +54 -0
  236. package/modules/status-ui/public/explore.js +67 -0
  237. package/package-lock.json +2 -2
  238. package/package.json +1 -1
  239. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  270. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  271. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  272. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  273. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  274. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  275. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  276. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  277. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  278. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  279. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  280. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  281. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  282. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  283. package/public-landing/create.html +252 -62
  284. package/public-landing/gate/README.md +137 -0
  285. package/public-landing/gate/dispmaps/bar.png +0 -0
  286. package/public-landing/gate/dispmaps/flat.png +0 -0
  287. package/public-landing/gate/dispmaps/key.png +0 -0
  288. package/public-landing/gate/index.html +587 -0
  289. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  290. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  291. package/public-landing/gate/space.css +799 -0
  292. package/public-landing/gate/space.js +1826 -0
  293. package/public-landing/gate/sprites/enter.png +0 -0
  294. package/public-landing/gate/sprites/search.png +0 -0
  295. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  296. package/public-landing/gate/sprites/tile.png +0 -0
  297. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  298. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  299. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  300. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  301. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  302. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  303. package/scripts/gds/abandon.js +6 -4
  304. package/scripts/gds/api.js +10 -5
  305. package/scripts/gds/api.ps1 +26 -0
  306. package/scripts/gds/api.sh +6 -0
  307. package/scripts/gds/audit-ships.js +7 -1
  308. package/scripts/gds/backfill-sessions.js +78 -18
  309. package/scripts/gds/backup.js +23 -1
  310. package/scripts/gds/box-connect-lib.js +124 -31
  311. package/scripts/gds/box-sync.js +66 -0
  312. package/scripts/gds/box.js +225 -53
  313. package/scripts/gds/cf-tunnel.js +145 -6
  314. package/scripts/gds/claim.js +34 -3
  315. package/scripts/gds/cli-lib.js +161 -21
  316. package/scripts/gds/code.js +6 -2
  317. package/scripts/gds/deploy-divergence.js +209 -0
  318. package/scripts/gds/dev-lib.js +309 -0
  319. package/scripts/gds/dev.js +322 -0
  320. package/scripts/gds/doctor.js +90 -61
  321. package/scripts/gds/fetch-art-key.js +1 -1
  322. package/scripts/gds/fetch-sound-prefs.js +2 -2
  323. package/scripts/gds/fitness.js +228 -12
  324. package/scripts/gds/gds-db-backup.service +16 -1
  325. package/scripts/gds/gds-db-offsite.service +9 -1
  326. package/scripts/gds/gen-api-client.js +1 -1
  327. package/scripts/gds/gen-api-docs.js +36 -14
  328. package/scripts/gds/gen-diagrams.js +45 -28
  329. package/scripts/gds/gen-file-map.js +2 -1
  330. package/scripts/gds/gen-freshness.js +32 -0
  331. package/scripts/gds/gen-repo-map.js +3 -2
  332. package/scripts/gds/gen-session-index.js +14 -5
  333. package/scripts/gds/hooks/pre-push.js +2 -2
  334. package/scripts/gds/init.js +60 -3
  335. package/scripts/gds/install-git-hooks.js +1 -1
  336. package/scripts/gds/key-card.js +15 -6
  337. package/scripts/gds/key-status.js +1 -1
  338. package/scripts/gds/linkify-refs.js +35 -9
  339. package/scripts/gds/lint-migrations.js +102 -0
  340. package/scripts/gds/login.js +58 -11
  341. package/scripts/gds/mirror-redact.js +5 -0
  342. package/scripts/gds/package-core.js +263 -4
  343. package/scripts/gds/paste-token.js +7 -1
  344. package/scripts/gds/preflight.js +126 -0
  345. package/scripts/gds/provision.js +268 -19
  346. package/scripts/gds/publish-manifest.js +22 -1
  347. package/scripts/gds/reauth.js +7 -3
  348. package/scripts/gds/regen-instance-docs.js +47 -0
  349. package/scripts/gds/run-unit-tests.js +42 -16
  350. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  351. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  352. package/scripts/gds/sequence.js +425 -0
  353. package/scripts/gds/session-cost.js +10 -1
  354. package/scripts/gds/setup.js +72 -5
  355. package/scripts/gds/shell.js +5 -3
  356. package/scripts/gds/ship.js +433 -59
  357. package/scripts/gds/smoke-gds.js +139 -8
  358. package/scripts/gds/start.js +171 -22
  359. package/scripts/gds/status.js +1 -1
  360. package/scripts/gds/triage.js +10 -9
  361. package/scripts/gds/upgrade.js +271 -7
  362. package/scripts/migrate.sh +10 -0
  363. package/scripts/status-mirror-sync.js +114 -84
  364. package/src/bongos/api-errors.js +1 -0
  365. package/src/bongos/auth.js +809 -107
  366. package/src/bongos/db-kernel.js +27 -0
  367. package/src/bongos/db.js +105 -0
  368. package/src/bongos/github-repos.js +93 -0
  369. package/src/bongos/module-scope-map.js +27 -1
  370. package/src/bongos/permission-path-check.js +136 -102
  371. package/src/bongos/repo-info.js +47 -11
  372. package/src/bongos/route-rank-check.js +429 -46
  373. package/src/bongos/routes/_helpers.js +12 -1
  374. package/src/bongos/routes/auth.js +312 -13
  375. package/src/bongos/routes/backup.js +58 -2
  376. package/src/bongos/routes/github.js +90 -0
  377. package/src/bongos/routes/instance.js +15 -3
  378. package/src/bongos/routes/me.js +35 -1
  379. package/src/bongos/routes.js +2 -0
  380. package/src/bongos/serve-internal.js +84 -15
  381. package/src/branding.js +49 -2
  382. package/src/instance-config.js +46 -0
  383. package/src/module-api.js +140 -1
  384. package/src/platform-server.js +86 -9
  385. package/tests/abandon_task.mjs +59 -3
  386. package/tests/api_cli_flags.mjs +53 -0
  387. package/tests/auth_page_gate.mjs +13 -0
  388. package/tests/backfill_sessions.mjs +127 -1
  389. package/tests/backup_cli.mjs +54 -0
  390. package/tests/bongos_box_connect.mjs +77 -0
  391. package/tests/box_access.mjs +130 -1
  392. package/tests/box_credential.mjs +125 -0
  393. package/tests/box_host_keys.mjs +24 -0
  394. package/tests/box_infra_resolve.mjs +149 -0
  395. package/tests/box_scope_session.mjs +6 -2
  396. package/tests/box_sync.mjs +55 -0
  397. package/tests/box_task_scope.mjs +99 -0
  398. package/tests/branding.mjs +41 -23
  399. package/tests/cf_ruleset_preflight.mjs +142 -0
  400. package/tests/cf_tunnel.mjs +37 -0
  401. package/tests/claim_gate_rebase.mjs +98 -0
  402. package/tests/clawback_confirmed_backlog.mjs +103 -0
  403. package/tests/cli_lib_formatters.mjs +20 -0
  404. package/tests/confirm_attribution.mjs +30 -46
  405. package/tests/credit_grant.mjs +198 -10
  406. package/tests/criterion_authoring_gate.mjs +272 -0
  407. package/tests/criterion_progress.mjs +33 -1
  408. package/tests/dev_lib.mjs +253 -0
  409. package/tests/devbox_artifact_gate.mjs +75 -0
  410. package/tests/devbox_dmg_backdrop.mjs +137 -0
  411. package/tests/devbox_icons.mjs +304 -0
  412. package/tests/devbox_installer_brand.mjs +232 -0
  413. package/tests/devbox_key_register.mjs +234 -0
  414. package/tests/docs_openapi_route.mjs +110 -0
  415. package/tests/doctor_hooks.mjs +141 -0
  416. package/tests/extract_json.mjs +140 -0
  417. package/tests/fitness.mjs +89 -0
  418. package/tests/gen_freshness.mjs +96 -0
  419. package/tests/github_repos_helpers.mjs +91 -0
  420. package/tests/github_token_store.mjs +121 -0
  421. package/tests/goal_advisory.mjs +251 -0
  422. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  423. package/tests/goal_routes.mjs +59 -4
  424. package/tests/goal_tier.mjs +12 -3
  425. package/tests/governance_catalog.mjs +129 -0
  426. package/tests/governance_claim_eligibility.mjs +262 -0
  427. package/tests/governance_config.mjs +187 -0
  428. package/tests/governance_ownership_scope.mjs +410 -0
  429. package/tests/governance_page_gates.mjs +324 -0
  430. package/tests/governance_parity.mjs +336 -0
  431. package/tests/governance_principals.mjs +232 -0
  432. package/tests/governance_protected_surfaces.mjs +208 -0
  433. package/tests/governance_require_permission.mjs +223 -0
  434. package/tests/governance_resolver.mjs +84 -0
  435. package/tests/governance_role_sync.mjs +231 -0
  436. package/tests/governance_seed.mjs +127 -0
  437. package/tests/governance_session_scope.mjs +434 -0
  438. package/tests/governance_vanilla_boot.mjs +203 -0
  439. package/tests/grade_attribution.mjs +7 -7
  440. package/tests/hall_audit.mjs +223 -0
  441. package/tests/hall_nav.mjs +191 -0
  442. package/tests/hall_palette.mjs +206 -0
  443. package/tests/hall_signout.mjs +124 -0
  444. package/tests/hook_user_agents.mjs +10 -0
  445. package/tests/idea_triage.mjs +162 -0
  446. package/tests/idp_device_exchange.mjs +198 -0
  447. package/tests/idp_federation.mjs +100 -0
  448. package/tests/init.mjs +51 -0
  449. package/tests/instance_config.mjs +65 -0
  450. package/tests/instance_mismatch_guard.mjs +23 -6
  451. package/tests/instance_release_config.mjs +104 -0
  452. package/tests/linkify_refs.mjs +15 -2
  453. package/tests/migrations_forward_safe.mjs +86 -0
  454. package/tests/module_api.mjs +1 -1
  455. package/tests/module_contributions.mjs +55 -0
  456. package/tests/module_loader.mjs +10 -5
  457. package/tests/module_scope_active_claims.mjs +94 -0
  458. package/tests/needs_rebase.mjs +92 -0
  459. package/tests/one_active_claim_per_session.mjs +76 -0
  460. package/tests/override_attribution.mjs +10 -8
  461. package/tests/override_request_ownership.mjs +8 -4
  462. package/tests/package_core.mjs +246 -1
  463. package/tests/parse_cookie_decode.mjs +42 -0
  464. package/tests/pay_on_land.mjs +129 -0
  465. package/tests/platform_boot.mjs +100 -1
  466. package/tests/post_login_landing.mjs +42 -0
  467. package/tests/preflight_checks.mjs +72 -0
  468. package/tests/provision.mjs +289 -7
  469. package/tests/pruned_lockfile_integrity.mjs +79 -0
  470. package/tests/publish_branch_route.mjs +53 -0
  471. package/tests/publish_reconciler.mjs +187 -2
  472. package/tests/repo_info_binding.mjs +248 -0
  473. package/tests/reward_gate.mjs +11 -2
  474. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  475. package/tests/reward_mode_surfacing.mjs +168 -0
  476. package/tests/route_permission_check.mjs +301 -0
  477. package/tests/route_rank_check.mjs +254 -12
  478. package/tests/route_validation.mjs +30 -0
  479. package/tests/sequence.mjs +267 -0
  480. package/tests/session_digest.mjs +44 -0
  481. package/tests/session_index.mjs +18 -6
  482. package/tests/session_records.mjs +39 -1
  483. package/tests/session_reward_on_land.mjs +381 -0
  484. package/tests/session_reward_reconcile.mjs +229 -0
  485. package/tests/ship_api_client_pathspec.mjs +100 -0
  486. package/tests/ship_base_flag.mjs +93 -0
  487. package/tests/ship_ci_deploy.mjs +140 -3
  488. package/tests/ship_upstream_deletion.mjs +57 -0
  489. package/tests/smoke_target.mjs +233 -0
  490. package/tests/start_env_guard.mjs +87 -0
  491. package/tests/start_rebase_warning.mjs +103 -0
  492. package/tests/status_mirror_sync.mjs +122 -0
  493. package/tests/stop_refcheck.mjs +19 -1
  494. package/tests/task_goal_link.mjs +15 -6
  495. package/tests/upgrade.mjs +393 -5
  496. package/tests/web_admission_status.mjs +162 -0
  497. package/tests/write_validation_gaps.mjs +110 -0
  498. package/.gitignore +0 -91
  499. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  500. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  501. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  502. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  503. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  504. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  505. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  506. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  507. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  508. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  509. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  510. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  511. package/scripts/hall-preview/fixtures/goals.json +0 -502
  512. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  513. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  514. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  515. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  516. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  517. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  518. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  519. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  520. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  521. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  522. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  523. package/scripts/hall-preview/fixtures/instance.json +0 -124
  524. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  525. package/scripts/hall-preview/fixtures/me.json +0 -476
  526. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  527. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  528. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  529. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  530. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  531. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  532. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  533. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  534. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  535. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  536. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  537. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  538. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  539. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  540. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  541. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  542. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  543. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  544. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  545. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  546. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  547. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  548. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  549. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  550. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  551. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  552. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  553. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -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);
@@ -10,6 +10,7 @@
10
10
  // Run: node tests/backup_cli.mjs
11
11
 
12
12
  import { strict as assert } from 'node:assert';
13
+ import { createRequire } from 'node:module';
13
14
 
14
15
  let passed = 0;
15
16
  let failed = 0;
@@ -94,6 +95,59 @@ t('trigger response: 409 when already running', () => {
94
95
  assert.equal(response.error, 'backup_already_running');
95
96
  });
96
97
 
98
+ // ---- staleness detection (task 1002479) -------------------------------------
99
+ // These import the REAL implementations rather than re-implementing them, so a
100
+ // regression in the shipped code fails here instead of passing against a copy.
101
+ const backupRoute = createRequire(import.meta.url)('../src/bongos/routes/backup.js');
102
+ const { parseDumpTimestamp, dumpAgeHours, isStaleDump } = backupRoute;
103
+
104
+ const NOW = Date.parse('2026-07-28T23:00:00Z');
105
+
106
+ t('parseDumpTimestamp: the dump stamp is NOT plain ISO — colons must be reinserted', () => {
107
+ // Regression guard: Date.parse on the raw stamp returns NaN. If someone
108
+ // "simplifies" this to Date.parse(stamp), staleness silently becomes unknown
109
+ // → every dump reads stale (or, with a sloppier fallback, every dump reads fresh).
110
+ assert.ok(Number.isNaN(Date.parse('2026-07-28T230421Z')), 'raw stamp should not parse as ISO');
111
+ assert.equal(parseDumpTimestamp('2026-07-28T230421Z'), Date.parse('2026-07-28T23:04:21Z'));
112
+ });
113
+
114
+ t('parseDumpTimestamp: rejects garbage rather than guessing', () => {
115
+ for (const bad of ['', null, undefined, 'nope', '2026-07-28', '2026-07-28T2304Z']) {
116
+ assert.equal(parseDumpTimestamp(bad), null, `should reject ${JSON.stringify(bad)}`);
117
+ }
118
+ });
119
+
120
+ t('dumpAgeHours: computes hours, floors at 0 for a future stamp', () => {
121
+ assert.equal(dumpAgeHours('2026-07-28T220000Z', NOW), 1);
122
+ assert.equal(dumpAgeHours('2026-07-27T230000Z', NOW), 24);
123
+ assert.equal(dumpAgeHours('2026-07-29T230000Z', NOW), 0, 'future stamp must not go negative');
124
+ assert.equal(dumpAgeHours('garbage', NOW), null);
125
+ });
126
+
127
+ t('isStaleDump: fresh dump is not stale', () => {
128
+ assert.equal(isStaleDump('2026-07-28T230421Z', NOW), false);
129
+ assert.equal(isStaleDump('2026-07-27T140000Z', NOW), false, '33h is inside the 36h threshold');
130
+ });
131
+
132
+ t('isStaleDump: the real 13-day cloudbongos outage is detected', () => {
133
+ // The actual last good dump before the 203/EXEC failure was 2026-07-15.
134
+ assert.equal(isStaleDump('2026-07-15T040302Z', NOW), true);
135
+ assert.ok(dumpAgeHours('2026-07-15T040302Z', NOW) > 300, 'should be ~13 days of hours');
136
+ });
137
+
138
+ t('isStaleDump: an undateable stamp fails LOUD (stale), never silently healthy', () => {
139
+ assert.equal(isStaleDump('garbage', NOW), true);
140
+ assert.equal(isStaleDump('', NOW), true);
141
+ assert.equal(isStaleDump(undefined, NOW), true);
142
+ });
143
+
144
+ t('isStaleDump: threshold boundary is exclusive', () => {
145
+ const at36h = '2026-07-27T110000Z'; // exactly 36h before NOW
146
+ assert.equal(dumpAgeHours(at36h, NOW), 36);
147
+ assert.equal(isStaleDump(at36h, NOW, 36), false, 'exactly at threshold is not yet stale');
148
+ assert.equal(isStaleDump('2026-07-27T105900Z', NOW, 36), true, 'one minute past is stale');
149
+ });
150
+
97
151
  // ---- summary ----------------------------------------------------------------
98
152
  console.log('');
99
153
  if (failed === 0) {
@@ -3,6 +3,7 @@
3
3
  // helpers are exercised. Picked up by scripts/gds/run-unit-tests.js (the `unit` gate).
4
4
  import assert from 'node:assert/strict';
5
5
  import { test } from 'node:test';
6
+ import crypto from 'node:crypto';
6
7
  import lib from '../scripts/gds/box-connect-lib.js';
7
8
 
8
9
  const HOST = 'box-alice.dev.amazonprimea.com';
@@ -63,6 +64,62 @@ test('buildKnownHostsUpsert drops the stale line for the host, keeps others, app
63
64
  assert.ok(next.includes(`${HOST} ssh-rsa AAAARSANEW`), 'appends every reported key');
64
65
  });
65
66
 
67
+ // task 1002509 — the upsert used to compare field 1 to the hostname with `!==`, so
68
+ // the two forms OpenSSH actually writes survived the "replace" and stale lines
69
+ // ACCUMULATED: the client kept verifying against the first, now-wrong entry
70
+ // ("Host denied (verification failed)") on a box that had just re-reported keys.
71
+ test('buildKnownHostsUpsert drops a host,ip entry for this box (the accumulation bug)', () => {
72
+ const existing = `${HOST},64.225.49.206 ssh-ed25519 STALE\n`;
73
+ const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW');
74
+ assert.ok(!next.includes('STALE'), 'a `host,ip` field-1 names this host and must be replaced');
75
+ assert.ok(next.includes(`${HOST} ssh-ed25519 AAAANEW`));
76
+ });
77
+
78
+ test('buildKnownHostsUpsert drops a HASHED entry for this box, keeps another host, keeps comments', () => {
79
+ const salt = crypto.randomBytes(20);
80
+ const hashFor = (h) => `|1|${salt.toString('base64')}|${crypto.createHmac('sha1', salt).update(h).digest('base64')}`;
81
+ const existing = [
82
+ '# hand-written note',
83
+ `${hashFor(HOST)} ssh-ed25519 STALEHASHED`,
84
+ `${hashFor('other.example.com')} ssh-ed25519 KEEPHASHED`,
85
+ ].join('\n') + '\n';
86
+ const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW');
87
+ assert.ok(!next.includes('STALEHASHED'), 'HashKnownHosts entries must be matched, not stepped over');
88
+ assert.ok(next.includes('KEEPHASHED'), 'another host hashed with the same salt stays');
89
+ assert.ok(next.includes('# hand-written note'), 'comments survive');
90
+ });
91
+
92
+ test('buildKnownHostsUpsert drops an IP-keyed line when the box IP is supplied', () => {
93
+ const existing = '203.0.113.7 ssh-ed25519 STALEIP\nunrelated.example.com ssh-ed25519 KEEP\n';
94
+ const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW', '203.0.113.7');
95
+ assert.ok(!next.includes('STALEIP'), 'DNS is repointed on every wake — an old IP line is stale too');
96
+ assert.ok(next.includes('KEEP'));
97
+ });
98
+
99
+ test('buildKnownHostsUpsert is idempotent — re-running never grows the file', () => {
100
+ const first = lib.buildKnownHostsUpsert('', HOST, 'ssh-ed25519 AAAANEW\nssh-rsa AAAARSA');
101
+ const second = lib.buildKnownHostsUpsert(first, HOST, 'ssh-ed25519 AAAANEW\nssh-rsa AAAARSA');
102
+ assert.equal(second, first);
103
+ assert.equal(second.split('\n').filter(Boolean).length, 2);
104
+ });
105
+
106
+ // task 1002509 — the old inline logic FAILED OPEN: no fingerprint (ssh-keygen absent
107
+ // from a GUI process's leaner PATH) or an unreadable key list meant "skip", so a
108
+ // builder whose key was never on the server got a bare "Permission denied". A
109
+ // redundant POST is free (the server upserts by fingerprint), uncertainty is not.
110
+ test('decideKeyRegistration registers on uncertainty and skips only on proof', () => {
111
+ const FP = 'SHA256:abc';
112
+ assert.deepEqual(lib.decideKeyRegistration({ created: true }), { register: true, why: 'created' });
113
+ assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [{ fingerprint: FP }] }).register, false);
114
+ assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [{ fingerprint: 'SHA256:other' }] }).register, true);
115
+ assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [] }).register, true);
116
+ // the two fail-open holes this closes:
117
+ assert.equal(lib.decideKeyRegistration({ fingerprint: null, listedKeys: [] }).register, true);
118
+ assert.equal(lib.decideKeyRegistration({ fingerprint: null, listedKeys: [{ fingerprint: FP }] }).register, true);
119
+ assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listFailed: true }).register, true);
120
+ assert.equal(lib.decideKeyRegistration({}).register, true, 'no information at all → register');
121
+ });
122
+
66
123
  test('buildRemoteShellCommand forces claude only on first run and always lands in /workspace', () => {
67
124
  const first = lib.buildRemoteShellCommand({ firstRun: true });
68
125
  const later = lib.buildRemoteShellCommand({ firstRun: false });
@@ -74,6 +131,26 @@ test('buildRemoteShellCommand forces claude only on first run and always lands i
74
131
  assert.match(later, /exec bash -l/);
75
132
  });
76
133
 
134
+ test('buildRemoteShellCommand re-scopes on connect ONLY with sync:true (BV1.R101)', () => {
135
+ const noSync = lib.buildRemoteShellCommand({ firstRun: false, sync: false });
136
+ const withSync = lib.buildRemoteShellCommand({ firstRun: false, sync: true });
137
+ assert.ok(!/box-sync/.test(noSync), 'default (sync:false) never re-scopes');
138
+ assert.match(withSync, /node scripts\/gds\/box-sync\.js \|\| true/);
139
+ // order: cd → re-scope → shell, so the builder lands in the scoped folder.
140
+ assert.ok(withSync.indexOf('cd /workspace') < withSync.indexOf('box-sync'), 'cd before sync');
141
+ assert.ok(withSync.indexOf('box-sync') < withSync.indexOf('exec bash -l'), 'sync before the shell');
142
+ // sync + firstRun: re-scope THEN the one-time claude /login handoff.
143
+ const both = lib.buildRemoteShellCommand({ firstRun: true, sync: true });
144
+ assert.ok(both.indexOf('box-sync') < both.indexOf('claude || true'), 'sync before the first-run login');
145
+ });
146
+
147
+ test('buildRemoteResyncCommand is a cd + re-scope one-shot (bongos code path) — no shell (BV1.R101)', () => {
148
+ const cmd = lib.buildRemoteResyncCommand();
149
+ assert.match(cmd, /cd \/workspace/);
150
+ assert.match(cmd, /node scripts\/gds\/box-sync\.js \|\| true/);
151
+ assert.ok(!/exec bash/.test(cmd), 'one-shot — never spawns a login shell');
152
+ });
153
+
77
154
  test('remoteAuthority builds a VS Code Remote-SSH authority', () => {
78
155
  assert.equal(lib.remoteAuthority(HOST), `ssh-remote+${HOST}`);
79
156
  });