@bongos/core 1.18.3 → 1.18.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.bongos-core.json +1576 -286
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +4 -2
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +6 -1
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/migrate.sh +99 -14
  347. package/scripts/status-mirror-sync.js +114 -84
  348. package/src/bongos/api-errors.js +1 -0
  349. package/src/bongos/auth.js +447 -102
  350. package/src/bongos/db-kernel.js +27 -0
  351. package/src/bongos/db.js +39 -0
  352. package/src/bongos/module-scope-map.js +27 -1
  353. package/src/bongos/permission-path-check.js +136 -102
  354. package/src/bongos/repo-info.js +47 -11
  355. package/src/bongos/route-rank-check.js +429 -46
  356. package/src/bongos/routes/_helpers.js +12 -1
  357. package/src/bongos/routes/auth.js +219 -2
  358. package/src/bongos/routes/backup.js +58 -2
  359. package/src/bongos/routes/me.js +35 -1
  360. package/src/bongos/serve-internal.js +84 -15
  361. package/src/branding.js +32 -0
  362. package/src/instance-config.js +30 -0
  363. package/src/module-api.js +124 -1
  364. package/src/platform-server.js +86 -9
  365. package/tests/abandon_task.mjs +59 -3
  366. package/tests/api_cli_flags.mjs +53 -0
  367. package/tests/auth_page_gate.mjs +13 -0
  368. package/tests/backfill_sessions.mjs +127 -1
  369. package/tests/backup_cli.mjs +54 -0
  370. package/tests/bongos_box_connect.mjs +77 -0
  371. package/tests/box_access.mjs +130 -1
  372. package/tests/box_credential.mjs +125 -0
  373. package/tests/box_host_keys.mjs +24 -0
  374. package/tests/box_infra_resolve.mjs +149 -0
  375. package/tests/box_scope_session.mjs +6 -2
  376. package/tests/box_sync.mjs +55 -0
  377. package/tests/box_task_scope.mjs +99 -0
  378. package/tests/branding.mjs +12 -0
  379. package/tests/cf_ruleset_preflight.mjs +142 -0
  380. package/tests/cf_tunnel.mjs +37 -0
  381. package/tests/claim_gate_rebase.mjs +98 -0
  382. package/tests/clawback_confirmed_backlog.mjs +103 -0
  383. package/tests/cli_lib_formatters.mjs +20 -0
  384. package/tests/confirm_attribution.mjs +30 -46
  385. package/tests/credit_grant.mjs +198 -10
  386. package/tests/criterion_authoring_gate.mjs +272 -0
  387. package/tests/criterion_progress.mjs +33 -1
  388. package/tests/dev_lib.mjs +253 -0
  389. package/tests/devbox_artifact_gate.mjs +75 -0
  390. package/tests/devbox_dmg_backdrop.mjs +137 -0
  391. package/tests/devbox_icons.mjs +304 -0
  392. package/tests/devbox_installer_brand.mjs +232 -0
  393. package/tests/devbox_key_register.mjs +234 -0
  394. package/tests/docs_openapi_route.mjs +110 -0
  395. package/tests/doctor_hooks.mjs +141 -0
  396. package/tests/extract_json.mjs +140 -0
  397. package/tests/fitness.mjs +89 -0
  398. package/tests/gen_freshness.mjs +96 -0
  399. package/tests/goal_advisory.mjs +251 -0
  400. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  401. package/tests/goal_routes.mjs +59 -4
  402. package/tests/goal_tier.mjs +12 -3
  403. package/tests/governance_catalog.mjs +129 -0
  404. package/tests/governance_claim_eligibility.mjs +262 -0
  405. package/tests/governance_config.mjs +187 -0
  406. package/tests/governance_ownership_scope.mjs +410 -0
  407. package/tests/governance_page_gates.mjs +324 -0
  408. package/tests/governance_parity.mjs +336 -0
  409. package/tests/governance_principals.mjs +232 -0
  410. package/tests/governance_protected_surfaces.mjs +208 -0
  411. package/tests/governance_require_permission.mjs +223 -0
  412. package/tests/governance_resolver.mjs +84 -0
  413. package/tests/governance_role_sync.mjs +231 -0
  414. package/tests/governance_seed.mjs +127 -0
  415. package/tests/governance_session_scope.mjs +434 -0
  416. package/tests/governance_vanilla_boot.mjs +203 -0
  417. package/tests/grade_attribution.mjs +7 -7
  418. package/tests/hall_audit.mjs +223 -0
  419. package/tests/hall_nav.mjs +191 -0
  420. package/tests/hall_palette.mjs +206 -0
  421. package/tests/hall_signout.mjs +124 -0
  422. package/tests/hook_user_agents.mjs +10 -0
  423. package/tests/idea_triage.mjs +162 -0
  424. package/tests/idp_device_exchange.mjs +93 -0
  425. package/tests/init.mjs +51 -0
  426. package/tests/instance_config.mjs +65 -0
  427. package/tests/instance_mismatch_guard.mjs +23 -6
  428. package/tests/instance_release_config.mjs +104 -0
  429. package/tests/linkify_refs.mjs +15 -2
  430. package/tests/migrate_root_resolution.mjs +124 -1
  431. package/tests/module_contributions.mjs +55 -0
  432. package/tests/module_loader.mjs +10 -5
  433. package/tests/module_scope_active_claims.mjs +94 -0
  434. package/tests/needs_rebase.mjs +92 -0
  435. package/tests/one_active_claim_per_session.mjs +76 -0
  436. package/tests/override_attribution.mjs +10 -8
  437. package/tests/override_request_ownership.mjs +8 -4
  438. package/tests/parse_cookie_decode.mjs +42 -0
  439. package/tests/pay_on_land.mjs +129 -0
  440. package/tests/platform_boot.mjs +100 -1
  441. package/tests/post_login_landing.mjs +42 -0
  442. package/tests/preflight_checks.mjs +72 -0
  443. package/tests/provision.mjs +132 -7
  444. package/tests/publish_branch_route.mjs +53 -0
  445. package/tests/publish_reconciler.mjs +187 -2
  446. package/tests/repo_info_binding.mjs +248 -0
  447. package/tests/reward_gate.mjs +11 -2
  448. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  449. package/tests/reward_mode_surfacing.mjs +168 -0
  450. package/tests/route_permission_check.mjs +301 -0
  451. package/tests/route_rank_check.mjs +254 -12
  452. package/tests/route_validation.mjs +30 -0
  453. package/tests/sequence.mjs +267 -0
  454. package/tests/session_digest.mjs +44 -0
  455. package/tests/session_index.mjs +18 -6
  456. package/tests/session_records.mjs +39 -1
  457. package/tests/session_reward_on_land.mjs +381 -0
  458. package/tests/session_reward_reconcile.mjs +229 -0
  459. package/tests/ship_api_client_pathspec.mjs +100 -0
  460. package/tests/ship_base_flag.mjs +93 -0
  461. package/tests/ship_ci_deploy.mjs +80 -3
  462. package/tests/ship_upstream_deletion.mjs +57 -0
  463. package/tests/smoke_target.mjs +233 -0
  464. package/tests/start_env_guard.mjs +87 -0
  465. package/tests/start_rebase_warning.mjs +103 -0
  466. package/tests/status_mirror_sync.mjs +122 -0
  467. package/tests/stop_refcheck.mjs +19 -1
  468. package/tests/task_goal_link.mjs +15 -6
  469. package/tests/upgrade.mjs +139 -0
  470. package/tests/web_admission_status.mjs +162 -0
  471. package/tests/write_validation_gaps.mjs +110 -0
@@ -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();
@@ -66,6 +66,15 @@ test('provisioningOrigin: optional, passes through config, env-overridable (task
66
66
  assert.equal(env.domains.provisioningOrigin, 'https://get.acme.example');
67
67
  });
68
68
 
69
+ test('landing.postLogin: config passes through, <PREFIX>_POST_LOGIN_LANDING overrides (task 1002240)', () => {
70
+ // Set in the instance pack, it survives the deep-merge.
71
+ const inst = resolveBranding({ neutral: NEUTRAL, instance: { ...SAMPLE, landing: { postLogin: '/gate/' } } });
72
+ assert.equal(inst.landing.postLogin, '/gate/');
73
+ // And the env override beats config (same precedence as the origins).
74
+ const env = resolveBranding({ neutral: NEUTRAL, instance: { ...SAMPLE, landing: { postLogin: '/builders' } }, env: { ACME_POST_LOGIN_LANDING: '/gate/' } });
75
+ assert.equal(env.landing.postLogin, '/gate/');
76
+ });
77
+
69
78
  test('missing required field throws', () => {
70
79
  assert.throws(() => resolveBranding({ neutral: { identity: { productName: 'X' } } }), /missing required field/);
71
80
  });
@@ -91,6 +100,9 @@ test('the committed neutral pack loads + resolves to the neutral identity (the t
91
100
  assert.equal(b.identity.productName, 'Cloud Bongos');
92
101
  assert.equal(b.identity.worldName, 'Cloud Bongos');
93
102
  assert.ok(b.domains.publicOrigin.startsWith('http://localhost'));
103
+ // The portable post-login landing default is the hall — never a /gate/ orbs
104
+ // site (which only a platform-home instance ships). Task 1002240.
105
+ assert.equal(b.landing.postLogin, '/builders');
94
106
  });
95
107
 
96
108
  test('resolveBranding does not freeze its input objects (no shared refs)', () => {
@@ -0,0 +1,142 @@
1
+ // tests/cf_ruleset_preflight.mjs — DB-free, network-free unit tests for the
2
+ // Cloudflare RULESET scope preflight (scripts/gds/cf-tunnel.js
3
+ // classifyRulesetScope + checkRulesetScope, task 1002511).
4
+ //
5
+ // WHY ITS OWN FILE: `cf_tunnel` is in run-unit-tests.js's INTEGRATION exclusion
6
+ // set, so anything added only to tests/cf_tunnel.mjs never runs in the default
7
+ // DB-free unit gate that /builder-ship smokes. This file is auto-discovered by
8
+ // that gate, so the preflight's behaviour is actually enforced on every ship.
9
+ //
10
+ // Run: node tests/cf_ruleset_preflight.mjs
11
+ import { test } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { createRequire } from 'node:module';
14
+ const require = createRequire(import.meta.url);
15
+ const cf = require('../scripts/gds/cf-tunnel.js');
16
+
17
+ const PHASE = 'http_request_late_transform';
18
+
19
+ // ── the constants the rule + the preflight share ────────────────────────────
20
+ test('the cookie-strip rule constants are exported for callers + this test', () => {
21
+ assert.equal(cf.COOKIE_STRIP_PHASE, PHASE);
22
+ assert.match(cf.COOKIE_STRIP_RULE_DESC, /Cookie/);
23
+ assert.match(cf.COOKIE_STRIP_EXPRESSION, /term-/);
24
+ assert.match(cf.COOKIE_STRIP_EXPRESSION, /sandbox-/);
25
+ });
26
+
27
+ // ── classifyRulesetScope: PURE, and a NEGATIVE detector only ────────────────
28
+ test('classifyRulesetScope: 403 → NOT readable + guidance naming the zone and Transform Rules', () => {
29
+ const r = cf.classifyRulesetScope({
30
+ status: 403,
31
+ body: { success: false, errors: [{ message: 'Actor does not have permission' }] },
32
+ phase: PHASE,
33
+ zone: 'cloudbongos.com',
34
+ });
35
+ assert.equal(r.readable, false);
36
+ assert.match(r.guidance, /Transform Rules/);
37
+ assert.match(r.guidance, /Cache Rules/);
38
+ assert.match(r.guidance, /cloudbongos\.com/);
39
+ assert.match(r.guidance, /API Tokens/i);
40
+ });
41
+
42
+ test('classifyRulesetScope: 401 → NOT readable (definitively no scope)', () => {
43
+ const r = cf.classifyRulesetScope({ status: 401, body: null, phase: PHASE, zone: 'cloudbongos.com' });
44
+ assert.equal(r.readable, false);
45
+ assert.match(r.reason, /401/);
46
+ });
47
+
48
+ test('classifyRulesetScope: 200 → readable, and NEVER claims edit scope is confirmed', () => {
49
+ const r = cf.classifyRulesetScope({
50
+ status: 200,
51
+ body: { success: true, result: { id: 'rs1', rules: [] } },
52
+ phase: PHASE,
53
+ zone: 'cloudbongos.com',
54
+ });
55
+ assert.equal(r.readable, true);
56
+ // HONESTY GATE: a GET proves READ, not EDIT. There must be no `editable` field,
57
+ // and the human-readable text must not read as a confirmed security control.
58
+ assert.equal('editable' in r, false, 'a read probe must never report an `editable` verdict');
59
+ assert.doesNotMatch(`${r.reason} ${r.guidance || ''}`, /scope confirmed|edit scope (is )?(confirmed|verified|present)/i);
60
+ assert.match(r.reason, /not proven|read/i, 'the reason must say edit scope is unproven');
61
+ });
62
+
63
+ test('classifyRulesetScope: 404 entrypoint → readable (an empty phase is legitimate)', () => {
64
+ const r = cf.classifyRulesetScope({
65
+ status: 404,
66
+ body: { success: false, errors: [{ message: 'ruleset not found' }] },
67
+ phase: PHASE,
68
+ zone: 'cloudbongos.com',
69
+ });
70
+ assert.equal(r.readable, true);
71
+ });
72
+
73
+ test('classifyRulesetScope: 500 → NOT readable, reported as inconclusive (never a pass)', () => {
74
+ const r = cf.classifyRulesetScope({ status: 500, body: null, phase: PHASE, zone: 'cloudbongos.com' });
75
+ assert.equal(r.readable, false);
76
+ assert.match(r.reason, /inconclusive|failed/i);
77
+ });
78
+
79
+ // ── checkRulesetScope: its OWN raw fetch, injectable, never a real call ─────
80
+ test('checkRulesetScope: probes the phase entrypoint (NOT /user/tokens/verify) and reports readable', async () => {
81
+ const calls = [];
82
+ const fetchImpl = async (url, opts = {}) => {
83
+ calls.push({ url, auth: opts.headers.Authorization });
84
+ if (url.includes('/zones?name=')) return { status: 200, json: async () => ({ success: true, result: [{ id: 'zoneA', name: 'cloudbongos.com' }] }) };
85
+ return { status: 200, json: async () => ({ success: true, result: { id: 'rs1', rules: [] } }) };
86
+ };
87
+ const r = await cf.checkRulesetScope({ zone: 'cloudbongos.com', phase: PHASE, token: 'tok_1', fetchImpl });
88
+ assert.equal(r.readable, true);
89
+ assert.equal(r.zone, 'cloudbongos.com');
90
+ const probe = calls[calls.length - 1];
91
+ assert.match(probe.url, new RegExp(`/zones/zoneA/rulesets/phases/${PHASE}/entrypoint$`));
92
+ assert.equal(probe.auth, 'Bearer tok_1');
93
+ // An IP-locked token returns "Invalid API Token" from verify (builder-box-lifecycle
94
+ // learning) — the preflight must probe the real operation instead.
95
+ assert.ok(!calls.some((c) => /tokens\/verify/.test(c.url)), 'must not use /user/tokens/verify');
96
+ });
97
+
98
+ test('checkRulesetScope: 403 on the entrypoint → NOT readable + actionable guidance', async () => {
99
+ const fetchImpl = async (url) => {
100
+ if (url.includes('/zones?name=')) return { status: 200, json: async () => ({ success: true, result: [{ id: 'zoneA', name: 'cloudbongos.com' }] }) };
101
+ return { status: 403, json: async () => ({ success: false, errors: [{ message: 'Actor does not have permission' }] }) };
102
+ };
103
+ const r = await cf.checkRulesetScope({ zone: 'cloudbongos.com', phase: PHASE, token: 'tok_1', fetchImpl });
104
+ assert.equal(r.readable, false);
105
+ assert.match(r.guidance, /Transform Rules/);
106
+ assert.match(r.guidance, /cloudbongos\.com/);
107
+ });
108
+
109
+ test('checkRulesetScope: a known zoneId skips the zone lookup (one call only)', async () => {
110
+ const calls = [];
111
+ const fetchImpl = async (url) => {
112
+ calls.push(url);
113
+ return { status: 200, json: async () => ({ success: true, result: { id: 'rs1', rules: [] } }) };
114
+ };
115
+ const r = await cf.checkRulesetScope({ zone: 'cloudbongos.com', phase: PHASE, token: 'tok_1', zoneId: 'zoneGiven', fetchImpl });
116
+ assert.equal(r.readable, true);
117
+ assert.equal(calls.length, 1);
118
+ assert.match(calls[0], /\/zones\/zoneGiven\/rulesets\//);
119
+ });
120
+
121
+ test('checkRulesetScope: no token → NOT readable, and ZERO network calls', async () => {
122
+ let called = false;
123
+ const fetchImpl = async () => { called = true; return { status: 200, json: async () => ({}) }; };
124
+ const r = await cf.checkRulesetScope({ zone: 'cloudbongos.com', phase: PHASE, token: '', fetchImpl });
125
+ assert.equal(r.readable, false);
126
+ assert.match(r.reason, /no CLOUDFLARE_API_TOKEN/);
127
+ assert.equal(called, false);
128
+ });
129
+
130
+ test('checkRulesetScope: a thrown fetch is caught → NOT readable, never throws', async () => {
131
+ const fetchImpl = async () => { throw new Error('ENETUNREACH'); };
132
+ const r = await cf.checkRulesetScope({ zone: 'cloudbongos.com', phase: PHASE, token: 'tok', fetchImpl });
133
+ assert.equal(r.readable, false);
134
+ assert.match(r.reason, /failed/i);
135
+ });
136
+
137
+ test('checkRulesetScope: the zone lookup rejecting the token → NOT readable', async () => {
138
+ const fetchImpl = async () => ({ status: 403, json: async () => ({ success: false, errors: [{ message: 'Invalid API Token' }] }) });
139
+ const r = await cf.checkRulesetScope({ zone: 'cloudbongos.com', phase: PHASE, token: 'tok', fetchImpl });
140
+ assert.equal(r.readable, false);
141
+ assert.match(r.guidance, /CLOUDFLARE_API_TOKEN/);
142
+ });
@@ -373,6 +373,10 @@ await ta('ensureCookieStripRule failure does NOT block ensureNamedTunnel (best-e
373
373
  const r = await cf.ensureNamedTunnel({ login: 'carol', zone: 'amazonprimea.com', terminalZone: 'amazonprimea.com' });
374
374
  assert.equal(r.token, 'tokZ', 'provisioning completes despite the cookie-strip rule failing');
375
375
  assert.equal(r.hostname, 'term-carol.amazonprimea.com');
376
+ // task 1002511: the failure is reported STRUCTURALLY, not only logged, so box.js
377
+ // can surface it in the provision summary instead of burying it mid-run.
378
+ assert.equal(r.edgeRules.cookieStrip.applied, false, 'a failed cookie-strip PUT must never report applied');
379
+ assert.ok(r.edgeRules.cookieStrip.error, 'the failure reason is captured');
376
380
  });
377
381
 
378
382
  console.log('edge cache bypass (#1065):');
@@ -446,6 +450,39 @@ await ta('ensureCacheBypassRule failure does NOT block ensureNamedTunnel (best-e
446
450
  const cf = new CfTunnelApi({ dryRun: false, token: TOKEN, accountId: ACCT, fetchImpl });
447
451
  const r = await cf.ensureNamedTunnel({ login: 'carol', zone: 'amazonprimea.com', terminalZone: 'amazonprimea.com' });
448
452
  assert.equal(r.token, 'tokZ', 'provisioning completes despite the cache rule failing');
453
+ assert.equal(r.edgeRules.cacheBypass.applied, false);
454
+ assert.ok(r.edgeRules.cacheBypass.error, 'the cache-rule failure reason is captured');
455
+ });
456
+
457
+ await ta('ensureNamedTunnel reports edgeRules.applied on the success path (both legs land)', async () => {
458
+ const RS = '/zones/zoneZ/rulesets/phases/http_request_late_transform/entrypoint';
459
+ const CS = '/zones/zoneZ/rulesets/phases/http_request_cache_settings/entrypoint';
460
+ const { fetchImpl } = makeFetch({
461
+ 'GET /accounts/acct123/cfd_tunnel': () => [],
462
+ 'POST /accounts/acct123/cfd_tunnel': ({ body }) => ({ id: 'tunZ', name: body.name }),
463
+ 'PUT /accounts/acct123/cfd_tunnel/tunZ/configurations': ({ body }) => ({ config: body.config }),
464
+ 'GET /accounts/acct123/cfd_tunnel/tunZ/token': () => 'tokZ',
465
+ 'GET /zones': () => [{ id: 'zoneZ' }],
466
+ 'GET /zones/zoneZ/dns_records': () => [],
467
+ 'POST /zones/zoneZ/dns_records': ({ body }) => ({ id: `rec-${body.name}`, ...body }),
468
+ [`GET ${RS}`]: () => ({ id: 'rs1', rules: [] }),
469
+ [`PUT ${RS}`]: ({ body }) => ({ id: 'rs1', rules: body.rules }),
470
+ [`GET ${CS}`]: () => ({ id: 'rs2', rules: [] }),
471
+ [`PUT ${CS}`]: ({ body }) => ({ id: 'rs2', rules: body.rules }),
472
+ });
473
+ const cf = new CfTunnelApi({ dryRun: false, token: TOKEN, accountId: ACCT, fetchImpl });
474
+ const r = await cf.ensureNamedTunnel({ login: 'carol', zone: 'amazonprimea.com', terminalZone: 'amazonprimea.com' });
475
+ assert.equal(r.edgeRules.cookieStrip.applied, true);
476
+ assert.equal(r.edgeRules.cookieStrip.error, null);
477
+ assert.equal(r.edgeRules.cacheBypass.applied, true);
478
+ assert.equal(r.token, 'tokZ', 'the bake values still come back');
479
+ });
480
+
481
+ await ta('ensureNamedTunnel in DRY-RUN reports applied:false (a plan never applied anything)', async () => {
482
+ const cf = new CfTunnelApi({ accountId: ACCT, fetchImpl: () => { throw new Error('should not fetch'); } });
483
+ const r = await cf.ensureNamedTunnel({ login: 'alice', zone: 'amazonprimea.com', terminalZone: 'amazonprimea.com' });
484
+ assert.equal(r.edgeRules.cookieStrip.applied, false, 'dry-run must not read as "the security rule is in place"');
485
+ assert.equal(r.edgeRules.cacheBypass.applied, false);
449
486
  });
450
487
 
451
488
  console.log('name helpers:');
@@ -0,0 +1,98 @@
1
+ // tests/claim_gate_rebase.mjs — ADR 0120 part 3 (real DB).
2
+ //
3
+ // The needs_rebase claim gate: a builder who owes a rebase on an un-landed
4
+ // confirmed task is refused NEW claims (409 REBASE_REQUIRED, naming the
5
+ // offending task) until they land or release it. claimTask runs a real
6
+ // BEGIN/FOR UPDATE transaction with many queries, so this is a real-DB
7
+ // integration test (self-skips without a reachable Postgres; also in the
8
+ // INTEGRATION set so the default DB-free lane never runs it).
9
+ //
10
+ // Run: DATABASE_URL=... node tests/claim_gate_rebase.mjs
11
+
12
+ import { createRequire } from 'node:module';
13
+ import { strict as assert } from 'node:assert';
14
+ const require = createRequire(import.meta.url);
15
+ const { Pool } = require('pg');
16
+
17
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
18
+ try {
19
+ await pool.query('SELECT 1');
20
+ } catch (e) {
21
+ console.log(`claim_gate_rebase.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
22
+ await pool.end().catch(() => {});
23
+ process.exit(0);
24
+ }
25
+
26
+ const seams = require('../src/module-seams.js');
27
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
28
+ const db = require('../modules/lifecycle/db.js');
29
+
30
+ const VER = 'ZZZ-CG', WORKER = 90000201, STALLED = 90000201, FRESH = 90000202;
31
+ const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
32
+
33
+ async function cleanup() {
34
+ await q(`DELETE FROM builder_achievements WHERE builder_id=$1`, [WORKER]).catch(() => {});
35
+ await q(`DELETE FROM credit_log WHERE builder_id=$1 OR task_id = ANY($2::bigint[])`, [WORKER, [STALLED, FRESH]]).catch(() => {});
36
+ await q(`DELETE FROM claims WHERE task_id = ANY($1::bigint[])`, [[STALLED, FRESH]]).catch(() => {});
37
+ await q(`DELETE FROM tasks WHERE id = ANY($1::bigint[])`, [[STALLED, FRESH]]).catch(() => {});
38
+ await q(`DELETE FROM builders WHERE id=$1`, [WORKER]).catch(() => {});
39
+ await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
40
+ }
41
+
42
+ let pass = 0;
43
+ const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
44
+
45
+ try {
46
+ await cleanup();
47
+ await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'cg','planning','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
48
+ // Archon so no rank/newcomer gate interferes; the gate under test is REBASE_REQUIRED.
49
+ await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'cg-w','cg-worker','CG Worker','archon')`, [WORKER]);
50
+ // STALLED: a confirmed task owned by WORKER, flagged needs_rebase.
51
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by) VALUES ($1,$2,'stalled','confirmed',10,'feature',$3)`, [STALLED, VER, WORKER]);
52
+ await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2, now())`, [STALLED, WORKER]);
53
+ await db.flagTaskNeedsRebase(String(STALLED), 'strand:no_branch_no_tip');
54
+ // FRESH: a ready, claimable task the worker will try to pick up.
55
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind) VALUES ($1,$2,'fresh','ready',10,'feature')`, [FRESH, VER]);
56
+
57
+ // 1) With STALLED flagged, claiming FRESH is refused with REBASE_REQUIRED.
58
+ let threw = null;
59
+ try {
60
+ await db.claimTask({ taskId: String(FRESH), builderId: String(WORKER), worktreeName: 'cg-fresh' });
61
+ } catch (e) { threw = e; }
62
+ ok(threw && threw.code === 'REBASE_REQUIRED', `claim refused with REBASE_REQUIRED (got ${threw && (threw.code || threw.message)})`);
63
+ ok(Array.isArray(threw.rebaseTasks) && threw.rebaseTasks.some((t) => Number(t.id) === STALLED),
64
+ 'the 409 names the offending stalled task');
65
+ // FRESH must be untouched (still ready, unclaimed).
66
+ const freshStatus = (await q(`SELECT status FROM tasks WHERE id=$1`, [FRESH]))[0].status;
67
+ ok(freshStatus === 'ready', 'the refused task stays ready (no partial claim)');
68
+
69
+ // ADR 0120 part 5: the SAME owed-rebase read, reached through the `lifecycle`
70
+ // kernel PORT — the exact wire the /me route resolves so the CLI (/builder-start)
71
+ // + the hall can surface the gate. Proves the port delegates to db and returns
72
+ // the fields those surfaces render.
73
+ const lifecycle = require('../modules/lifecycle/lifecycle.js');
74
+ const owedViaPort = await lifecycle.needsRebaseTasksForBuilder(String(WORKER));
75
+ ok(owedViaPort.some((t) => Number(t.id) === STALLED),
76
+ 'the lifecycle port surfaces the owed rebase (the /me → CLI/hall read path)');
77
+ ok(owedViaPort.every((t) => 'title' in t && 'needs_rebase_reason' in t && 'needs_rebase_at' in t),
78
+ 'each owed row carries the fields the CLI + hall render');
79
+
80
+ // 2) Landing the stalled task clears the flag → the gate opens.
81
+ await db.shipTask({ taskId: String(STALLED), builderId: String(WORKER) });
82
+ const stalledFlag = (await q(`SELECT needs_rebase FROM tasks WHERE id=$1`, [STALLED]))[0].needs_rebase;
83
+ ok(stalledFlag === false, 'shipping the stalled task cleared needs_rebase');
84
+
85
+ // 3) Now the claim succeeds.
86
+ const claim = await db.claimTask({ taskId: String(FRESH), builderId: String(WORKER), worktreeName: 'cg-fresh' });
87
+ ok(claim && claim.id && Number(claim.task_id) === FRESH, 'claim succeeds once the rebase chore is cleared');
88
+ const activeClaim = (await q(`SELECT builder_id FROM claims WHERE task_id=$1 AND released_at IS NULL`, [FRESH]))[0];
89
+ ok(activeClaim && Number(activeClaim.builder_id) === WORKER, 'the worker now holds an active claim on the task');
90
+
91
+ console.log(`\nclaim_gate_rebase (ADR 0120 part 3): ${pass} passed, 0 failed`);
92
+ } catch (e) {
93
+ console.error('claim_gate_rebase.mjs FAILED:', e.stack || e.message);
94
+ process.exitCode = 1;
95
+ } finally {
96
+ await cleanup();
97
+ await pool.end();
98
+ }
@@ -0,0 +1,103 @@
1
+ // tests/clawback_confirmed_backlog.mjs — ADR 0120 part 4 (real DB).
2
+ //
3
+ // Runs the ACTUAL migration SQL (migrations/core_194_confirmed_backlog_clawback.sql)
4
+ // against seeded fixtures and asserts the one-time clawback + re-gate:
5
+ // • each STILL-CONFIRMED task's task.confirmed credit is reversed (a negative
6
+ // task.confirmed_reversal row) and builders.total_credits nets back down,
7
+ // • those tasks are flagged needs_rebase='clawback:confirmed_backlog',
8
+ // • a SHIPPED task (legitimately landed) is left completely untouched,
9
+ // • the migration is idempotent (a second run books nothing new).
10
+ //
11
+ // Real-DB integration test: self-skips without a reachable Postgres; also in the
12
+ // INTEGRATION set so the default DB-free lane never runs it.
13
+ //
14
+ // Run: DATABASE_URL=... node tests/clawback_confirmed_backlog.mjs
15
+
16
+ import { createRequire } from 'node:module';
17
+ import { readFileSync } from 'node:fs';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { dirname, join } from 'node:path';
20
+ import { strict as assert } from 'node:assert';
21
+ const require = createRequire(import.meta.url);
22
+ const { Pool } = require('pg');
23
+
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ const MIGRATION_SQL = readFileSync(join(here, '..', 'migrations', 'core_194_confirmed_backlog_clawback.sql'), 'utf8');
26
+
27
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
28
+ try {
29
+ await pool.query('SELECT 1');
30
+ } catch (e) {
31
+ console.log(`clawback_confirmed_backlog.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
32
+ await pool.end().catch(() => {});
33
+ process.exit(0);
34
+ }
35
+
36
+ const VER = 'ZZZ-CB', A = 90000301, B = 90000302, T_A = 90000301, T_B = 90000302, T_S = 90000303;
37
+ const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
38
+ const total = async (id) => Number((await q(`SELECT total_credits FROM builders WHERE id=$1`, [id]))[0]?.total_credits ?? 0);
39
+ const reversals = async (taskId) => q(`SELECT delta FROM credit_log WHERE task_id=$1 AND reason='task.confirmed_reversal'`, [taskId]);
40
+ const flag = async (taskId) => (await q(`SELECT needs_rebase, needs_rebase_reason FROM tasks WHERE id=$1`, [taskId]))[0];
41
+
42
+ async function cleanup() {
43
+ await q(`DELETE FROM builder_achievements WHERE builder_id = ANY($1::bigint[])`, [[A, B]]).catch(() => {});
44
+ await q(`DELETE FROM credit_log WHERE builder_id = ANY($1::bigint[]) OR task_id = ANY($2::bigint[])`, [[A, B], [T_A, T_B, T_S]]).catch(() => {});
45
+ await q(`DELETE FROM tasks WHERE id = ANY($1::bigint[])`, [[T_A, T_B, T_S]]).catch(() => {});
46
+ await q(`DELETE FROM builders WHERE id = ANY($1::bigint[])`, [[A, B]]).catch(() => {});
47
+ await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
48
+ }
49
+
50
+ let pass = 0;
51
+ const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
52
+
53
+ try {
54
+ await cleanup();
55
+ await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'cb','planning','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
56
+ await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES
57
+ ($1,'cb-a','cb-a','CB A','thetes'), ($2,'cb-b','cb-b','CB B','thetes')`, [A, B]);
58
+ // Two still-confirmed tasks (unlanded) + one legitimately shipped task.
59
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by) VALUES
60
+ ($1,$4,'confirmed A','confirmed',20,'feature',$2),
61
+ ($5,$4,'confirmed B','confirmed',15,'feature',$3),
62
+ ($6,$4,'shipped legit','shipped',30,'feature',$2)`, [T_A, A, B, VER, T_B, T_S]);
63
+ // Seed the credits the OLD confirm-time rule would have paid (credit_log INSERT
64
+ // fires the total_credits trigger, exactly like a real award).
65
+ await q(`INSERT INTO credit_log (builder_id,task_id,delta,reason,description) VALUES
66
+ ($1,$2,20,'task.confirmed','seed A'),
67
+ ($3,$4,15,'task.confirmed','seed B'),
68
+ ($1,$5,30,'task.shipped','seed shipped-legit')`, [A, T_A, B, T_B, T_S]);
69
+
70
+ ok(await total(A) === 50, 'seed: builder A total_credits = 50 (20 confirmed + 30 shipped)');
71
+ ok(await total(B) === 15, 'seed: builder B total_credits = 15 (confirmed)');
72
+
73
+ // ---- run the ACTUAL migration ----
74
+ await pool.query(MIGRATION_SQL);
75
+
76
+ // 1. Reversals booked for the confirmed tasks, NOT the shipped one.
77
+ const rA = await reversals(T_A), rB = await reversals(T_B), rS = await reversals(T_S);
78
+ ok(rA.length === 1 && Number(rA[0].delta) === -20, 'confirmed task A: one -20 reversal');
79
+ ok(rB.length === 1 && Number(rB[0].delta) === -15, 'confirmed task B: one -15 reversal');
80
+ ok(rS.length === 0, 'the SHIPPED task is NOT clawed back (legitimately landed)');
81
+
82
+ // 2. total_credits netted back down by the reversed amounts.
83
+ ok(await total(A) === 30, 'builder A total_credits 50 → 30 (confirmed credit reversed, shipped kept)');
84
+ ok(await total(B) === 0, 'builder B total_credits 15 → 0');
85
+
86
+ // 3. Confirmed tasks re-gated; shipped task untouched.
87
+ ok((await flag(T_A)).needs_rebase === true && (await flag(T_A)).needs_rebase_reason === 'clawback:confirmed_backlog', 'task A flagged needs_rebase (clawback)');
88
+ ok((await flag(T_B)).needs_rebase === true, 'task B flagged needs_rebase');
89
+ ok((await flag(T_S)).needs_rebase === false, 'the shipped task is NOT flagged');
90
+
91
+ // 4. Idempotent — a second run books nothing new.
92
+ await pool.query(MIGRATION_SQL);
93
+ ok((await reversals(T_A)).length === 1, 'idempotent: still exactly one reversal for task A after a second run');
94
+ ok(await total(A) === 30, 'idempotent: builder A total_credits unchanged on re-run');
95
+
96
+ console.log(`\nclawback_confirmed_backlog (ADR 0120 part 4): ${pass} passed, 0 failed`);
97
+ } catch (e) {
98
+ console.error('clawback_confirmed_backlog.mjs FAILED:', e.stack || e.message);
99
+ process.exitCode = 1;
100
+ } finally {
101
+ await cleanup();
102
+ await pool.end();
103
+ }
@@ -46,6 +46,26 @@ test('fmtUsd: two-decimal dollar string', () => {
46
46
  assert.equal(cli.fmtUsd(1.239), '$1.24'); // rounds via toFixed
47
47
  });
48
48
 
49
+ // hallBase(base) — the builders'-hall root for a given API base (task 1002233).
50
+ // Pass an explicit base so the test never depends on the module-load env.
51
+ test('hallBase: derives the hall root from the base URL origin (path/query dropped)', () => {
52
+ assert.equal(cli.hallBase('https://cloudbongos.com'), 'https://cloudbongos.com/builders');
53
+ assert.equal(cli.hallBase('https://cloudbongos.com/'), 'https://cloudbongos.com/builders');
54
+ assert.equal(cli.hallBase('https://inst.example/api/gds'), 'https://inst.example/builders');
55
+ assert.equal(cli.hallBase('http://localhost:3000'), 'http://localhost:3000/builders');
56
+ });
57
+
58
+ test('hallBase: reflects whatever base it is given — no hardcoded founder host', () => {
59
+ const out = cli.hallBase('https://staging.example.org');
60
+ assert.equal(out, 'https://staging.example.org/builders');
61
+ assert.ok(!/amazonprimea/.test(out), 'must not reintroduce a hardcoded amazonprimea host');
62
+ });
63
+
64
+ test('hallBase: a non-URL base falls back to `${base}/builders` (never throws)', () => {
65
+ assert.equal(cli.hallBase('not a url'), 'not a url/builders');
66
+ assert.equal(cli.hallBase(''), '/builders');
67
+ });
68
+
49
69
  test('fmtUsd: null renders the em-dash placeholder (edge)', () => {
50
70
  assert.equal(cli.fmtUsd(null), '—');
51
71
  });