@bongos/core 1.18.3 → 1.18.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/.bongos-core.json +1575 -285
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +4 -2
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +6 -1
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/status-mirror-sync.js +114 -84
  347. package/src/bongos/api-errors.js +1 -0
  348. package/src/bongos/auth.js +447 -102
  349. package/src/bongos/db-kernel.js +27 -0
  350. package/src/bongos/db.js +39 -0
  351. package/src/bongos/module-scope-map.js +27 -1
  352. package/src/bongos/permission-path-check.js +136 -102
  353. package/src/bongos/repo-info.js +47 -11
  354. package/src/bongos/route-rank-check.js +429 -46
  355. package/src/bongos/routes/_helpers.js +12 -1
  356. package/src/bongos/routes/auth.js +219 -2
  357. package/src/bongos/routes/backup.js +58 -2
  358. package/src/bongos/routes/me.js +35 -1
  359. package/src/bongos/serve-internal.js +84 -15
  360. package/src/branding.js +32 -0
  361. package/src/instance-config.js +30 -0
  362. package/src/module-api.js +112 -1
  363. package/src/platform-server.js +86 -9
  364. package/tests/abandon_task.mjs +59 -3
  365. package/tests/api_cli_flags.mjs +53 -0
  366. package/tests/auth_page_gate.mjs +13 -0
  367. package/tests/backfill_sessions.mjs +127 -1
  368. package/tests/backup_cli.mjs +54 -0
  369. package/tests/bongos_box_connect.mjs +77 -0
  370. package/tests/box_access.mjs +130 -1
  371. package/tests/box_credential.mjs +125 -0
  372. package/tests/box_host_keys.mjs +24 -0
  373. package/tests/box_infra_resolve.mjs +149 -0
  374. package/tests/box_scope_session.mjs +6 -2
  375. package/tests/box_sync.mjs +55 -0
  376. package/tests/box_task_scope.mjs +99 -0
  377. package/tests/branding.mjs +12 -0
  378. package/tests/cf_ruleset_preflight.mjs +142 -0
  379. package/tests/cf_tunnel.mjs +37 -0
  380. package/tests/claim_gate_rebase.mjs +98 -0
  381. package/tests/clawback_confirmed_backlog.mjs +103 -0
  382. package/tests/cli_lib_formatters.mjs +20 -0
  383. package/tests/confirm_attribution.mjs +30 -46
  384. package/tests/credit_grant.mjs +198 -10
  385. package/tests/criterion_authoring_gate.mjs +272 -0
  386. package/tests/criterion_progress.mjs +33 -1
  387. package/tests/dev_lib.mjs +253 -0
  388. package/tests/devbox_artifact_gate.mjs +75 -0
  389. package/tests/devbox_dmg_backdrop.mjs +137 -0
  390. package/tests/devbox_icons.mjs +304 -0
  391. package/tests/devbox_installer_brand.mjs +232 -0
  392. package/tests/devbox_key_register.mjs +234 -0
  393. package/tests/docs_openapi_route.mjs +110 -0
  394. package/tests/doctor_hooks.mjs +141 -0
  395. package/tests/extract_json.mjs +140 -0
  396. package/tests/fitness.mjs +89 -0
  397. package/tests/gen_freshness.mjs +96 -0
  398. package/tests/goal_advisory.mjs +251 -0
  399. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  400. package/tests/goal_routes.mjs +59 -4
  401. package/tests/goal_tier.mjs +12 -3
  402. package/tests/governance_catalog.mjs +129 -0
  403. package/tests/governance_claim_eligibility.mjs +262 -0
  404. package/tests/governance_config.mjs +187 -0
  405. package/tests/governance_ownership_scope.mjs +410 -0
  406. package/tests/governance_page_gates.mjs +324 -0
  407. package/tests/governance_parity.mjs +336 -0
  408. package/tests/governance_principals.mjs +232 -0
  409. package/tests/governance_protected_surfaces.mjs +208 -0
  410. package/tests/governance_require_permission.mjs +223 -0
  411. package/tests/governance_resolver.mjs +84 -0
  412. package/tests/governance_role_sync.mjs +231 -0
  413. package/tests/governance_seed.mjs +127 -0
  414. package/tests/governance_session_scope.mjs +434 -0
  415. package/tests/governance_vanilla_boot.mjs +203 -0
  416. package/tests/grade_attribution.mjs +7 -7
  417. package/tests/hall_audit.mjs +223 -0
  418. package/tests/hall_nav.mjs +191 -0
  419. package/tests/hall_palette.mjs +206 -0
  420. package/tests/hall_signout.mjs +124 -0
  421. package/tests/hook_user_agents.mjs +10 -0
  422. package/tests/idea_triage.mjs +162 -0
  423. package/tests/idp_device_exchange.mjs +93 -0
  424. package/tests/init.mjs +51 -0
  425. package/tests/instance_config.mjs +65 -0
  426. package/tests/instance_mismatch_guard.mjs +23 -6
  427. package/tests/instance_release_config.mjs +104 -0
  428. package/tests/linkify_refs.mjs +15 -2
  429. package/tests/module_contributions.mjs +55 -0
  430. package/tests/module_loader.mjs +10 -5
  431. package/tests/module_scope_active_claims.mjs +94 -0
  432. package/tests/needs_rebase.mjs +92 -0
  433. package/tests/one_active_claim_per_session.mjs +76 -0
  434. package/tests/override_attribution.mjs +10 -8
  435. package/tests/override_request_ownership.mjs +8 -4
  436. package/tests/parse_cookie_decode.mjs +42 -0
  437. package/tests/pay_on_land.mjs +129 -0
  438. package/tests/platform_boot.mjs +100 -1
  439. package/tests/post_login_landing.mjs +42 -0
  440. package/tests/preflight_checks.mjs +72 -0
  441. package/tests/provision.mjs +132 -7
  442. package/tests/publish_branch_route.mjs +53 -0
  443. package/tests/publish_reconciler.mjs +187 -2
  444. package/tests/repo_info_binding.mjs +248 -0
  445. package/tests/reward_gate.mjs +11 -2
  446. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  447. package/tests/reward_mode_surfacing.mjs +168 -0
  448. package/tests/route_permission_check.mjs +301 -0
  449. package/tests/route_rank_check.mjs +254 -12
  450. package/tests/route_validation.mjs +30 -0
  451. package/tests/sequence.mjs +267 -0
  452. package/tests/session_digest.mjs +44 -0
  453. package/tests/session_index.mjs +18 -6
  454. package/tests/session_records.mjs +39 -1
  455. package/tests/session_reward_on_land.mjs +381 -0
  456. package/tests/session_reward_reconcile.mjs +229 -0
  457. package/tests/ship_api_client_pathspec.mjs +100 -0
  458. package/tests/ship_base_flag.mjs +93 -0
  459. package/tests/ship_ci_deploy.mjs +80 -3
  460. package/tests/ship_upstream_deletion.mjs +57 -0
  461. package/tests/smoke_target.mjs +233 -0
  462. package/tests/start_env_guard.mjs +87 -0
  463. package/tests/start_rebase_warning.mjs +103 -0
  464. package/tests/status_mirror_sync.mjs +122 -0
  465. package/tests/stop_refcheck.mjs +19 -1
  466. package/tests/task_goal_link.mjs +15 -6
  467. package/tests/upgrade.mjs +139 -0
  468. package/tests/web_admission_status.mjs +162 -0
  469. package/tests/write_validation_gaps.mjs +110 -0
@@ -0,0 +1,104 @@
1
+ // tests/instance_release_config.mjs
2
+ //
3
+ // serve-internal mounts /downloads/devbox/* and /downloads/bongos/* only when the
4
+ // instance ships the matching RELEASE config (config/devbox-app.json,
5
+ // config/bongos-cli.json). Those configs are boundary-EXCLUDED from the packaged core,
6
+ // so they live at the INSTANCE root — which on a vendored instance (@bongos/core in
7
+ // node_modules, ADR 0108) is a DIFFERENT directory from the core package dir
8
+ // serve-internal.js sits in. The old core-relative `require('../../config/…')` looked
9
+ // only in node_modules/@bongos/core/config/, so a vendored instance's Dev Box download
10
+ // routes never mounted (task 1002371; ADR 0145 papered over it by copying the config
11
+ // into node_modules — lost on the next npm ci). Same class as ADR 0144's vendored
12
+ // infra/ path bug.
13
+ //
14
+ // The first test is THE regression: a config present ONLY at the instance root must be
15
+ // found. It fails against the old resolution (which could not see the instance root at
16
+ // all) and passes against the instance-root-first ladder.
17
+ //
18
+ // serve-internal is DB-free (the pg pool connects lazily), so the end-to-end case mounts
19
+ // it on a throwaway express app over loopback — same pattern as tests/docs_openapi_route.mjs.
20
+ //
21
+ // Run: node --test tests/instance_release_config.mjs
22
+
23
+ import { test } from 'node:test';
24
+ import assert from 'node:assert/strict';
25
+ import { createRequire } from 'node:module';
26
+ import fs from 'node:fs';
27
+ import os from 'node:os';
28
+ import path from 'node:path';
29
+
30
+ const require = createRequire(import.meta.url);
31
+
32
+ // A minimal but VALID release config — buildDevboxDownloads rejects a bad
33
+ // releases_repo/version at boot, so the end-to-end mount below proves real parsing.
34
+ const DEVBOX_CFG = {
35
+ releases_repo: 'example-owner/example-devbox',
36
+ version: '9.9.9',
37
+ assets: { 'mac-arm64': 'Example-Dev-Box-9.9.9-arm64.dmg' },
38
+ aliases: { mac: 'mac-arm64' },
39
+ };
40
+
41
+ // Write config/<name> under a fresh temp dir and return the dir (a stand-in root).
42
+ function makeRoot(name, contents) {
43
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-root-'));
44
+ if (name) {
45
+ fs.mkdirSync(path.join(dir, 'config'), { recursive: true });
46
+ fs.writeFileSync(path.join(dir, 'config', name), contents);
47
+ }
48
+ return dir;
49
+ }
50
+
51
+ // Point the process at a temp INSTANCE root BEFORE serve-internal loads, so the default
52
+ // (uninjected) resolution runs the vendored shape: config at the instance root, absent
53
+ // from the core package dir. This checkout ships no config/devbox-app.json — it is
54
+ // instance-only — so the core tier genuinely has nothing to find, exactly like a
55
+ // freshly-vendored box.
56
+ const INSTANCE_ROOT = makeRoot('devbox-app.json', JSON.stringify(DEVBOX_CFG));
57
+ process.env.CLOUDBONGOS_INSTANCE_ROOT = INSTANCE_ROOT;
58
+ const { tryLoadInstanceConfig, mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
59
+
60
+ test('finds a release config that exists ONLY at the instance root (the vendored bug)', () => {
61
+ const instance = makeRoot('devbox-app.json', '{"version":"1.0.0"}');
62
+ const core = makeRoot(null);
63
+ assert.deepEqual(tryLoadInstanceConfig('devbox-app.json', [instance, core]), { version: '1.0.0' });
64
+ });
65
+
66
+ test('the instance root wins over the core package dir when both carry a copy', () => {
67
+ const instance = makeRoot('devbox-app.json', '{"from":"instance"}');
68
+ const core = makeRoot('devbox-app.json', '{"from":"core"}');
69
+ assert.equal(tryLoadInstanceConfig('devbox-app.json', [instance, core]).from, 'instance');
70
+ });
71
+
72
+ test('falls back to the core package dir (single checkout + the copied-in workaround)', () => {
73
+ const instance = makeRoot(null);
74
+ const core = makeRoot('bongos-cli.json', '{"from":"core"}');
75
+ assert.equal(tryLoadInstanceConfig('bongos-cli.json', [instance, core]).from, 'core');
76
+ });
77
+
78
+ test('absent from every root → null, so the download surface just does not mount', () => {
79
+ assert.equal(tryLoadInstanceConfig('devbox-app.json', [makeRoot(null), makeRoot(null)]), null);
80
+ assert.equal(tryLoadInstanceConfig('devbox-app.json', []), null);
81
+ });
82
+
83
+ test('a MALFORMED config still throws — only a MISSING file is tolerated', () => {
84
+ const bad = makeRoot('devbox-app.json', '{ this is not valid json ');
85
+ assert.throws(() => tryLoadInstanceConfig('devbox-app.json', [bad]), SyntaxError);
86
+ });
87
+
88
+ test('/downloads/devbox/version mounts from an INSTANCE-root config (end to end)', async () => {
89
+ const express = require('express');
90
+ const app = express();
91
+ mountInternalSurfaces(app);
92
+ const server = await new Promise((resolve) => {
93
+ const s = app.listen(0, () => resolve(s));
94
+ });
95
+ try {
96
+ const res = await fetch(`http://127.0.0.1:${server.address().port}/downloads/devbox/version`);
97
+ assert.equal(res.status, 200, 'the route must mount when only the INSTANCE root has the config');
98
+ const body = await res.json();
99
+ assert.equal(body.version, DEVBOX_CFG.version);
100
+ assert.match(body.assets['mac-arm64'], /^https:\/\/github\.com\/example-owner\/example-devbox\//);
101
+ } finally {
102
+ await new Promise((r) => server.close(r));
103
+ }
104
+ });
@@ -252,12 +252,25 @@ t('a known idea link is never unwrapped by the past-mislink pass', () => {
252
252
  assert.equal(runIds(s, ids), s);
253
253
  });
254
254
 
255
- t('discoverDocs finds CLAUDE.md + session logs, only .md, skips the generated ideas file', () => {
255
+ t('discoverDocs finds CLAUDE.md, recurses docs/ subdirs (only .md), skips the generated ideas file', () => {
256
256
  const docs = discoverDocs();
257
257
  assert.ok(docs.includes('CLAUDE.md'), 'includes CLAUDE.md');
258
- assert.ok(docs.some((d) => d.startsWith('docs/session-logs/')), 'includes session logs');
259
258
  assert.ok(docs.every((d) => d.endsWith('.md')), 'only markdown files');
260
259
  assert.ok(!docs.includes('generative-ideas.md'), 'excludes the generated ideas file');
260
+ // Recurses into docs/ subdirectories. docs/adr/*.md is the always-present nested
261
+ // case; assert recursion against it (task 1002234). Session logs are the canonical
262
+ // nested example the linkifier targets, but the neutral core carries none (they're
263
+ // instance-specific / not committed), so that stricter check is conditional — it
264
+ // still verifies them on an instance whose docs/session-logs/ is populated.
265
+ assert.ok(docs.some((d) => d.startsWith('docs/') && d.split('/').length >= 3),
266
+ 'recurses into docs/ subdirectories (e.g. docs/adr/*.md)');
267
+ const fs = require('node:fs');
268
+ const path = require('node:path');
269
+ const logsDir = path.resolve(process.cwd(), 'docs/session-logs');
270
+ const hasSessionLogMd = fs.existsSync(logsDir) && fs.readdirSync(logsDir).some((f) => f.endsWith('.md'));
271
+ if (hasSessionLogMd) {
272
+ assert.ok(docs.some((d) => d.startsWith('docs/session-logs/')), 'includes session logs when present');
273
+ }
261
274
  });
262
275
 
263
276
  console.log(`\nlinkify_refs: ${passed} passed, ${failed} failed`);
@@ -146,3 +146,58 @@ test('settings-panel seam: ABSENT → empty; REGISTERED → present, sorted by o
146
146
  assert.throws(() => OTB.contributeSettingsPanel({ id: 'x' }), /requires id \+ mount/);
147
147
  delete global.window;
148
148
  });
149
+
150
+ // --- hall-widget placement governance (task 2177) ----------------------------
151
+ // Home grew unbounded once; contributeHallWidget now refuses the registrations
152
+ // that let it. Rejections are console.error + null (NOT a throw) so one bad
153
+ // third-party widget can't blank the hall — so these assert the RETURN VALUE and
154
+ // that the widget never reached the registry.
155
+
156
+ test('hall-widget registry: placement rules reject bad registrations, accept good ones', () => {
157
+ global.window = {};
158
+ // The settings-panel test above already required dom-utils.js, and the IIFE
159
+ // assigns window.OTB exactly once at load — so re-requiring a cached module
160
+ // would leave window.OTB undefined. Evict it to get a fresh registry.
161
+ const domUtils = path.join(ROOT, 'modules', 'hall-ui', 'public', 'dom-utils.js');
162
+ delete require.cache[require.resolve(domUtils)];
163
+ require(domUtils);
164
+ const OTB = global.window.OTB;
165
+
166
+ const errors = [];
167
+ const realError = console.error;
168
+ console.error = (msg) => errors.push(String(msg));
169
+ const reg = (o) => OTB.contributeHallWidget({ label: 'L', mount() {}, ...o });
170
+
171
+ try {
172
+ // the structural contract still THROWS (first-party programmer error)
173
+ assert.throws(() => reg({ label: 'no id' }), /requires id \+ label \+ mount/);
174
+
175
+ // unknown zone / unknown view — a typo used to mount the widget on Home
176
+ // silently, which read as "my widget vanished"
177
+ assert.equal(reg({ id: 'a', view: 'home', zone: 'home-sidebar' }), null);
178
+ assert.match(errors.at(-1), /unknown zone "home-sidebar"/);
179
+ assert.equal(reg({ id: 'b', view: 'dashboard', zone: 'home-main', summary: true }), null);
180
+ assert.match(errors.at(-1), /unknown view "dashboard"/);
181
+
182
+ // a Home widget must NAME its zone — there is no home-main fallback any more
183
+ assert.equal(reg({ id: 'c' }), null);
184
+ assert.match(errors.at(-1), /must declare a zone/);
185
+
186
+ // a non-core home-main widget must be summary or collapsed
187
+ assert.equal(reg({ id: 'd', view: 'home', zone: 'home-main' }), null);
188
+ assert.match(errors.at(-1), /summary:true .* or collapsedByDefault:true/);
189
+
190
+ // …and the escapes from that rule work
191
+ assert.ok(reg({ id: 'e', view: 'home', zone: 'home-main', summary: true }));
192
+ assert.ok(reg({ id: 'f', view: 'home', zone: 'home-main', collapsedByDefault: true }));
193
+ assert.ok(reg({ id: 'g', view: 'home', zone: 'home-main', core: true }));
194
+ // a non-Home view needs no zone (Standing widgets carry `group` instead)
195
+ assert.ok(reg({ id: 'h', view: 'standing', group: 'work' }));
196
+
197
+ // only the accepted four reached the registry
198
+ assert.deepEqual(OTB.hallWidgetIds(), ['e', 'f', 'g', 'h']);
199
+ } finally {
200
+ console.error = realError;
201
+ delete global.window;
202
+ }
203
+ });
@@ -168,13 +168,18 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
168
168
  // onboarding (R81) + sessions (R82) + autonomy (R83 — conductor/architect/gate
169
169
  // + the routine-timers poller) are the tranche-2 core-domain carves (ADR 0093),
170
170
  // then status-ui (R84) + hall-ui (R85) the two web-surface carves; and
171
- // provisioning (ADR 0111 / task 1942) — a net-new feature module (instance hosting).
172
- // All are discovered; none has validation errors or coreVersion mismatches.
173
- // Membership stays byte-identical via src/modules.js (each leaves MODULE_REGISTRY
174
- // and reappears here as discovered). Order is the readdir sort.
171
+ // provisioning (ADR 0111 / task 1942) — a net-new feature module (instance hosting);
172
+ // platform-identity (ADR 0141) the hub IdP; and governance (ADR 0151 / BV1.R94) the
173
+ // configurable roles/permissions module. All are discovered; none has validation
174
+ // errors or coreVersion mismatches. Order is the readdir sort.
175
+ //
176
+ // NB this is the CORE package tree: game / art-pipeline / character-anim / demo are
177
+ // instance/content-excluded from the published core (ADR 0098) so they are not
178
+ // present here — the discovered set is the 17 always-shipped core-domain + feature
179
+ // modules below.
175
180
  loader._reset();
176
181
  const snap = loader.loaderSnapshot();
177
- assert.deepEqual(snap.loaded, ['art-pipeline', 'autonomy', 'builder-settings', 'character-anim', 'demo', 'dev-box', 'discord', 'economy', 'game', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'provisioning', 'security', 'sessions', 'status-ui']);
182
+ assert.deepEqual(snap.loaded, ['autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'governance', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
178
183
  assert.deepEqual(snap.errors, []);
179
184
  assert.deepEqual(snap.skipped, []);
180
185
  loader._reset();
@@ -0,0 +1,94 @@
1
+ // tests/module_scope_active_claims.mjs
2
+ //
3
+ // Unit guard for db.moduleScopeForActiveClaims (goal 1000051 / R92, ADR 0148 §2) —
4
+ // the lifecycle-port resolver the dev-box source-access path (R93/R94) reads to
5
+ // derive a builder's TASK-scoped sparse-checkout: the union of module KEYS across
6
+ // the builder's active claims' goals (claims → tasks → goals.scope_modules).
7
+ //
8
+ // DB-free, same posture as goal_achievement.mjs: the function honours the #538
9
+ // deps.pool injection seam (ONLY when NODE_ENV==='test'), so a SQL-aware fake
10
+ // client drives it with no live Postgres. We assert the query SHAPE (joins
11
+ // claims→tasks→goals, unnests scope_modules, filters ACTIVE claims), the bound
12
+ // params, the rows→keys mapping, and the numeric-input guard. The actual
13
+ // set-union/dedup is Postgres's DISTINCT (exercised end-to-end once box-access is
14
+ // wired — R95); here we prove the port method's contract.
15
+ //
16
+ // Run: node tests/module_scope_active_claims.mjs
17
+
18
+ import { strict as assert } from 'node:assert';
19
+ import { createRequire } from 'node:module';
20
+ import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
21
+
22
+ process.env.NODE_ENV = 'test';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const { moduleScopeForActiveClaims } = require('../modules/lifecycle/db.js');
26
+
27
+ const { test, summary } = makeRunner();
28
+
29
+ function fakePool(handler) {
30
+ const c = makeSqlAwareClient(handler);
31
+ return { query: (sql, params) => c._client.query(sql, params), connect: c.connect, queries: c._client.queries };
32
+ }
33
+ const norm = (s) => String(s).replace(/\s+/g, ' ').trim();
34
+ const find = (queries, re) => queries.find((q) => re.test(norm(q.sql)));
35
+
36
+ // The one read this function issues: claims → tasks → goals, unnest scope_modules,
37
+ // active claims only. The fake returns whatever module-key rows we hand it (the
38
+ // DISTINCT/ORDER BY is Postgres's job — here we just prove the plumbing).
39
+ function scopeHandler(rows) {
40
+ return (s) => {
41
+ if (/FROM claims c/.test(s) && /JOIN goals g/.test(s) && /unnest\(g\.scope_modules\)/.test(s)) {
42
+ return { rows };
43
+ }
44
+ return null;
45
+ };
46
+ }
47
+
48
+ // =========================================================================
49
+ // Happy path — active claims put N goals' scope_modules in scope
50
+ // =========================================================================
51
+ await test('returns the module keys the active claims scope, in DB order', async () => {
52
+ const pool = fakePool(scopeHandler([{ module_key: 'dev-box' }, { module_key: 'kernel' }, { module_key: 'lifecycle' }]));
53
+ const keys = await moduleScopeForActiveClaims(3, { pool });
54
+ assert.deepEqual(keys, ['dev-box', 'kernel', 'lifecycle'], 'maps rows → module_key list');
55
+
56
+ const q = find(pool.queries, /FROM claims c JOIN tasks t .* JOIN goals g/);
57
+ assert.ok(q, 'issues the claims→tasks→goals scope read');
58
+ assert.match(norm(q.sql), /c\.builder_id = \$1 AND c\.released_at IS NULL/, 'active claims only, keyed on builder');
59
+ assert.match(norm(q.sql), /unnest\(g\.scope_modules\)/, 'unnests the goal scope_modules array');
60
+ assert.match(norm(q.sql), /SELECT DISTINCT/, 'DISTINCT dedupes the union across claims');
61
+ assert.deepEqual(q.params, [3], 'binds the builder id');
62
+ });
63
+
64
+ // =========================================================================
65
+ // No active claim (or no scoped goal) → base-only signal ([])
66
+ // =========================================================================
67
+ await test('no active claim / no scoped goal → empty array (box falls back to BASE-only)', async () => {
68
+ const pool = fakePool(scopeHandler([]));
69
+ const keys = await moduleScopeForActiveClaims(3, { pool });
70
+ assert.deepEqual(keys, [], 'zero rows → []');
71
+ });
72
+
73
+ // =========================================================================
74
+ // Input guard — a non-numeric builder id never reaches SQL
75
+ // =========================================================================
76
+ await test('non-numeric builderId throws before any query', async () => {
77
+ const pool = fakePool(scopeHandler([{ module_key: 'dev-box' }]));
78
+ await assert.rejects(() => moduleScopeForActiveClaims('not-a-number', { pool }), /must be numeric/);
79
+ await assert.rejects(() => moduleScopeForActiveClaims(NaN, { pool }), /must be numeric/);
80
+ assert.equal(pool.queries.length, 0, 'guard trips before the pool is touched');
81
+ });
82
+
83
+ // =========================================================================
84
+ // The #538 deps.pool test seam is actually wired (we ARE in NODE_ENV==='test')
85
+ // =========================================================================
86
+ await test('honours the deps.pool test seam', async () => {
87
+ let touched = false;
88
+ const pool = fakePool((s) => { touched = true; return /unnest/.test(s) ? { rows: [{ module_key: 'game' }] } : null; });
89
+ const keys = await moduleScopeForActiveClaims(7, { pool });
90
+ assert.ok(touched, 'the injected pool was used');
91
+ assert.deepEqual(keys, ['game']);
92
+ });
93
+
94
+ summary();
@@ -0,0 +1,92 @@
1
+ // tests/needs_rebase.mjs — ADR 0120 part 2 (real DB).
2
+ //
3
+ // Exercises db.flagTaskNeedsRebase + the flag-clearing on ship against a live
4
+ // Postgres (DATABASE_URL). The fake-client harness can't cover the actual
5
+ // WHERE-clause guards (status='confirmed' AND needs_rebase=false) or the
6
+ // migration-093 columns, so this is a real-DB integration test. It SELF-SKIPS
7
+ // (exit 0) when no DB is reachable, so it is safe on a bare worktree; it is also
8
+ // listed in run-unit-tests.js's INTEGRATION set so the default DB-free lane
9
+ // never runs it.
10
+ //
11
+ // Run: DATABASE_URL=... node tests/needs_rebase.mjs
12
+
13
+ import { createRequire } from 'node:module';
14
+ import { strict as assert } from 'node:assert';
15
+ const require = createRequire(import.meta.url);
16
+ const { Pool } = require('pg');
17
+
18
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
19
+
20
+ // Reachability probe — skip cleanly if there's no local Postgres.
21
+ try {
22
+ await pool.query('SELECT 1');
23
+ } catch (e) {
24
+ console.log(`needs_rebase.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
25
+ await pool.end().catch(() => {});
26
+ process.exit(0);
27
+ }
28
+
29
+ const seams = require('../src/module-seams.js');
30
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
31
+ const db = require('../modules/lifecycle/db.js');
32
+
33
+ const VER = 'ZZZ-NR', WORKER = 90000101, TASK = 90000101;
34
+ const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
35
+ const taskRow = async () => (await q(`SELECT status, needs_rebase, needs_rebase_reason, needs_rebase_at FROM tasks WHERE id=$1`, [TASK]))[0];
36
+
37
+ async function cleanup() {
38
+ await q(`DELETE FROM builder_achievements WHERE builder_id=$1`, [WORKER]).catch(() => {});
39
+ await q(`DELETE FROM credit_log WHERE builder_id=$1 OR task_id=$2`, [WORKER, TASK]).catch(() => {});
40
+ await q(`DELETE FROM claims WHERE task_id=$1`, [TASK]).catch(() => {});
41
+ await q(`DELETE FROM tasks WHERE id=$1`, [TASK]).catch(() => {});
42
+ await q(`DELETE FROM builders WHERE id=$1`, [WORKER]).catch(() => {});
43
+ await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
44
+ }
45
+
46
+ let pass = 0;
47
+ const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
48
+
49
+ try {
50
+ await cleanup();
51
+ await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'nr','planning','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
52
+ await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'nr-w','nr-worker','NR Worker','thetes')`, [WORKER]);
53
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by) VALUES ($1,$2,'nr fixture','confirmed',10,'feature',$3)`, [TASK, VER, WORKER]);
54
+ await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2,now())`, [TASK, WORKER]);
55
+
56
+ // 1) flag a CONFIRMED task → flagged, reason + at set, returns true.
57
+ const first = await db.flagTaskNeedsRebase(String(TASK), 'strand:no_branch_no_tip');
58
+ ok(first === true, 'flagTaskNeedsRebase returns true when it newly flags a confirmed task');
59
+ let t = await taskRow();
60
+ ok(t.needs_rebase === true, 'tasks.needs_rebase is true after flagging');
61
+ ok(t.needs_rebase_reason === 'strand:no_branch_no_tip', 'reason recorded');
62
+ ok(t.needs_rebase_at != null, 'needs_rebase_at timestamp set');
63
+
64
+ // 2) idempotent — a second flag is a no-op (returns false, at unchanged).
65
+ const atWas = t.needs_rebase_at;
66
+ const second = await db.flagTaskNeedsRebase(String(TASK), 'strand:different');
67
+ ok(second === false, 'a re-flag of an already-flagged task returns false (idempotent)');
68
+ t = await taskRow();
69
+ ok(t.needs_rebase_reason === 'strand:no_branch_no_tip', 'reason NOT overwritten on re-flag');
70
+ ok(String(t.needs_rebase_at.getTime?.() ?? t.needs_rebase_at) === String(atWas.getTime?.() ?? atWas), 'needs_rebase_at unchanged on re-flag');
71
+
72
+ // 3) shipping the task CLEARS the flag (the land completed).
73
+ await db.shipTask({ taskId: String(TASK), builderId: String(WORKER) });
74
+ t = await taskRow();
75
+ ok(t.status === 'shipped', 'task shipped');
76
+ ok(t.needs_rebase === false, 'needs_rebase cleared on ship');
77
+ ok(t.needs_rebase_reason === null && t.needs_rebase_at === null, 'reason + at cleared on ship');
78
+
79
+ // 4) a NON-confirmed task cannot be flagged (guard: WHERE status='confirmed').
80
+ const onShipped = await db.flagTaskNeedsRebase(String(TASK), 'should-not-apply');
81
+ ok(onShipped === false, 'flagTaskNeedsRebase is a no-op on a non-confirmed (shipped) task');
82
+ t = await taskRow();
83
+ ok(t.needs_rebase === false, 'a shipped task stays unflagged');
84
+
85
+ console.log(`\nneeds_rebase (ADR 0120 part 2): ${pass} passed, 0 failed`);
86
+ } catch (e) {
87
+ console.error('needs_rebase.mjs FAILED:', e.message);
88
+ process.exitCode = 1;
89
+ } finally {
90
+ await cleanup();
91
+ await pool.end();
92
+ }
@@ -107,5 +107,81 @@ t('refusal carries the canonical reason code', () => {
107
107
  assert.equal(d.reason, 'session_already_has_active_claim');
108
108
  });
109
109
 
110
+ // =========================================================================
111
+ // Worktree-keying fix — the real commingle boundary is the WORKING TREE, not
112
+ // the shared auth session. A builder's concurrent worktrees share ONE auth
113
+ // session_id (the CLI token lives in ~/.config), so keying on the session alone
114
+ // wrongly blocked the second parallel worktree. Refusal must scope to the SAME
115
+ // worktree_name; a different worktree is a different branch → allowed.
116
+ // =========================================================================
117
+
118
+ // (e) THE FIX: same auth session, DIFFERENT worktree → ALLOWED (the parallel
119
+ // worktrees the code's own comment promised "must keep working").
120
+ t('same session, different worktree → ALLOWED (parallel worktrees)', () => {
121
+ const d = shouldRefuseSessionClaim({
122
+ creatorSessionId: 'sess-1',
123
+ worktreeName: 'wt-feature-b',
124
+ activeClaimsForSession: [
125
+ { claim_id: 40, task_id: 50, creator_session_id: 'sess-1', worktree_name: 'wt-feature-a', title: 'A' },
126
+ ],
127
+ });
128
+ assert.equal(d.refuse, false);
129
+ assert.equal(d.inFlightTaskId, null);
130
+ });
131
+
132
+ // (f) Same auth session, SAME worktree → refused (a real commingle: one tree,
133
+ // one branch, two tasks).
134
+ t('same session, same worktree → refused', () => {
135
+ const d = shouldRefuseSessionClaim({
136
+ creatorSessionId: 'sess-1',
137
+ worktreeName: 'wt-feature-a',
138
+ activeClaimsForSession: [
139
+ { claim_id: 41, task_id: 51, creator_session_id: 'sess-1', worktree_name: 'wt-feature-a', title: 'A' },
140
+ ],
141
+ });
142
+ assert.equal(d.refuse, true);
143
+ assert.equal(d.inFlightTaskId, 51);
144
+ assert.equal(d.reason, 'session_already_has_active_claim');
145
+ });
146
+
147
+ // (g) Both worktree-less (a plain clone, no --worktree): the single branch really
148
+ // would commingle → still refused (the safe default, unchanged behaviour).
149
+ t('same session, both worktree-less (null) → refused (shared clone)', () => {
150
+ const d = shouldRefuseSessionClaim({
151
+ creatorSessionId: 'sess-1',
152
+ worktreeName: null,
153
+ activeClaimsForSession: [
154
+ { claim_id: 42, task_id: 52, creator_session_id: 'sess-1', worktree_name: null, title: 'A' },
155
+ ],
156
+ });
157
+ assert.equal(d.refuse, true);
158
+ assert.equal(d.inFlightTaskId, 52);
159
+ });
160
+
161
+ // (h) A worktree-bound existing claim must NOT block a later worktree-less claim
162
+ // (they are genuinely different trees), and vice-versa.
163
+ t('worktree-bound claim does not block a worktree-less claim (different trees)', () => {
164
+ const held = { claim_id: 43, task_id: 53, creator_session_id: 'sess-1', worktree_name: 'wt-x', title: 'X' };
165
+ assert.equal(shouldRefuseSessionClaim({ creatorSessionId: 'sess-1', worktreeName: null, activeClaimsForSession: [held] }).refuse, false);
166
+ const heldNull = { claim_id: 44, task_id: 54, creator_session_id: 'sess-1', worktree_name: null, title: 'N' };
167
+ assert.equal(shouldRefuseSessionClaim({ creatorSessionId: 'sess-1', worktreeName: 'wt-y', activeClaimsForSession: [heldNull] }).refuse, false);
168
+ });
169
+
170
+ // (i) Correct in-flight task is named when MULTIPLE claims exist across worktrees
171
+ // — the block points at the one in the SAME tree, not just the first row.
172
+ t('names the in-flight task in the MATCHING worktree, not the first row', () => {
173
+ const d = shouldRefuseSessionClaim({
174
+ creatorSessionId: 'sess-1',
175
+ worktreeName: 'wt-two',
176
+ activeClaimsForSession: [
177
+ { claim_id: 45, task_id: 60, creator_session_id: 'sess-1', worktree_name: 'wt-one', title: 'One' },
178
+ { claim_id: 46, task_id: 61, creator_session_id: 'sess-1', worktree_name: 'wt-two', title: 'Two' },
179
+ ],
180
+ });
181
+ assert.equal(d.refuse, true);
182
+ assert.equal(d.inFlightTaskId, 61);
183
+ assert.equal(d.inFlightTitle, 'Two');
184
+ });
185
+
110
186
  console.log(`\n${passed} passed, ${failed} failed`);
111
187
  process.exit(failed ? 1 : 0);
@@ -95,15 +95,17 @@ await test('approve: tasks.shipped_by is attributed to the REQUESTER, not the Ar
95
95
  'must not re-SELECT the requester row already locked by the UPDATE');
96
96
  });
97
97
 
98
- await test('approve: the task.confirmed credit_log row is for the REQUESTER', async () => {
98
+ await test('approve: ADR 0120 pay-on-land — NO credit at override-approval; the reward waits for the land', async () => {
99
99
  const pool = fakePool({ requesterId: REQUESTER, decision: 'approve', task: TASK });
100
- await decideOverrideRequest({ requestId: 1, archonId: ARCHON, decision: 'approve' }, { pool });
101
-
102
- const credit = find(pool._client.queries, /INSERT INTO credit_log/);
103
- assert.ok(credit, 'a credit_log INSERT must run');
104
- assert.equal(credit.params[0], REQUESTER, 'credit_log.builder_id must be the requester');
105
- assert.equal(credit.params[1], TASK.id, 'credit_log.task_id must be the task');
106
- assert.ok(credit.params[2] > 0, 'credit delta must be positive');
100
+ const out = await decideOverrideRequest({ requestId: 1, archonId: ARCHON, decision: 'approve' }, { pool });
101
+
102
+ // The override-approval flips completed→confirmed and attributes shipped_by to
103
+ // the requester (asserted above), but the per-task credit now lands at the
104
+ // confirmed→shipped flip (shipTask → awardLandingCredit), NOT here — so a
105
+ // stranded override-confirmed task pays nothing until its code is on `main`.
106
+ assert.equal(out.confirmResult.creditsAwarded, 0, 'no per-task credit lands at override-approval');
107
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0,
108
+ 'override-approval must run ZERO credit_log INSERTs — the reward waits for the land');
107
109
  });
108
110
 
109
111
  await test('approve: transaction commits (not rolls back)', async () => {
@@ -183,10 +183,14 @@ await test('approve: credit + tasks.shipped_by go to the CLAIM HOLDER, not the r
183
183
  assert.deepEqual(taskUpdate.params, [TASK.id, WORKER],
184
184
  `shipped_by must bind to the claim holder ${WORKER}, got ${JSON.stringify(taskUpdate.params)}`);
185
185
 
186
- const credit = find(pool._client.queries, /INSERT INTO credit_log/);
187
- assert.ok(credit, 'a credit_log INSERT must run');
188
- assert.equal(credit.params[0], WORKER, 'credit_log.builder_id must be the claim holder');
189
- assert.ok(credit.params[2] > 0, 'credit delta must be positive');
186
+ // ADR 0120 pay-on-land: the per-task credit no longer lands at
187
+ // override-approval — it lands at the confirmed→shipped flip (shipTask). The
188
+ // credit-theft guard this test exists for is still intact: creditedBuilderId +
189
+ // shipped_by are the CLAIM HOLDER (asserted above), so when the credit DOES
190
+ // land at ship it lands on the worker, never the requester/Archon.
191
+ assert.equal(out.confirmResult.creditsAwarded, 0, 'no per-task credit lands at override-approval');
192
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0,
193
+ 'override-approval must run ZERO credit_log INSERTs — the reward waits for the land');
190
194
  });
191
195
 
192
196
  await test('approve: the claim-holder lookup happens BEFORE the tasks UPDATE binds shipped_by', async () => {
@@ -0,0 +1,42 @@
1
+ // tests/parse_cookie_decode.mjs — regression for the post-login redirect landing
2
+ // on /gate/ instead of the requested page (task 1002241).
3
+ //
4
+ // The onboarding wizard's "Connect GitHub" button hits /auth/web/start?return=/create,
5
+ // which stores the return target via `res.cookie(RETURN_COOKIE, '/create', …)`. Express
6
+ // `res.cookie` percent-ENCODES the value, so the wire cookie is `gds_return=%2Fcreate`.
7
+ // parseCookie() must DECODE symmetrically on read — else the callback runs
8
+ // safeReturnTo('%2Fcreate'), which (not starting with '/') is parsed as a URL, throws,
9
+ // returns null, and the post-login redirect silently falls through to the default
10
+ // landing (cloudbongos.com → /gate/). Decoding is a no-op for hex/simple values, so it
11
+ // can't regress the state/session/repos cookies.
12
+ import { createRequire } from 'node:module';
13
+ const require = createRequire(import.meta.url);
14
+ const { parseCookie } = require('../src/bongos/routes/_helpers.js');
15
+
16
+ let pass = 0, fail = 0;
17
+ const ok = (cond, msg) => { if (cond) { pass++; console.log(' ok ', msg); } else { fail++; console.log(' FAIL', msg); } };
18
+ const req = (cookie) => ({ headers: { cookie } });
19
+
20
+ console.log('parseCookie — decodes percent-encoded values (task 1002241):');
21
+ ok(parseCookie(req('gds_return=%2Fcreate'), 'gds_return') === '/create',
22
+ '"%2Fcreate" → "/create" (the Connect-GitHub return bug)');
23
+ ok(parseCookie(req('gds_return=https%3A%2F%2Fbuilders.cloudbongos.com%2Fx'), 'gds_return')
24
+ === 'https://builders.cloudbongos.com/x',
25
+ 'encoded full https URL round-trips (the #726 subdomain return, same latent bug)');
26
+
27
+ console.log('\nparseCookie — no-op / safe for the other cookies:');
28
+ ok(parseCookie(req('gds_oauth_state=03e92dd24cad3219c637e50fc9f115ef'), 'gds_oauth_state')
29
+ === '03e92dd24cad3219c637e50fc9f115ef', 'hex state value unchanged');
30
+ ok(parseCookie(req('gds_session=' + 'a'.repeat(64)), 'gds_session') === 'a'.repeat(64),
31
+ 'hex session token unchanged');
32
+ ok(parseCookie(req('gds_oauth_repos=1'), 'gds_oauth_repos') === '1', 'repos "1" unchanged');
33
+
34
+ console.log('\nparseCookie — misc:');
35
+ ok(parseCookie(req(''), 'gds_return') === null, 'no cookies → null');
36
+ ok(parseCookie(req('other=x; gds_return=%2Fcreate'), 'gds_return') === '/create',
37
+ 'finds the named cookie among several');
38
+ ok(parseCookie(req('gds_return=%E0%A4%A'), 'gds_return') === '%E0%A4%A',
39
+ 'malformed percent-escape falls back to the raw value (no throw)');
40
+
41
+ console.log(`\nparseCookie decode tests: ${pass} passed, ${fail} failed`);
42
+ if (fail) process.exit(1);