@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
@@ -76,7 +76,7 @@ t('cloneSpecForScope: starter sparsePaths is a COPY (mutating it cannot poison t
76
76
  console.log('\nSTARTER_SPARSE_PATHS — the scoped surface (ADR 0052 §9.2):');
77
77
 
78
78
  t('starter surface includes the build tooling + game runtime', () => {
79
- for (const p of ['.claude', 'scripts', 'src/bongos', 'src/world', 'src/rooms', 'public', '.devcontainer', 'infra']) {
79
+ for (const p of ['.claude', 'scripts', 'src/bongos', 'src/world', 'src/rooms', 'public', '.devcontainer', 'infra', 'tests']) {
80
80
  assert.ok(access.STARTER_SPARSE_PATHS.includes(p), `starter scope must include ${p}`);
81
81
  }
82
82
  });
@@ -182,5 +182,134 @@ t('reconcileSourceAccess: inactivity outranks an otherwise in-sync full Metic bo
182
182
  assert.deepEqual(d, { action: 'deprovision', reason: 'builder-inactive' });
183
183
  });
184
184
 
185
+ // ===========================================================================
186
+ // TASK-SCOPED source access (ADR 0148 / goal 1000051, R93). BASE_SPARSE_PATHS,
187
+ // the pure sparsePathsForScopeKeys(scopeKeys, moduleScopeMap) cone-filter, and
188
+ // the decideSourceAccess three-way branch. Uses the REAL module-scope-map (a pure
189
+ // core module) as the map the route (R94) will pass in.
190
+ // ===========================================================================
191
+ const scopeMap = require('../src/bongos/module-scope-map.js');
192
+ const BASE = access.BASE_SPARSE_PATHS;
193
+
194
+ console.log('\nBASE_SPARSE_PATHS — the non-product base a task-scoped box layers on:');
195
+
196
+ t('BASE carries the methodology + tooling, NOT the product/game', () => {
197
+ for (const p of ['.devcontainer', '.claude', 'scripts', 'src/bongos', 'docs', 'infra', 'tests']) {
198
+ assert.ok(BASE.includes(p), `BASE must include ${p}`);
199
+ }
200
+ for (const p of ['src/world', 'src/rooms', 'public', 'art', 'modules/status-ui']) {
201
+ assert.ok(!BASE.includes(p), `BASE must NOT include the product surface ${p}`);
202
+ }
203
+ });
204
+
205
+ console.log('\nsparsePathsForScopeKeys — BASE ∪ the claims’ module DIRS (cone-mode):');
206
+
207
+ t('no keys (empty array) → BASE only, in declared order', () => {
208
+ assert.deepEqual(access.sparsePathsForScopeKeys([], scopeMap), [...BASE]);
209
+ });
210
+
211
+ t('one key → BASE + that module’s directory (status-ui → modules/status-ui)', () => {
212
+ const out = access.sparsePathsForScopeKeys(['status-ui'], scopeMap);
213
+ assert.deepEqual(out, [...BASE, 'modules/status-ui']);
214
+ });
215
+
216
+ t('cone-filter: dev-box keeps its DIRS, DROPS its file globs (scripts/gds/box.js)', () => {
217
+ const out = access.sparsePathsForScopeKeys(['dev-box'], scopeMap);
218
+ assert.ok(out.includes('modules/dev-box'), 'the module dir is kept');
219
+ assert.ok(out.includes('devbox-app'), 'the app dir is kept');
220
+ assert.ok(!out.some((p) => p.endsWith('.js') || p.endsWith('.sh')), 'no file globs survive the cone filter');
221
+ assert.ok(!out.includes('scripts/gds/box.js'), 'the box CLI file is dropped (BASE scripts covers it)');
222
+ });
223
+
224
+ t('cone-filter: the kernel key contributes only its real DIR globs (config, src/module-loader)', () => {
225
+ const out = access.sparsePathsForScopeKeys(['kernel'], scopeMap);
226
+ // kernel is mostly individual src/bongos/*.js FILES (dropped — BASE src/bongos
227
+ // covers them via cone ancestor-inclusion) + a few genuine DIR globs.
228
+ assert.ok(out.includes('config'), 'config/ is a real dir the task needs, not in BASE');
229
+ assert.ok(out.includes('src/module-loader'), 'src/module-loader/ is a real dir not in BASE');
230
+ assert.ok(!out.includes('src/bongos/db.js'), 'the kernel FILE globs are dropped');
231
+ assert.ok(!out.includes('server.js'), 'top-level files never enter the sparse set (cone auto-includes them)');
232
+ });
233
+
234
+ t('N keys → union, deduped + sorted; unknown keys contribute nothing', () => {
235
+ const out = access.sparsePathsForScopeKeys(['status-ui', 'hall-ui'], scopeMap);
236
+ assert.deepEqual(out, [...BASE, 'modules/hall-ui', 'modules/status-ui'], 'union of both dirs, sorted');
237
+ assert.deepEqual(access.sparsePathsForScopeKeys(['status-ui', 'status-ui'], scopeMap), [...BASE, 'modules/status-ui'], 'dedup');
238
+ assert.deepEqual(access.sparsePathsForScopeKeys(['bogus'], scopeMap), [...BASE], 'unknown key → no extra paths');
239
+ });
240
+
241
+ t('missing/!scopeForModule moduleScopeMap → BASE only (guards the missing map)', () => {
242
+ assert.deepEqual(access.sparsePathsForScopeKeys(['dev-box'], null), [...BASE]);
243
+ assert.deepEqual(access.sparsePathsForScopeKeys(['dev-box'], {}), [...BASE]);
244
+ });
245
+
246
+ console.log('\ndecideSourceAccess — the ADR 0148 three-way (rank still gates ALLOW/DENY):');
247
+
248
+ t('scopeKeys present + non-empty → TASK scope, BASE ∪ the module dirs (rank no longer caps breadth)', () => {
249
+ const d = access.decideSourceAccess({ rank: 'xenos', status: 'active', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
250
+ assert.equal(d.allowed, true);
251
+ assert.equal(d.scope, 'task');
252
+ assert.equal(d.spec.mode, 'sparse');
253
+ assert.deepEqual(d.spec.sparsePaths, [...BASE, 'modules/status-ui'], 'even a Xenos with a claim gets the claim’s module scope');
254
+ });
255
+
256
+ t('scopeKeys present + EMPTY → BASE-only (a live box with no active claim)', () => {
257
+ const d = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: [], moduleScopeMap: scopeMap });
258
+ assert.equal(d.allowed, true);
259
+ assert.equal(d.scope, 'base');
260
+ assert.deepEqual(d.spec.sparsePaths, [...BASE], 'idle Metic+ now gets BASE-only, not the whole repo (ADR 0148 flagged change)');
261
+ });
262
+
263
+ t('scopeKeys ABSENT → the pre-ADR-0148 RANK fallback is preserved EXACTLY (R93-before-R94 safety)', () => {
264
+ assert.equal(access.decideSourceAccess({ rank: 'metic', status: 'active' }).scope, 'full', 'no claim signal → rank fallback (full)');
265
+ assert.equal(access.decideSourceAccess({ rank: 'xenos', status: 'active' }).scope, 'starter', 'no claim signal → rank fallback (starter)');
266
+ });
267
+
268
+ t('the ALLOW/DENY gates run BEFORE scope selection', () => {
269
+ const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
270
+ assert.equal(inactive.allowed, false);
271
+ assert.equal(inactive.reason, 'BUILDER_INACTIVE');
272
+ const belowFloor = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
273
+ assert.equal(belowFloor.allowed, false);
274
+ assert.equal(belowFloor.reason, 'INSUFFICIENT_RANK');
275
+ });
276
+
277
+ console.log('\nscopeMetadata — the credential-FREE projection for GET /box/scope (BV1.R100):');
278
+
279
+ t('task scope → modules deduped+sorted, sparse_paths carried, NO credential fields', () => {
280
+ const scopeKeys = ['dev-box', 'onboarding', 'dev-box']; // dup on purpose
281
+ const decision = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys, moduleScopeMap: scopeMap });
282
+ const meta = access.scopeMetadata({ decision, scopeKeys });
283
+ assert.equal(meta.scope, 'task');
284
+ assert.equal(meta.mode, 'sparse');
285
+ assert.deepEqual(meta.modules, ['dev-box', 'onboarding'], 'deduped + sorted');
286
+ assert.ok(Array.isArray(meta.sparse_paths) && meta.sparse_paths.includes('modules/dev-box'));
287
+ assert.ok(!('credential' in meta) && !('repo_url' in meta), 'never carries credential fields');
288
+ });
289
+
290
+ t('empty claims → base scope, modules is [] (not null) — a live box with no active claim', () => {
291
+ const decision = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: [], moduleScopeMap: scopeMap });
292
+ const meta = access.scopeMetadata({ decision, scopeKeys: [] });
293
+ assert.equal(meta.scope, 'base');
294
+ assert.deepEqual(meta.modules, []);
295
+ assert.deepEqual(meta.sparse_paths, [...BASE]);
296
+ });
297
+
298
+ t('rank fallback (scopeKeys absent) → modules is null (breadth is rank-driven, not per-module)', () => {
299
+ const decision = access.decideSourceAccess({ rank: 'metic', status: 'active' });
300
+ const meta = access.scopeMetadata({ decision, scopeKeys: undefined });
301
+ assert.equal(meta.scope, 'full');
302
+ assert.equal(meta.mode, 'full');
303
+ assert.equal(meta.modules, null);
304
+ });
305
+
306
+ t('a DENIED decision projects to null (the route returns 403, never a scope body)', () => {
307
+ const denied = access.decideSourceAccess({ rank: 'xenos', status: 'inactive', scopeKeys: [], moduleScopeMap: scopeMap });
308
+ assert.equal(denied.allowed, false);
309
+ assert.equal(access.scopeMetadata({ decision: denied, scopeKeys: [] }), null);
310
+ assert.equal(access.scopeMetadata({}), null, 'no decision → null');
311
+ assert.equal(access.scopeMetadata(), null, 'no args → null');
312
+ });
313
+
185
314
  console.log(`\nbox_access.mjs: ${passed} passed, ${failed} failed`);
186
315
  process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,125 @@
1
+ // tests/box_credential.mjs
2
+ //
3
+ // R102 (goal 1000051, ADR 0148 C4) — VERIFY the server-side-token posture holds
4
+ // once source scope is CLAIM-based (R92–R94). The security property task-scoping
5
+ // must not weaken: the GitHub credential is server-issued per fetch from the prod
6
+ // ENV (never the repo — ADR 0022), the box never receives a fabricated token, and
7
+ // the claim-scoped ACCESS DECISION carries NO secret — the token is minted
8
+ // SEPARATELY, so changing what the box materializes (sparse paths) never touches
9
+ // the token model.
10
+ //
11
+ // Pure + hermetic: makeEnvCredentialProvider takes an injected env; decideSourceAccess
12
+ // is pure. NO env, NO network, NO box. The DEFERRED hardening (short-lived per-claim
13
+ // credential + hard "can't over-pull" isolation) is documented in ADR 0148 C4 /
14
+ // Consequences — this test pins the posture that ships TODAY.
15
+ //
16
+ // Run: node tests/box_credential.mjs
17
+
18
+ import { strict as assert } from 'node:assert';
19
+ import { createRequire } from 'node:module';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const cred = require('../modules/dev-box/box-credential.js');
23
+ const access = require('../modules/dev-box/box-access.js');
24
+ const scopeMap = require('../src/bongos/module-scope-map.js');
25
+
26
+ let passed = 0;
27
+ let failed = 0;
28
+ function t(name, fn) {
29
+ try { fn(); passed++; console.log(` PASS ${name}`); }
30
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
31
+ }
32
+
33
+ const REPO = 'https://github.com/acme/portable-core.git';
34
+ const STARTER_REPO = 'https://github.com/acme/starter-surface.git';
35
+
36
+ console.log('box-credential — the token is SERVER-ENV sourced, never fabricated (ADR 0022):');
37
+
38
+ t('no env configured → configured:false + token:null (the box gets guidance, not a token)', () => {
39
+ const p = cred.makeEnvCredentialProvider({});
40
+ for (const scope of ['full', 'starter']) {
41
+ const c = p.issue(scope, { nowMs: 0 });
42
+ assert.equal(c.configured, false, `${scope}: unconfigured`);
43
+ assert.equal(c.token, null, `${scope}: NO token is ever fabricated`);
44
+ assert.equal(c.repoUrl, null, `${scope}: no repo without env`);
45
+ assert.match(c.guidance || '', /env/i, `${scope}: guidance points at the prod env`);
46
+ }
47
+ });
48
+
49
+ t('repo set but token unset → still configured:false (no partial/empty-token leak)', () => {
50
+ const c = cred.makeEnvCredentialProvider({ BOX_SOURCE_REPO_URL: REPO }).issue('full', { nowMs: 0 });
51
+ assert.equal(c.configured, false);
52
+ assert.equal(c.token, null);
53
+ });
54
+
55
+ t('token comes STRAIGHT from the env var — and lands ONLY in .token, no other field', () => {
56
+ const p = cred.makeEnvCredentialProvider({
57
+ BOX_SOURCE_REPO_URL: REPO,
58
+ BOX_SOURCE_FULL_TOKEN: 'FULL-SECRET-abc',
59
+ BOX_SOURCE_STARTER_TOKEN: 'STARTER-SECRET-xyz',
60
+ });
61
+ const full = p.issue('full', { nowMs: 0 });
62
+ assert.equal(full.token, 'FULL-SECRET-abc', 'full scope → the FULL env token, verbatim');
63
+ const starter = p.issue('starter', { nowMs: 0 });
64
+ assert.equal(starter.token, 'STARTER-SECRET-xyz', 'starter scope → the STARTER env token, verbatim');
65
+ // The secret must appear in NO field other than `token` (scope/repoUrl/guidance/
66
+ // expiresAt/ttl are safe to log + audit — see the source_grant audit).
67
+ for (const c of [full, starter]) {
68
+ for (const [k, v] of Object.entries(c)) {
69
+ if (k === 'token') continue;
70
+ assert.ok(!String(v).includes(c.token), `secret must not leak into .${k}`);
71
+ }
72
+ }
73
+ });
74
+
75
+ console.log('\nbox-credential — the credential SCOPE is rank-based; scope can never over-reach the repo:');
76
+
77
+ t('a separate starter repo is a HARD boundary: only "full" reaches the full repo', () => {
78
+ const p = cred.makeEnvCredentialProvider({
79
+ BOX_SOURCE_REPO_URL: REPO,
80
+ BOX_SOURCE_STARTER_REPO_URL: STARTER_REPO,
81
+ BOX_SOURCE_FULL_TOKEN: 'F',
82
+ BOX_SOURCE_STARTER_TOKEN: 'S',
83
+ });
84
+ assert.equal(p.issue('full', { nowMs: 0 }).repoUrl, REPO, 'full → the full repo');
85
+ const starter = p.issue('starter', { nowMs: 0 });
86
+ assert.equal(starter.repoUrl, STARTER_REPO, 'starter → the SEPARATE starter repo, cannot reach the full one');
87
+ assert.equal(starter.separateStarterRepo, true);
88
+ });
89
+
90
+ t('fail-closed: an UNEXPECTED scope label ("task"/"base") never escalates to the full repo', () => {
91
+ // R94 passes only rank scopes ('starter'|'full') to issue(); this pins the safety
92
+ // margin if a task/base label ever reached it — it takes the NON-full (starter)
93
+ // branch, so on a separate-starter-repo instance it can't reach the crown jewels.
94
+ const p = cred.makeEnvCredentialProvider({
95
+ BOX_SOURCE_REPO_URL: REPO,
96
+ BOX_SOURCE_STARTER_REPO_URL: STARTER_REPO,
97
+ BOX_SOURCE_FULL_TOKEN: 'F',
98
+ BOX_SOURCE_STARTER_TOKEN: 'S',
99
+ });
100
+ for (const label of ['task', 'base', 'wizard', undefined]) {
101
+ const c = p.issue(label, { nowMs: 0 });
102
+ assert.equal(c.repoUrl, STARTER_REPO, `scope='${label}' must NOT reach the full repo`);
103
+ assert.notEqual(c.token, 'F', `scope='${label}' must NOT get the full-only token`);
104
+ }
105
+ });
106
+
107
+ console.log('\ntask-scoping decoupling — the claim-scoped ACCESS DECISION carries NO secret:');
108
+
109
+ t('decideSourceAccess returns scope/mode/sparsePaths only — the token is minted separately', () => {
110
+ const d = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
111
+ assert.equal(d.allowed, true);
112
+ assert.deepEqual(Object.keys(d.spec).sort(), ['mode', 'scope', 'sparsePaths'], 'the spec is scope/mode/sparsePaths — NO credential field');
113
+ const blob = JSON.stringify(d);
114
+ assert.ok(!/token|credential|secret|ghp_|github_pat/i.test(blob), 'the decision blob holds no secret-shaped field');
115
+ });
116
+
117
+ t('revocation scrubs at the credential layer: an inactive/below-floor builder is denied BEFORE any credential', () => {
118
+ const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
119
+ assert.equal(inactive.allowed, false, 'inactive → denied (the route never reaches issue())');
120
+ const belowFloor = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
121
+ assert.equal(belowFloor.allowed, false, 'below floor → denied (no credential path)');
122
+ });
123
+
124
+ console.log(`\nbox_credential.mjs: ${passed} passed, ${failed} failed`);
125
+ process.exit(failed === 0 ? 0 : 1);
@@ -99,4 +99,28 @@ const RSA = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/FqVQUVRAmcDHRBeEZZwuW0xgsAP
99
99
  assert.equal(r.error, 'expect_type_and_key_only');
100
100
  }
101
101
 
102
+ // 11) hostKeysAreStale (task 1002509) — a rebuild mints fresh host keys, and the row
103
+ // keeps the OLD ones until the new box re-reports. Serving those is the "Host denied
104
+ // (verification failed)" lockout, so a report that predates the current droplet's
105
+ // activation must be refused and the client must keyscan instead.
106
+ {
107
+ const { hostKeysAreStale } = require('../modules/dev-box/boxes.js');
108
+ assert.equal(
109
+ hostKeysAreStale({ hostKeysAt: '2026-07-01T08:00:00Z', activeSince: '2026-07-01T09:00:00Z' }),
110
+ true,
111
+ 'a report older than the activation describes a box that no longer exists'
112
+ );
113
+ assert.equal(
114
+ hostKeysAreStale({ hostKeysAt: '2026-07-01T10:00:00Z', activeSince: '2026-07-01T09:00:00Z' }),
115
+ false,
116
+ 'a report from after this box came up is the live identity'
117
+ );
118
+ assert.equal(hostKeysAreStale({ hostKeysAt: null, activeSince: '2026-07-01T09:00:00Z' }), true);
119
+ assert.equal(hostKeysAreStale({ hostKeysAt: '2026-07-01T10:00:00Z', activeSince: null }), true);
120
+ assert.equal(hostKeysAreStale({}), true, 'no evidence of freshness → not fresh');
121
+ assert.equal(hostKeysAreStale({ hostKeysAt: 'garbage', activeSince: '2026-07-01T09:00:00Z' }), true);
122
+ const t = new Date('2026-07-01T09:00:00Z');
123
+ assert.equal(hostKeysAreStale({ hostKeysAt: t, activeSince: t }), false, 'Date objects, equal instant → fresh');
124
+ }
125
+
102
126
  console.log('box_host_keys: all assertions passed');
@@ -0,0 +1,149 @@
1
+ // tests/box_infra_resolve.mjs
2
+ //
3
+ // R98 (goal 1000051/1000052, ADR 0150) — box.js locates the box cloud-init from
4
+ // the INSTANCE root (not the vendored @bongos/core package) and FAILS LOUD when it
5
+ // cannot, so a box can never be provisioned with empty user-data (= a bare box that
6
+ // boots with no clone, no toolchain, no terminal).
7
+ //
8
+ // Pure + hermetic: boxInfraCandidates takes injected {env, repoRoot}; readCloudInit
9
+ // takes an explicit infraDir. Temp dirs stand in for a real infra/ tree. NO DB, NO
10
+ // network, NO droplet — requiring box.js does not run main() (require.main guard)
11
+ // and loadDeps() (the DB layer) is only reached inside main().
12
+ //
13
+ // Run: node tests/box_infra_resolve.mjs
14
+
15
+ import { strict as assert } from 'node:assert';
16
+ import { createRequire } from 'node:module';
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import { makeRunner } from './helpers.mjs';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const box = require('../scripts/gds/box.js');
24
+
25
+ const { test, summary } = makeRunner();
26
+
27
+ // The 9 scripts readCloudInit gz+b64-inlines into the cloud-init — missing ANY of
28
+ // them half-wires the box. Kept in sync with box.js's fileGzB64 calls.
29
+ const INLINED = [
30
+ 'box-source-fetch.sh', 'box-authorized-keys-fetch.sh', 'box-terminal.service',
31
+ 'box-terminal-credsync.service', 'box-terminal-credential.path', 'cloudflared.service',
32
+ 'cloudflared-run.sh', 'close-box.sh', 'otb-box-launch.sh',
33
+ ];
34
+ const TEMPLATE = box.BOX_INFRA_TEMPLATE; // 'builder-box-cloud-init.yaml'
35
+
36
+ // A minimal template exercising a few of the substitution tokens (readCloudInit
37
+ // still calls fileGzB64 for ALL 9 inlined files regardless, so the happy path must
38
+ // provide every one).
39
+ const TEMPLATE_BODY = [
40
+ '#cloud-config',
41
+ 'write_files:',
42
+ ' - path: /opt/box-source-fetch.sh',
43
+ ' encoding: gz+b64',
44
+ ' content: __BOX_SOURCE_FETCH_B64__',
45
+ ' - path: /opt/cloudflared-run.sh',
46
+ ' encoding: gz+b64',
47
+ ' content: __CLOUDFLARED_RUN_B64__',
48
+ 'runcmd:',
49
+ ' - git clone __REPO_URL__ /workspace',
50
+ '',
51
+ ].join('\n');
52
+
53
+ const tmps = [];
54
+ function tmpdir() {
55
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'boxinfra-'));
56
+ tmps.push(d);
57
+ return d;
58
+ }
59
+ // Build an infra/ dir; choose whether the template + which inlined files exist.
60
+ function makeInfra({ template = true, inlined = INLINED } = {}) {
61
+ const root = tmpdir();
62
+ const infra = path.join(root, 'infra');
63
+ fs.mkdirSync(infra, { recursive: true });
64
+ if (template) fs.writeFileSync(path.join(infra, TEMPLATE), TEMPLATE_BODY);
65
+ for (const f of inlined) fs.writeFileSync(path.join(infra, f), `# ${f}\n`);
66
+ return { root, infra };
67
+ }
68
+
69
+ // ===========================================================================
70
+ // boxInfraCandidates — ordering (pure, injected; ADR 0150 prong A)
71
+ // ===========================================================================
72
+ await test('candidate order: override → explicit instance root → repoRoot → node_modules walk-up', () => {
73
+ const env = { CLOUDBONGOS_BOX_INFRA_DIR: '/ovr', CLOUDBONGOS_INSTANCE_ROOT: '/inst' };
74
+ const c = box.boxInfraCandidates({ env, repoRoot: '/srv/app/node_modules/@bongos/core' });
75
+ assert.equal(c[0], path.resolve('/ovr'), 'explicit override is first');
76
+ assert.equal(c[1], path.join('/inst', 'infra'), 'then the explicit instance root');
77
+ assert.equal(c[2], '/srv/app/node_modules/@bongos/core/infra', 'then REPO_ROOT/infra');
78
+ // the load-bearing case: a vendored core walks UP out of node_modules to its host
79
+ // instance root, so infra/ is found with no pointer at all.
80
+ assert.ok(c.includes('/srv/app/infra'), 'walk-up recovers the host instance root infra/');
81
+ });
82
+
83
+ await test('repoRoot/infra precedes its ancestors in the walk-up (robust to any cwd pointer)', () => {
84
+ const c = box.boxInfraCandidates({ env: {}, repoRoot: '/x/y/z' });
85
+ const i0 = c.indexOf('/x/y/z/infra');
86
+ const i1 = c.indexOf('/x/y/infra');
87
+ const i2 = c.indexOf('/x/infra');
88
+ assert.ok(i0 >= 0 && i1 > i0 && i2 > i1, 'repoRoot/infra, then /x/y/infra, then /x/infra — in order');
89
+ });
90
+
91
+ // ===========================================================================
92
+ // resolveBoxInfraDir — picks the FIRST candidate that actually holds the template
93
+ // ===========================================================================
94
+ await test('resolveBoxInfraDir picks the override dir when it holds the template', () => {
95
+ const { infra } = makeInfra();
96
+ const got = box.resolveBoxInfraDir({ env: { CLOUDBONGOS_BOX_INFRA_DIR: infra }, repoRoot: '/nope/does/not/exist' });
97
+ assert.equal(got, infra);
98
+ });
99
+
100
+ await test('resolveBoxInfraDir falls THROUGH a template-less override to the instance-root infra/', () => {
101
+ const emptyOverride = path.join(tmpdir(), 'infra');
102
+ fs.mkdirSync(emptyOverride, { recursive: true }); // exists, but no template inside
103
+ const { infra } = makeInfra(); // a real infra/ to fall through to
104
+ const instRoot = path.dirname(infra); // <root>, so <root>/infra === the real one
105
+ const got = box.resolveBoxInfraDir({
106
+ env: { CLOUDBONGOS_BOX_INFRA_DIR: emptyOverride, CLOUDBONGOS_INSTANCE_ROOT: instRoot },
107
+ repoRoot: '/nope',
108
+ });
109
+ assert.equal(got, infra, 'skips the template-less override, takes the instance-root infra/');
110
+ });
111
+
112
+ await test('resolveBoxInfraDir → null when NO candidate holds the template', () => {
113
+ const bare = tmpdir(); // a dir tree with no infra/ anywhere
114
+ const got = box.resolveBoxInfraDir({ env: {}, repoRoot: path.join(bare, 'node_modules', '@bongos', 'core') });
115
+ assert.equal(got, null);
116
+ });
117
+
118
+ // ===========================================================================
119
+ // readCloudInit — FAIL LOUD (ADR 0150 prong B): never return null / empty
120
+ // ===========================================================================
121
+ await test('readCloudInit THROWS when infra cannot be located (never returns null)', () => {
122
+ assert.throws(
123
+ () => box.readCloudInit({ infraDir: null }),
124
+ /not found|boot bare|ADR 0150/i,
125
+ );
126
+ });
127
+
128
+ await test('readCloudInit THROWS when infra is INCOMPLETE (an inlined script is missing)', () => {
129
+ const { infra } = makeInfra({ inlined: INLINED.filter((f) => f !== 'cloudflared-run.sh') });
130
+ assert.throws(
131
+ () => box.readCloudInit({ infraDir: infra }),
132
+ /incomplete|cloudflared-run\.sh|ADR 0150/i,
133
+ );
134
+ });
135
+
136
+ // ===========================================================================
137
+ // readCloudInit — happy path: renders non-empty + every placeholder substituted
138
+ // ===========================================================================
139
+ await test('readCloudInit renders a non-empty cloud-init with no leftover __PLACEHOLDER__ tokens', () => {
140
+ const { infra } = makeInfra();
141
+ const out = box.readCloudInit({
142
+ infraDir: infra, authorizedKeysBlock: 'ssh-ed25519 AAAA', boxEnv: 'X=1', gdsSession: '{"t":1}',
143
+ });
144
+ assert.ok(out && out.length > 0, 'renders a non-empty cloud-init');
145
+ assert.ok(!/__[A-Z0-9_]+__/.test(out), 'no unsubstituted __PLACEHOLDER__ tokens remain');
146
+ });
147
+
148
+ for (const d of tmps) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ } }
149
+ summary();
@@ -45,11 +45,15 @@ const t = async (name, fn) => {
45
45
  };
46
46
 
47
47
  // ---- allowBoxScope middleware ------------------------------------------------
48
- await t('allowBoxScope sets req.allowBoxScope = true and calls next()', async () => {
48
+ // BV1.R99: the opt-in is no longer a bare boolean flag — it DECLARES the box
49
+ // scope's own permission allow-list, and requireBuilder checks the declaration
50
+ // against the registry. The reachability contract below is unchanged.
51
+ await t('allowBoxScope declares the box scope\'s allow-list and calls next()', async () => {
49
52
  const req = {};
50
53
  let nexted = false;
51
54
  auth.allowBoxScope(req, fakeRes(), () => { nexted = true; });
52
- assert.equal(req.allowBoxScope, true);
55
+ assert.deepEqual([...req.sessionScopeNeeds], [...auth.sessionScopeFor('box').permissions]);
56
+ assert.ok(req.sessionScopeNeeds.length > 0, 'an empty declaration would deny every box route');
53
57
  assert.equal(nexted, true);
54
58
  });
55
59
 
@@ -0,0 +1,55 @@
1
+ // tests/box_sync.mjs
2
+ //
3
+ // BV1.R96 (goal 1000051) — "sync now": box-sync.js locates the box source-fetch
4
+ // script (so re-running it re-scopes the box to the active claim) and degrades
5
+ // cleanly off a box. Pure + hermetic: resolveFetchScript threads {env, repoRoot}
6
+ // to the shared resolveBoxInfraDir (ADR 0150 / R98); temp dirs stand in for a real
7
+ // box infra/ tree. NO DB, NO network, NO box.
8
+ //
9
+ // Run: node tests/box_sync.mjs
10
+
11
+ import { strict as assert } from 'node:assert';
12
+ import { createRequire } from 'node:module';
13
+ import fs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { makeRunner } from './helpers.mjs';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const boxSync = require('../scripts/gds/box-sync.js');
20
+
21
+ const { test, summary } = makeRunner();
22
+
23
+ const tmps = [];
24
+ function makeInfra({ template = true, fetch = true } = {}) {
25
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'boxsync-'));
26
+ tmps.push(root);
27
+ const infra = path.join(root, 'infra');
28
+ fs.mkdirSync(infra, { recursive: true });
29
+ // resolveBoxInfraDir validates an infra/ by the presence of the cloud-init template.
30
+ if (template) fs.writeFileSync(path.join(infra, 'builder-box-cloud-init.yaml'), '#cloud-config\n');
31
+ if (fetch) fs.writeFileSync(path.join(infra, boxSync.FETCH_SCRIPT), '#!/usr/bin/env bash\necho fetch\n');
32
+ return { root, infra };
33
+ }
34
+
35
+ await test('resolves the box source-fetch script when it is present on the box', () => {
36
+ const { infra } = makeInfra();
37
+ const got = boxSync.resolveFetchScript({ env: { CLOUDBONGOS_BOX_INFRA_DIR: infra }, repoRoot: '/nope' });
38
+ assert.equal(got, path.join(infra, boxSync.FETCH_SCRIPT));
39
+ });
40
+
41
+ await test('→ null when the infra/ has no source-fetch script (template present, fetch absent)', () => {
42
+ const { infra } = makeInfra({ fetch: false });
43
+ const got = boxSync.resolveFetchScript({ env: { CLOUDBONGOS_BOX_INFRA_DIR: infra }, repoRoot: '/nope' });
44
+ assert.equal(got, null);
45
+ });
46
+
47
+ await test('→ null when there is no infra/ at all (i.e. NOT on a box)', () => {
48
+ const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'boxsync-bare-'));
49
+ tmps.push(bare);
50
+ const got = boxSync.resolveFetchScript({ env: {}, repoRoot: path.join(bare, 'node_modules', '@bongos', 'core') });
51
+ assert.equal(got, null);
52
+ });
53
+
54
+ for (const d of tmps) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ } }
55
+ summary();
@@ -0,0 +1,99 @@
1
+ // tests/box_task_scope.mjs
2
+ //
3
+ // R95 (goal 1000051, ADR 0148) — the end-to-end PURE decision for task-scoped
4
+ // source access, composing the two halves shipped in R92 + R93:
5
+ // lifecycle.moduleScopeForActiveClaims (claims → tasks → goals.scope_modules)
6
+ // → box-access.decideSourceAccess({ scopeKeys, moduleScopeMap })
7
+ // Proves R95's done-when as named behavior: 0 active claims → BASE-only; N active
8
+ // claims → BASE ∪ the union of ALL N goals' module scopes.
9
+ //
10
+ // DB-free: moduleScopeForActiveClaims honours the #538 deps.pool seam
11
+ // (NODE_ENV==='test'), so a SQL-aware fake pool stands in for Postgres — we hand it
12
+ // the rows the DISTINCT-union query would return (the union itself is Postgres's
13
+ // job, shape-proven in module_scope_active_claims.mjs) and assert the WHOLE chain
14
+ // down to the sparse-checkout paths. The real module-scope-map (a pure core module)
15
+ // is the map the route passes in.
16
+ //
17
+ // Run: node tests/box_task_scope.mjs
18
+
19
+ import { strict as assert } from 'node:assert';
20
+ import { createRequire } from 'node:module';
21
+ import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
22
+
23
+ process.env.NODE_ENV = 'test';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const db = require('../modules/lifecycle/db.js');
27
+ const access = require('../modules/dev-box/box-access.js');
28
+ const scopeMap = require('../src/bongos/module-scope-map.js');
29
+
30
+ const { test, summary } = makeRunner();
31
+ const BASE = access.BASE_SPARSE_PATHS;
32
+
33
+ // Fake pool: the resolver issues ONE query (claims→tasks→goals, unnest
34
+ // scope_modules); return the module-key rows a builder's active claims would
35
+ // produce (already DISTINCT-unioned + ORDER BY m, as Postgres would).
36
+ function poolReturning(moduleKeyRows) {
37
+ const c = makeSqlAwareClient((s) =>
38
+ (/FROM claims c/.test(s) && /unnest\(g\.scope_modules\)/.test(s)) ? { rows: moduleKeyRows } : null);
39
+ return { query: (sql, params) => c._client.query(sql, params), connect: c.connect, queries: c._client.queries };
40
+ }
41
+
42
+ // The full chain a box-fetch runs: resolve the active-claim scope, then decide the
43
+ // source spec from it (rank still gates ALLOW/DENY).
44
+ async function specForClaims({ builderId = 3, rank = 'metic', status = 'active', moduleKeyRows }) {
45
+ const pool = poolReturning(moduleKeyRows);
46
+ const scopeKeys = await db.moduleScopeForActiveClaims(builderId, { pool });
47
+ return { scopeKeys, decision: access.decideSourceAccess({ rank, status, scopeKeys, moduleScopeMap: scopeMap }) };
48
+ }
49
+
50
+ // =========================================================================
51
+ // 0 active claims → BASE-only (the pre-existing Bongos instance, nothing extra)
52
+ // =========================================================================
53
+ await test('0 active claims → base-only spec', async () => {
54
+ const { scopeKeys, decision } = await specForClaims({ moduleKeyRows: [] });
55
+ assert.deepEqual(scopeKeys, [], 'no active claim → no scope keys');
56
+ assert.equal(decision.allowed, true);
57
+ assert.equal(decision.scope, 'base');
58
+ assert.deepEqual(decision.spec.sparsePaths, [...BASE], 'the box carries only BASE');
59
+ });
60
+
61
+ // =========================================================================
62
+ // N active claims across goals → BASE ∪ the UNION of all N goals' scopes
63
+ // =========================================================================
64
+ await test('N active claims → base ∪ the union of all their goals’ module scopes', async () => {
65
+ // Two claims: goal A scoped {dev-box, status-ui}, goal B scoped {hall-ui}.
66
+ // Postgres DISTINCT-unions + ORDER BY m to this key set:
67
+ const { scopeKeys, decision } = await specForClaims({
68
+ moduleKeyRows: [{ module_key: 'dev-box' }, { module_key: 'hall-ui' }, { module_key: 'status-ui' }],
69
+ });
70
+ assert.deepEqual(scopeKeys, ['dev-box', 'hall-ui', 'status-ui'], 'union across the active claims');
71
+ assert.equal(decision.scope, 'task');
72
+ // BASE ∪ each module's DIRECTORY globs (dev-box's box-CLI FILE globs cone-filtered out).
73
+ assert.deepEqual(
74
+ decision.spec.sparsePaths,
75
+ [...BASE, 'devbox-app', 'modules/dev-box', 'modules/hall-ui', 'modules/status-ui'],
76
+ 'every claimed goal’s module dirs are materialized, deduped + sorted',
77
+ );
78
+ assert.ok(!decision.spec.sparsePaths.includes('scripts/gds/box.js'), 'file globs never enter the sparse set');
79
+ });
80
+
81
+ // =========================================================================
82
+ // One claim, one goal → BASE ∪ that goal's scope (the single-claim base case)
83
+ // =========================================================================
84
+ await test('a single claim → base ∪ that one goal’s module scope', async () => {
85
+ const { decision } = await specForClaims({ moduleKeyRows: [{ module_key: 'status-ui' }] });
86
+ assert.equal(decision.scope, 'task');
87
+ assert.deepEqual(decision.spec.sparsePaths, [...BASE, 'modules/status-ui']);
88
+ });
89
+
90
+ // =========================================================================
91
+ // Rank/status STILL gate ALLOW/DENY ahead of any task scope (ADR 0148 C4 posture)
92
+ // =========================================================================
93
+ await test('an inactive builder is denied even with claims in scope', async () => {
94
+ const inactive = await specForClaims({ status: 'inactive', moduleKeyRows: [{ module_key: 'dev-box' }] });
95
+ assert.equal(inactive.decision.allowed, false);
96
+ assert.equal(inactive.decision.reason, 'BUILDER_INACTIVE');
97
+ });
98
+
99
+ summary();