@bongos/core 1.18.0 → 1.18.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (553) hide show
  1. package/.bongos-core.json +1740 -640
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +5 -3
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +11 -2
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  27. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  28. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  29. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  30. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  31. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  32. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  33. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  34. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  35. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  36. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  37. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  38. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  39. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  40. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  41. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  42. package/docs/adr/README.md +15 -2
  43. package/docs/api/openapi.json +2648 -483
  44. package/docs/api-reference.md +73 -13
  45. package/docs/architecture.md +4 -2
  46. package/docs/branding-contract.md +3 -1
  47. package/docs/canonical-permissions.md +21 -8
  48. package/docs/file-map.md +15 -7
  49. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  50. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  51. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  52. package/docs/onboarding/diagrams/assertions.json +4 -7
  53. package/docs/onboarding/drachmae-and-karma.md +3 -3
  54. package/docs/onboarding/primer.md +17 -16
  55. package/docs/onboarding/slash-commands.md +124 -0
  56. package/docs/recipes/builder-box-lifecycle.md +23 -0
  57. package/docs/recipes/devbox-app-release.md +14 -4
  58. package/docs/recipes/instance-first-run.md +16 -0
  59. package/docs/recipes/local-dev.md +81 -0
  60. package/docs/recipes/ops-gotchas.md +19 -1
  61. package/docs/recipes/overnight-builder.md +1 -1
  62. package/docs/recipes/packaging-the-core.md +13 -0
  63. package/docs/recipes/parallel-goal-waves.md +174 -0
  64. package/docs/recipes/ship-and-regrade.md +2 -2
  65. package/docs/recipes/standalone-live-docs.md +48 -0
  66. package/docs/recipes/upgrading-the-core.md +5 -2
  67. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  68. package/migrations/003_pms.sql +6 -7
  69. package/migrations/006_pms_v2.sql +3 -31
  70. package/migrations/039_newcomer_friendly_column.sql +5 -0
  71. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  72. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  73. package/migrations/core_193_task_needs_rebase.sql +28 -0
  74. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  75. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  76. package/migrations/core_196_session_reward_topup.sql +69 -0
  77. package/modules/autonomy/pollers/routine-timers.js +9 -8
  78. package/modules/builder-settings/wandering-prefs.js +61 -18
  79. package/modules/dev-box/app/README.md +73 -0
  80. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  81. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  82. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  83. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  84. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  85. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  86. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  87. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  88. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  89. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  90. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  91. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  92. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  93. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  94. package/modules/dev-box/app/build-resources/icon.png +0 -0
  95. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  96. package/modules/dev-box/app/electron-builder.yml +95 -0
  97. package/modules/dev-box/app/package-lock.json +5291 -0
  98. package/modules/dev-box/app/package.json +30 -0
  99. package/modules/dev-box/app/renderer/app.js +319 -0
  100. package/modules/dev-box/app/renderer/index.html +45 -0
  101. package/modules/dev-box/app/renderer/styles.css +246 -0
  102. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  103. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  104. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  105. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  106. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  107. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  108. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  109. package/modules/dev-box/app/src/box-controller.js +287 -0
  110. package/modules/dev-box/app/src/brand.generated.json +13 -0
  111. package/modules/dev-box/app/src/brand.js +16 -0
  112. package/modules/dev-box/app/src/config.js +19 -0
  113. package/modules/dev-box/app/src/gds-client.js +90 -0
  114. package/modules/dev-box/app/src/main.js +610 -0
  115. package/modules/dev-box/app/src/pairing.js +57 -0
  116. package/modules/dev-box/app/src/preload.js +29 -0
  117. package/modules/dev-box/app/src/session-store.js +58 -0
  118. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  119. package/modules/dev-box/app/src/task-picker.js +70 -0
  120. package/modules/dev-box/app/src/tray.js +99 -0
  121. package/modules/dev-box/app/src/updater.js +55 -0
  122. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  123. package/modules/dev-box/app/test/unit.mjs +526 -0
  124. package/modules/dev-box/box-access.js +121 -4
  125. package/modules/dev-box/boxes.js +20 -0
  126. package/modules/dev-box/module.json +1 -1
  127. package/modules/dev-box/routes/box.js +87 -3
  128. package/modules/economy/credits.js +116 -25
  129. package/modules/economy/reward.js +8 -0
  130. package/modules/economy/streak.js +2 -1
  131. package/modules/governance/CLAUDE.md +15 -0
  132. package/modules/governance/catalog.js +419 -0
  133. package/modules/governance/config.js +174 -0
  134. package/modules/governance/db.js +125 -0
  135. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  136. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  137. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  138. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  139. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  140. package/modules/governance/module.json +10 -0
  141. package/modules/governance/protected-surfaces.js +88 -0
  142. package/modules/governance/protected-surfaces.json +80 -0
  143. package/modules/governance/rank-roles.js +83 -0
  144. package/modules/governance/resolver.js +221 -0
  145. package/modules/governance/routes/governance.js +89 -0
  146. package/modules/governance/routes/role-sync.js +49 -0
  147. package/modules/governance/session-scopes.js +127 -0
  148. package/modules/governance/session-scopes.json +38 -0
  149. package/modules/hall-ui/public/atlas.css +117 -0
  150. package/modules/hall-ui/public/atlas.html +3 -114
  151. package/modules/hall-ui/public/brand-apply.js +34 -6
  152. package/modules/hall-ui/public/builders.js +187 -219
  153. package/modules/hall-ui/public/diagrams.html +2 -0
  154. package/modules/hall-ui/public/dom-utils.js +27 -0
  155. package/modules/hall-ui/public/drachmae.html +2 -0
  156. package/modules/hall-ui/public/drachmae.js +54 -1
  157. package/modules/hall-ui/public/gate.css +98 -0
  158. package/modules/hall-ui/public/gate.html +3 -95
  159. package/modules/hall-ui/public/goal-inbox.js +4 -1
  160. package/modules/hall-ui/public/goals-page.js +626 -114
  161. package/modules/hall-ui/public/goals.css +79 -1
  162. package/modules/hall-ui/public/goals.html +38 -11
  163. package/modules/hall-ui/public/hall-widgets.js +62 -52
  164. package/modules/hall-ui/public/harbor.css +123 -0
  165. package/modules/hall-ui/public/harbor.html +3 -120
  166. package/modules/hall-ui/public/index.html +11 -4
  167. package/modules/hall-ui/public/instances-panel.js +4 -1
  168. package/modules/hall-ui/public/modules.css +63 -0
  169. package/modules/hall-ui/public/modules.html +3 -60
  170. package/modules/hall-ui/public/my-projects.html +78 -0
  171. package/modules/hall-ui/public/my-projects.js +142 -0
  172. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  173. package/modules/hall-ui/public/not-ready.html +2 -0
  174. package/modules/hall-ui/public/pair.css +73 -0
  175. package/modules/hall-ui/public/pair.html +3 -70
  176. package/modules/hall-ui/public/palette.js +302 -0
  177. package/modules/hall-ui/public/primer.css +13 -0
  178. package/modules/hall-ui/public/primer.html +2 -0
  179. package/modules/hall-ui/public/profile.html +2 -0
  180. package/modules/hall-ui/public/ranks.html +2 -0
  181. package/modules/hall-ui/public/roadmap.css +179 -0
  182. package/modules/hall-ui/public/roadmap.html +9 -172
  183. package/modules/hall-ui/public/roadmap.js +329 -316
  184. package/modules/hall-ui/public/scouting.html +45 -0
  185. package/modules/hall-ui/public/scouting.js +73 -0
  186. package/modules/hall-ui/public/sessions.css +16 -0
  187. package/modules/hall-ui/public/sessions.html +3 -13
  188. package/modules/hall-ui/public/settings.html +2 -0
  189. package/modules/hall-ui/public/settings.js +4 -4
  190. package/modules/hall-ui/public/shell.js +143 -18
  191. package/modules/hall-ui/public/style.css +231 -39
  192. package/modules/hall-ui/public/task-detail.js +347 -26
  193. package/modules/hall-ui/public/task.html +6 -1
  194. package/modules/hall-ui/public/watch.html +2 -0
  195. package/modules/hall-ui/public/work.html +8 -6
  196. package/modules/hall-ui/public/work.js +1 -1
  197. package/modules/ideas/inbox.js +61 -11
  198. package/modules/ideas/routes/inbox.js +23 -0
  199. package/modules/lifecycle/claim-eligibility.js +72 -0
  200. package/modules/lifecycle/db.js +519 -139
  201. package/modules/lifecycle/done-when.js +18 -3
  202. package/modules/lifecycle/github-push.js +19 -2
  203. package/modules/lifecycle/goal-advisory.js +114 -0
  204. package/modules/lifecycle/goal-authz.js +62 -0
  205. package/modules/lifecycle/lifecycle.js +9 -0
  206. package/modules/lifecycle/publish-reconciler.js +109 -6
  207. package/modules/lifecycle/routes/claims.js +68 -23
  208. package/modules/lifecycle/routes/done-when.js +110 -15
  209. package/modules/lifecycle/routes/goals.js +8 -38
  210. package/modules/lifecycle/routes/tasks.js +38 -6
  211. package/modules/lifecycle/ship-card.js +6 -0
  212. package/modules/onboarding/onboarding-state.js +1 -1
  213. package/modules/platform-identity/CLAUDE.md +15 -0
  214. package/modules/platform-identity/hub-device.js +147 -0
  215. package/modules/platform-identity/hub-keys.js +151 -0
  216. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  217. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  218. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  219. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  220. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  221. package/modules/platform-identity/module.json +18 -0
  222. package/modules/platform-identity/platform-identity.js +531 -0
  223. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  224. package/modules/platform-identity/routes/my-projects.js +140 -0
  225. package/modules/platform-identity/routes/projects.js +107 -0
  226. package/modules/platform-identity/routes/scouting.js +35 -0
  227. package/modules/platform-identity/routes/sso.js +402 -0
  228. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  229. package/modules/platform-identity/ui/.gitkeep +0 -0
  230. package/modules/security/routes/reports.js +10 -0
  231. package/modules/sessions/db.js +166 -21
  232. package/modules/sessions/module.json +1 -1
  233. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  234. package/modules/sessions/routes/sessions.js +33 -0
  235. package/modules/status-ui/public/explore.html +54 -0
  236. package/modules/status-ui/public/explore.js +67 -0
  237. package/package-lock.json +2 -2
  238. package/package.json +1 -1
  239. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  270. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  271. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  272. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  273. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  274. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  275. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  276. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  277. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  278. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  279. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  280. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  281. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  282. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  283. package/public-landing/create.html +252 -62
  284. package/public-landing/gate/README.md +137 -0
  285. package/public-landing/gate/dispmaps/bar.png +0 -0
  286. package/public-landing/gate/dispmaps/flat.png +0 -0
  287. package/public-landing/gate/dispmaps/key.png +0 -0
  288. package/public-landing/gate/index.html +587 -0
  289. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  290. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  291. package/public-landing/gate/space.css +799 -0
  292. package/public-landing/gate/space.js +1826 -0
  293. package/public-landing/gate/sprites/enter.png +0 -0
  294. package/public-landing/gate/sprites/search.png +0 -0
  295. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  296. package/public-landing/gate/sprites/tile.png +0 -0
  297. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  298. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  299. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  300. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  301. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  302. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  303. package/scripts/gds/abandon.js +6 -4
  304. package/scripts/gds/api.js +10 -5
  305. package/scripts/gds/api.ps1 +26 -0
  306. package/scripts/gds/api.sh +6 -0
  307. package/scripts/gds/audit-ships.js +7 -1
  308. package/scripts/gds/backfill-sessions.js +78 -18
  309. package/scripts/gds/backup.js +23 -1
  310. package/scripts/gds/box-connect-lib.js +124 -31
  311. package/scripts/gds/box-sync.js +66 -0
  312. package/scripts/gds/box.js +225 -53
  313. package/scripts/gds/cf-tunnel.js +145 -6
  314. package/scripts/gds/claim.js +34 -3
  315. package/scripts/gds/cli-lib.js +161 -21
  316. package/scripts/gds/code.js +6 -2
  317. package/scripts/gds/deploy-divergence.js +209 -0
  318. package/scripts/gds/dev-lib.js +309 -0
  319. package/scripts/gds/dev.js +322 -0
  320. package/scripts/gds/doctor.js +90 -61
  321. package/scripts/gds/fetch-art-key.js +1 -1
  322. package/scripts/gds/fetch-sound-prefs.js +2 -2
  323. package/scripts/gds/fitness.js +228 -12
  324. package/scripts/gds/gds-db-backup.service +16 -1
  325. package/scripts/gds/gds-db-offsite.service +9 -1
  326. package/scripts/gds/gen-api-client.js +1 -1
  327. package/scripts/gds/gen-api-docs.js +36 -14
  328. package/scripts/gds/gen-diagrams.js +45 -28
  329. package/scripts/gds/gen-file-map.js +2 -1
  330. package/scripts/gds/gen-freshness.js +32 -0
  331. package/scripts/gds/gen-repo-map.js +3 -2
  332. package/scripts/gds/gen-session-index.js +14 -5
  333. package/scripts/gds/hooks/pre-push.js +2 -2
  334. package/scripts/gds/init.js +60 -3
  335. package/scripts/gds/install-git-hooks.js +1 -1
  336. package/scripts/gds/key-card.js +15 -6
  337. package/scripts/gds/key-status.js +1 -1
  338. package/scripts/gds/linkify-refs.js +35 -9
  339. package/scripts/gds/lint-migrations.js +102 -0
  340. package/scripts/gds/login.js +58 -11
  341. package/scripts/gds/mirror-redact.js +5 -0
  342. package/scripts/gds/package-core.js +263 -4
  343. package/scripts/gds/paste-token.js +7 -1
  344. package/scripts/gds/preflight.js +126 -0
  345. package/scripts/gds/provision.js +268 -19
  346. package/scripts/gds/publish-manifest.js +22 -1
  347. package/scripts/gds/reauth.js +7 -3
  348. package/scripts/gds/regen-instance-docs.js +47 -0
  349. package/scripts/gds/run-unit-tests.js +42 -16
  350. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  351. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  352. package/scripts/gds/sequence.js +425 -0
  353. package/scripts/gds/session-cost.js +10 -1
  354. package/scripts/gds/setup.js +72 -5
  355. package/scripts/gds/shell.js +5 -3
  356. package/scripts/gds/ship.js +433 -59
  357. package/scripts/gds/smoke-gds.js +139 -8
  358. package/scripts/gds/start.js +171 -22
  359. package/scripts/gds/status.js +1 -1
  360. package/scripts/gds/triage.js +10 -9
  361. package/scripts/gds/upgrade.js +271 -7
  362. package/scripts/migrate.sh +10 -0
  363. package/scripts/status-mirror-sync.js +114 -84
  364. package/src/bongos/api-errors.js +1 -0
  365. package/src/bongos/auth.js +809 -107
  366. package/src/bongos/db-kernel.js +27 -0
  367. package/src/bongos/db.js +105 -0
  368. package/src/bongos/github-repos.js +93 -0
  369. package/src/bongos/module-scope-map.js +27 -1
  370. package/src/bongos/permission-path-check.js +136 -102
  371. package/src/bongos/repo-info.js +47 -11
  372. package/src/bongos/route-rank-check.js +429 -46
  373. package/src/bongos/routes/_helpers.js +12 -1
  374. package/src/bongos/routes/auth.js +312 -13
  375. package/src/bongos/routes/backup.js +58 -2
  376. package/src/bongos/routes/github.js +90 -0
  377. package/src/bongos/routes/instance.js +15 -3
  378. package/src/bongos/routes/me.js +35 -1
  379. package/src/bongos/routes.js +2 -0
  380. package/src/bongos/serve-internal.js +84 -15
  381. package/src/branding.js +49 -2
  382. package/src/instance-config.js +46 -0
  383. package/src/module-api.js +140 -1
  384. package/src/platform-server.js +86 -9
  385. package/tests/abandon_task.mjs +59 -3
  386. package/tests/api_cli_flags.mjs +53 -0
  387. package/tests/auth_page_gate.mjs +13 -0
  388. package/tests/backfill_sessions.mjs +127 -1
  389. package/tests/backup_cli.mjs +54 -0
  390. package/tests/bongos_box_connect.mjs +77 -0
  391. package/tests/box_access.mjs +130 -1
  392. package/tests/box_credential.mjs +125 -0
  393. package/tests/box_host_keys.mjs +24 -0
  394. package/tests/box_infra_resolve.mjs +149 -0
  395. package/tests/box_scope_session.mjs +6 -2
  396. package/tests/box_sync.mjs +55 -0
  397. package/tests/box_task_scope.mjs +99 -0
  398. package/tests/branding.mjs +41 -23
  399. package/tests/cf_ruleset_preflight.mjs +142 -0
  400. package/tests/cf_tunnel.mjs +37 -0
  401. package/tests/claim_gate_rebase.mjs +98 -0
  402. package/tests/clawback_confirmed_backlog.mjs +103 -0
  403. package/tests/cli_lib_formatters.mjs +20 -0
  404. package/tests/confirm_attribution.mjs +30 -46
  405. package/tests/credit_grant.mjs +198 -10
  406. package/tests/criterion_authoring_gate.mjs +272 -0
  407. package/tests/criterion_progress.mjs +33 -1
  408. package/tests/dev_lib.mjs +253 -0
  409. package/tests/devbox_artifact_gate.mjs +75 -0
  410. package/tests/devbox_dmg_backdrop.mjs +137 -0
  411. package/tests/devbox_icons.mjs +304 -0
  412. package/tests/devbox_installer_brand.mjs +232 -0
  413. package/tests/devbox_key_register.mjs +234 -0
  414. package/tests/docs_openapi_route.mjs +110 -0
  415. package/tests/doctor_hooks.mjs +141 -0
  416. package/tests/extract_json.mjs +140 -0
  417. package/tests/fitness.mjs +89 -0
  418. package/tests/gen_freshness.mjs +96 -0
  419. package/tests/github_repos_helpers.mjs +91 -0
  420. package/tests/github_token_store.mjs +121 -0
  421. package/tests/goal_advisory.mjs +251 -0
  422. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  423. package/tests/goal_routes.mjs +59 -4
  424. package/tests/goal_tier.mjs +12 -3
  425. package/tests/governance_catalog.mjs +129 -0
  426. package/tests/governance_claim_eligibility.mjs +262 -0
  427. package/tests/governance_config.mjs +187 -0
  428. package/tests/governance_ownership_scope.mjs +410 -0
  429. package/tests/governance_page_gates.mjs +324 -0
  430. package/tests/governance_parity.mjs +336 -0
  431. package/tests/governance_principals.mjs +232 -0
  432. package/tests/governance_protected_surfaces.mjs +208 -0
  433. package/tests/governance_require_permission.mjs +223 -0
  434. package/tests/governance_resolver.mjs +84 -0
  435. package/tests/governance_role_sync.mjs +231 -0
  436. package/tests/governance_seed.mjs +127 -0
  437. package/tests/governance_session_scope.mjs +434 -0
  438. package/tests/governance_vanilla_boot.mjs +203 -0
  439. package/tests/grade_attribution.mjs +7 -7
  440. package/tests/hall_audit.mjs +223 -0
  441. package/tests/hall_nav.mjs +191 -0
  442. package/tests/hall_palette.mjs +206 -0
  443. package/tests/hall_signout.mjs +124 -0
  444. package/tests/hook_user_agents.mjs +10 -0
  445. package/tests/idea_triage.mjs +162 -0
  446. package/tests/idp_device_exchange.mjs +198 -0
  447. package/tests/idp_federation.mjs +100 -0
  448. package/tests/init.mjs +51 -0
  449. package/tests/instance_config.mjs +65 -0
  450. package/tests/instance_mismatch_guard.mjs +23 -6
  451. package/tests/instance_release_config.mjs +104 -0
  452. package/tests/linkify_refs.mjs +15 -2
  453. package/tests/migrations_forward_safe.mjs +86 -0
  454. package/tests/module_api.mjs +1 -1
  455. package/tests/module_contributions.mjs +55 -0
  456. package/tests/module_loader.mjs +10 -5
  457. package/tests/module_scope_active_claims.mjs +94 -0
  458. package/tests/needs_rebase.mjs +92 -0
  459. package/tests/one_active_claim_per_session.mjs +76 -0
  460. package/tests/override_attribution.mjs +10 -8
  461. package/tests/override_request_ownership.mjs +8 -4
  462. package/tests/package_core.mjs +246 -1
  463. package/tests/parse_cookie_decode.mjs +42 -0
  464. package/tests/pay_on_land.mjs +129 -0
  465. package/tests/platform_boot.mjs +100 -1
  466. package/tests/post_login_landing.mjs +42 -0
  467. package/tests/preflight_checks.mjs +72 -0
  468. package/tests/provision.mjs +289 -7
  469. package/tests/pruned_lockfile_integrity.mjs +79 -0
  470. package/tests/publish_branch_route.mjs +53 -0
  471. package/tests/publish_reconciler.mjs +187 -2
  472. package/tests/repo_info_binding.mjs +248 -0
  473. package/tests/reward_gate.mjs +11 -2
  474. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  475. package/tests/reward_mode_surfacing.mjs +168 -0
  476. package/tests/route_permission_check.mjs +301 -0
  477. package/tests/route_rank_check.mjs +254 -12
  478. package/tests/route_validation.mjs +30 -0
  479. package/tests/sequence.mjs +267 -0
  480. package/tests/session_digest.mjs +44 -0
  481. package/tests/session_index.mjs +18 -6
  482. package/tests/session_records.mjs +39 -1
  483. package/tests/session_reward_on_land.mjs +381 -0
  484. package/tests/session_reward_reconcile.mjs +229 -0
  485. package/tests/ship_api_client_pathspec.mjs +100 -0
  486. package/tests/ship_base_flag.mjs +93 -0
  487. package/tests/ship_ci_deploy.mjs +140 -3
  488. package/tests/ship_upstream_deletion.mjs +57 -0
  489. package/tests/smoke_target.mjs +233 -0
  490. package/tests/start_env_guard.mjs +87 -0
  491. package/tests/start_rebase_warning.mjs +103 -0
  492. package/tests/status_mirror_sync.mjs +122 -0
  493. package/tests/stop_refcheck.mjs +19 -1
  494. package/tests/task_goal_link.mjs +15 -6
  495. package/tests/upgrade.mjs +393 -5
  496. package/tests/web_admission_status.mjs +162 -0
  497. package/tests/write_validation_gaps.mjs +110 -0
  498. package/.gitignore +0 -91
  499. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  500. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  501. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  502. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  503. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  504. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  505. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  506. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  507. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  508. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  509. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  510. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  511. package/scripts/hall-preview/fixtures/goals.json +0 -502
  512. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  513. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  514. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  515. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  516. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  517. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  518. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  519. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  520. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  521. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  522. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  523. package/scripts/hall-preview/fixtures/instance.json +0 -124
  524. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  525. package/scripts/hall-preview/fixtures/me.json +0 -476
  526. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  527. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  528. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  529. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  530. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  531. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  532. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  533. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  534. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  535. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  536. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  537. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  538. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  539. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  540. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  541. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  542. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  543. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  544. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  545. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  546. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  547. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  548. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  549. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  550. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  551. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  552. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  553. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -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 () => {
@@ -16,6 +16,9 @@ import { strict as assert } from 'node:assert';
16
16
  import { createRequire } from 'node:module';
17
17
  import zlib from 'node:zlib';
18
18
  import crypto from 'node:crypto';
19
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
20
+ import { tmpdir } from 'node:os';
21
+ import { join, dirname } from 'node:path';
19
22
 
20
23
  const require = createRequire(import.meta.url);
21
24
  const m = require('../scripts/gds/package-core.js');
@@ -25,14 +28,22 @@ const {
25
28
  ARTIFACT_NAME, MANIFEST_SCHEMA, TAR_ROOT,
26
29
  // task 1824 — the consumer-core file-classification layer
27
30
  isDocPath, functionalSecretLeaks, processCoreFile,
31
+ // idea 1000543 — npm-never-installed files dropped from the pin
32
+ isNpmStripped,
28
33
  // task 2121 — agent-doc (CLAUDE.md/AGENTS.md) stub
29
34
  isAgentDoc, CORE_AGENT_DOC_STUB,
30
35
  // task 1882 (ADR 0108 §2) — the real installable npm package
31
36
  CORE_DEP_NAMES, buildPackageJson, resolveLockDep, lockfileClosure, prunePackageLock,
32
37
  // task 2089 (ADR 0134) — private-by-default npm publish
33
38
  publishArgs,
39
+ // task 1002218 (audit H3) — build-time self-verify pin reproduction
40
+ reproducePinFromTree,
41
+ // task 1002220 (audit H6) — post-build leak scan
42
+ scanArtifactForLeaks, PRIOR_INSTANCE_LITERALS,
43
+ // task 1002217 (audit H4) — the fixed neutral floor
44
+ PRIOR_INSTANCE_FLOOR,
34
45
  } = m;
35
- const { buildRedactionRules, collectSensitiveLiterals } = require('../scripts/gds/mirror-redact.js');
46
+ const { buildRedactionRules, collectSensitiveLiterals, redactText, findLeaks } = require('../scripts/gds/mirror-redact.js');
36
47
 
37
48
  // A synthetic instance brand for the classification tests (never the real OTB literals).
38
49
  const TEST_BRANDING = {
@@ -485,5 +496,239 @@ t('prunePackageLock: on the real repo lockfile, drops the vast majority of packa
485
496
  assert.ok(keptCount > 0);
486
497
  });
487
498
 
499
+ // ---- task 1002218 (audit H3): build-time self-verify pin reproduction ----
500
+ // Build a tiny unpacked tree + a matching sidecar manifest (same hash fns the packager uses),
501
+ // then prove reproducePinFromTree accepts a faithful reproduction and rejects each tamper.
502
+ function makeTreeAndSidecar() {
503
+ const root = mkdtempSync(join(tmpdir(), 'pc-selfverify-'));
504
+ const files = [
505
+ { path: 'src/a.js', content: 'module.exports = 1\n', mode: '0000644' },
506
+ { path: 'sub/dir/b.json', content: '{"k":2}\n', mode: '0000644' },
507
+ { path: 'bin/run.sh', content: '#!/bin/sh\necho hi\n', mode: '0000755' },
508
+ ];
509
+ for (const f of files) {
510
+ const abs = join(root, ...f.path.split('/'));
511
+ mkdirSync(dirname(abs), { recursive: true });
512
+ writeFileSync(abs, f.content);
513
+ }
514
+ const meta = files
515
+ .map((f) => ({ path: f.path, mode: normalizeMode(f.mode), sha256: sha256hex(Buffer.from(f.content)) }))
516
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
517
+ const sidecar = { core_version: '9.9.9', tree_sha256: canonicalTreeHash(meta), files: meta, tarball: { sha256: 'abc123', bytes: 0 } };
518
+ return { root, sidecar, files };
519
+ }
520
+
521
+ t('reproducePinFromTree: a faithful tree reproduces the pin (ok, no problems)', () => {
522
+ const { root, sidecar } = makeTreeAndSidecar();
523
+ try {
524
+ const r = reproducePinFromTree({ treeRoot: root, sidecar }); // no tgzPath → skip the tarball-bytes check
525
+ assert.equal(r.ok, true, r.problems.join('; '));
526
+ assert.equal(r.fileCount, 3);
527
+ } finally { rmSync(root, { recursive: true, force: true }); }
528
+ });
529
+
530
+ t('reproducePinFromTree: a tampered file content → not ok (content mismatch)', () => {
531
+ const { root, sidecar } = makeTreeAndSidecar();
532
+ try {
533
+ writeFileSync(join(root, 'src', 'a.js'), 'module.exports = 666 // tampered\n');
534
+ const r = reproducePinFromTree({ treeRoot: root, sidecar });
535
+ assert.equal(r.ok, false);
536
+ assert.ok(r.problems.some((p) => /content mismatch: src\/a\.js/.test(p)), r.problems.join('; '));
537
+ } finally { rmSync(root, { recursive: true, force: true }); }
538
+ });
539
+
540
+ t('reproducePinFromTree: a missing file → not ok (missing in artifact)', () => {
541
+ const { root, sidecar } = makeTreeAndSidecar();
542
+ try {
543
+ rmSync(join(root, 'sub', 'dir', 'b.json'));
544
+ const r = reproducePinFromTree({ treeRoot: root, sidecar });
545
+ assert.equal(r.ok, false);
546
+ assert.ok(r.problems.some((p) => /missing in artifact: sub\/dir\/b\.json/.test(p)), r.problems.join('; '));
547
+ } finally { rmSync(root, { recursive: true, force: true }); }
548
+ });
549
+
550
+ t('reproducePinFromTree: a manifest whose tree_sha256 lies → not ok (tree hash mismatch)', () => {
551
+ const { root, sidecar } = makeTreeAndSidecar();
552
+ try {
553
+ const lying = { ...sidecar, tree_sha256: 'f'.repeat(64) }; // every file matches, but the recorded hash is wrong
554
+ const r = reproducePinFromTree({ treeRoot: root, sidecar: lying });
555
+ assert.equal(r.ok, false);
556
+ assert.ok(r.problems.some((p) => /recomputed tree_sha256/.test(p)), r.problems.join('; '));
557
+ } finally { rmSync(root, { recursive: true, force: true }); }
558
+ });
559
+
560
+ t('reproducePinFromTree: a .tgz whose bytes differ from manifest.tarball.sha256 → not ok', () => {
561
+ const { root, sidecar } = makeTreeAndSidecar();
562
+ const tgz = join(root, 'artifact.tgz');
563
+ try {
564
+ writeFileSync(tgz, Buffer.from('the actual tarball bytes'));
565
+ const withTb = { ...sidecar, tarball: { sha256: sha256hex(Buffer.from('DIFFERENT bytes')), bytes: 24 } };
566
+ const r = reproducePinFromTree({ treeRoot: root, sidecar: withTb, tgzPath: tgz });
567
+ assert.equal(r.ok, false);
568
+ assert.ok(r.problems.some((p) => /\.tgz sha256/.test(p)), r.problems.join('; '));
569
+ } finally { rmSync(root, { recursive: true, force: true }); }
570
+ });
571
+
572
+ // ---- task 1002220 (audit H6): post-build leak scan ----
573
+ t('scanArtifactForLeaks: flags a prior-instance literal in any shipped file', () => {
574
+ const files = [
575
+ { path: 'src/x.js', content: 'const base = "https://amazonprimea.com";\n' },
576
+ { path: 'docs/readme.md', content: 'Owned by lars589.\n' },
577
+ { path: 'src/clean.js', content: 'export const ok = true;\n' },
578
+ ];
579
+ const r = scanArtifactForLeaks(files);
580
+ const hitPaths = r.literalHits.map((h) => h.path);
581
+ assert.ok(hitPaths.includes('src/x.js'), 'amazonprimea in a functional file must be flagged');
582
+ assert.ok(hitPaths.includes('docs/readme.md'), 'owner login in a doc must be flagged too (literal scan spans all files)');
583
+ assert.ok(!hitPaths.includes('src/clean.js'), 'a clean file must not be flagged');
584
+ });
585
+
586
+ t('scanArtifactForLeaks: flags a public IP but not a private/localhost one', () => {
587
+ const r = scanArtifactForLeaks([
588
+ { path: 'a.js', content: 'host = 104.236.254.243\n' }, // public → flagged
589
+ { path: 'b.js', content: 'host = 127.0.0.1; lan = 10.0.0.5\n' }, // localhost + private → not
590
+ ]);
591
+ const byPath = Object.fromEntries(r.literalHits.map((h) => [h.path, h.label]));
592
+ assert.ok(byPath['a.js'] === 'public-ip', `expected a public-ip hit on a.js, got ${JSON.stringify(r.literalHits)}`);
593
+ assert.ok(!('b.js' in byPath), 'localhost/private IPs must not be flagged');
594
+ });
595
+
596
+ t('scanArtifactForLeaks: credential SHAPE warning fires on functional files, not docs', () => {
597
+ const key = `sk-ant-${'A1b2C3d4E5f6G7h8'}`; // a fake Anthropic-shaped token
598
+ const r = scanArtifactForLeaks([
599
+ { path: 'src/leak.js', content: `const k = "${key}";\n` },
600
+ { path: 'docs/guide.md', content: `example key: ${key}\n` }, // docs are redacted+gated elsewhere → not a shape warning here
601
+ ]);
602
+ const warnPaths = r.secretWarnings.map((w) => w.path);
603
+ assert.ok(warnPaths.includes('src/leak.js'), 'a credential shape in a functional file must warn');
604
+ assert.ok(!warnPaths.includes('docs/guide.md'), 'docs are out of scope for the credential-shape warning');
605
+ const w = r.secretWarnings.find((x) => x.path === 'src/leak.js');
606
+ assert.ok(w.shapes.includes('anthropic-key'), `expected anthropic-key shape, got ${JSON.stringify(w.shapes)}`);
607
+ });
608
+
609
+ t('scanArtifactForLeaks: ordinary long identifiers / base64 hashes do NOT warn (low false-positive)', () => {
610
+ const r = scanArtifactForLeaks([
611
+ { path: 'src/normal.js', content: 'const integrityHash = "sha512-Abc123Def456Ghi789Jkl012Mno345Pqr678";\nfunction reallyLongDescriptiveFunctionName() {}\n' },
612
+ { path: 'data.json', content: JSON.stringify({ sha256: 'a'.repeat(64), token: 'short' }) },
613
+ ]);
614
+ assert.equal(r.secretWarnings.length, 0, `no credential shapes expected, got ${JSON.stringify(r.secretWarnings)}`);
615
+ });
616
+
617
+ t('PRIOR_INSTANCE_LITERALS: carries the customer-zero tokens the neutral trunk must not ship', () => {
618
+ assert.ok(Array.isArray(PRIOR_INSTANCE_LITERALS) && PRIOR_INSTANCE_LITERALS.length >= 3);
619
+ // Case-insensitive: the floor tokens are lowercased for the redaction rules + findLeaks
620
+ // (both case-insensitive), so assert coverage, not exact case.
621
+ const lower = PRIOR_INSTANCE_LITERALS.map((s) => s.toLowerCase());
622
+ for (const lit of ['amazonprimea', 'off the boats', 'lars589']) {
623
+ assert.ok(lower.includes(lit), `floor list should cover ${lit}`);
624
+ }
625
+ });
626
+
627
+ // ---- task 1002217 (audit H4): the neutral floor gate (fails closed with no branding.json) ----
628
+ t('neutral floor: with EMPTY branding + the floor as extra, docs de-brand and the gate covers the floor', () => {
629
+ // The F2 gap: with no branding.json, buildRedactionRules({})/collectSensitiveLiterals({}) are
630
+ // empty, so a doc keeps its prior-instance identity. Injecting PRIOR_INSTANCE_FLOOR as `extra`
631
+ // (exactly what package-core's main() now does) closes it.
632
+ const rules = buildRedactionRules({}, PRIOR_INSTANCE_FLOOR);
633
+ const lits = collectSensitiveLiterals({}, PRIOR_INSTANCE_FLOOR);
634
+ const doc = 'See https://amazonprimea.com, owned by lars589 — the Off the Boats world.';
635
+ const red = redactText(doc, rules);
636
+ assert.ok(!/amazonprimea/i.test(red), `amazonprimea survived: ${red}`);
637
+ assert.ok(!/lars589/i.test(red), `owner login survived: ${red}`);
638
+ assert.ok(!/off the boats/i.test(red), `product name survived: ${red}`);
639
+ assert.deepEqual(findLeaks(red, lits), [], `gate still sees a floor leak after redaction: ${JSON.stringify(findLeaks(red, lits))}`);
640
+ });
641
+
642
+ t('neutral floor: the fail-closed gate WOULD catch an un-redacted survivor (empty branding)', () => {
643
+ const lits = collectSensitiveLiterals({}, PRIOR_INSTANCE_FLOOR);
644
+ // If a doc reached the gate still carrying a floor literal, findLeaks (the fail-closed detector)
645
+ // flags it — which aborts the build in package-core (atomic, produces nothing).
646
+ assert.ok(findLeaks('leftover amazonprimea reference', lits).length > 0, 'floor token must be gated');
647
+ assert.ok(findLeaks('signed off by lars589', lits).length > 0, 'floor owner must be gated');
648
+ });
649
+
650
+ t('neutral floor: WITHOUT the floor, empty branding leaks (proves the floor is what closes F2)', () => {
651
+ // The counterfactual — the pre-H4 neutral-trunk behavior: empty branding + no floor = no rules,
652
+ // no gate literals, so the doc ships its prior-instance identity verbatim.
653
+ const rules = buildRedactionRules({});
654
+ const lits = collectSensitiveLiterals({});
655
+ const doc = 'amazonprimea + lars589';
656
+ assert.equal(redactText(doc, rules).includes('amazonprimea'), true, 'no-floor redaction is a no-op (the F2 gap)');
657
+ assert.equal(findLeaks(doc, lits).length, 0, 'no-floor gate sees nothing (the F2 gap)');
658
+ });
659
+
660
+ // ---- idea 1000543: npm-never-installed files are dropped from the pin ----
661
+ // npm strips .gitignore/.npmignore/.npmrc from an installed package, so a pin that
662
+ // LISTS one fails when a consumer recomputes the tree hash over node_modules/@bongos/core
663
+ // (verifyInstalledPin, upgrade.js) -> the vendored `bongos upgrade` auto-rolls-back.
664
+ t('isNpmStripped: matches .gitignore/.npmignore/.npmrc at root and any depth', () => {
665
+ assert.equal(isNpmStripped('.gitignore'), true);
666
+ assert.equal(isNpmStripped('.npmignore'), true);
667
+ assert.equal(isNpmStripped('.npmrc'), true);
668
+ assert.equal(isNpmStripped('modules/economy/.gitignore'), true); // npm strips nested ones too
669
+ assert.equal(isNpmStripped('a/b/c/.npmrc'), true);
670
+ });
671
+
672
+ t('isNpmStripped: does NOT match ordinary files or basename lookalikes', () => {
673
+ assert.equal(isNpmStripped('src/index.js'), false);
674
+ assert.equal(isNpmStripped('package.json'), false);
675
+ assert.equal(isNpmStripped('docs/.gitignore.md'), false); // .gitignore is not the basename here
676
+ assert.equal(isNpmStripped('gitignore'), false); // no leading dot
677
+ assert.equal(isNpmStripped('src/.gitignoreignore'), false);
678
+ assert.equal(isNpmStripped(''), false);
679
+ assert.equal(isNpmStripped(null), false);
680
+ });
681
+
682
+ t('the packager selection drops the stripped files from the pinned set (models main())', () => {
683
+ const treePaths = ['.gitignore', 'src/a.js', 'migrations/001.sql', 'modules/x/.npmrc', 'README.md'];
684
+ const kept = treePaths.filter((p) => !isNpmStripped(p)); // exactly main()'s `publishable` filter
685
+ assert.deepEqual(kept, ['src/a.js', 'migrations/001.sql', 'README.md']);
686
+ });
687
+
688
+ // End-to-end proof against the consume-side recompute. reproducePinFromTree and upgrade.js's
689
+ // verifyInstalledPin both iterate manifest.files[] and rebuild canonicalTreeHash, so a manifest
690
+ // that LISTS .gitignore fails when npm has stripped it (the bug); a manifest built through the
691
+ // isNpmStripped filter does not list it and reproduces cleanly (the fix).
692
+ function buildManifestAndNpmInstalledTree({ includeGitignore }) {
693
+ const root = mkdtempSync(join(tmpdir(), 'pc-npmstrip-'));
694
+ const treeFiles = [ // the FULL git tree the packager enumerates, incl. a tracked .gitignore
695
+ { path: '.gitignore', content: 'dist/\nnode_modules/\n' },
696
+ { path: 'src/a.js', content: 'module.exports = 1\n' },
697
+ { path: 'package.json', content: '{"name":"@bongos/core"}\n' },
698
+ ];
699
+ // What the packager PINS: pre-fix keeps everything; post-fix drops the npm-stripped files.
700
+ const pinned = treeFiles.filter((f) => (includeGitignore ? true : !isNpmStripped(f.path)));
701
+ const files = pinned
702
+ .map((f) => ({ path: f.path, mode: normalizeMode('100644'), sha256: sha256hex(Buffer.from(f.content)) }))
703
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
704
+ const sidecar = { core_version: '9.9.9', tree_sha256: canonicalTreeHash(files), files };
705
+ // What `npm install` ACTUALLY materializes: everything EXCEPT the files npm strips.
706
+ for (const f of treeFiles) {
707
+ if (isNpmStripped(f.path)) continue; // npm never writes .gitignore into the installed package
708
+ const abs = join(root, ...f.path.split('/'));
709
+ mkdirSync(dirname(abs), { recursive: true });
710
+ writeFileSync(abs, f.content);
711
+ }
712
+ return { root, sidecar };
713
+ }
714
+
715
+ t('idea 1000543 (the bug): a manifest that PINS .gitignore FAILS against an npm-installed tree', () => {
716
+ const { root, sidecar } = buildManifestAndNpmInstalledTree({ includeGitignore: true });
717
+ try {
718
+ const r = reproducePinFromTree({ treeRoot: root, sidecar });
719
+ assert.equal(r.ok, false, 'pinning .gitignore must fail once npm has stripped it');
720
+ assert.ok(r.problems.some((p) => /\.gitignore/.test(p)), `expected a .gitignore problem, got: ${r.problems.join('; ')}`);
721
+ } finally { rmSync(root, { recursive: true, force: true }); }
722
+ });
723
+
724
+ t('idea 1000543 (the fix): the isNpmStripped filter makes the pin reproduce against the npm-installed tree', () => {
725
+ const { root, sidecar } = buildManifestAndNpmInstalledTree({ includeGitignore: false });
726
+ try {
727
+ assert.ok(!sidecar.files.some((f) => f.path === '.gitignore'), 'the fixed manifest must not list .gitignore');
728
+ const r = reproducePinFromTree({ treeRoot: root, sidecar });
729
+ assert.equal(r.ok, true, `the fixed manifest must reproduce; problems: ${r.problems.join('; ')}`);
730
+ } finally { rmSync(root, { recursive: true, force: true }); }
731
+ });
732
+
488
733
  console.log(`\n${passed} passed, ${failed} failed`);
489
734
  process.exit(failed ? 1 : 0);
@@ -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);
@@ -0,0 +1,129 @@
1
+ // tests/pay_on_land.mjs
2
+ //
3
+ // ADR 0120 part 1 (pay-on-land) — the per-task credit + idea-promotion bonus
4
+ // are booked at the confirmed→SHIPPED flip (db.shipTask → awardLandingCredit),
5
+ // NOT at confirm. This is the mirror of confirm_attribution.mjs / override_
6
+ // attribution.mjs / grade_attribution.mjs, which now assert NO credit at their
7
+ // (confirm-time) transition; here we assert the credit lands at ship.
8
+ //
9
+ // The contract this pins:
10
+ // • ship awards a positive per-task credit, reason='task.shipped'
11
+ // • the credit is attributed to the CLAIM HOLDER (the builder who did the
12
+ // work), even when a DIFFERENT builder (the reconciler / a supervisor)
13
+ // triggers the ship
14
+ // • idempotent on (task_id, 'task.shipped'): a re-ship / reconciler re-sweep
15
+ // books nothing a second time
16
+ // • credits_reward = 0 → no credit_log INSERT
17
+ // • a task not in 'confirmed' state throws TASK_NOT_CONFIRMED, awards nothing
18
+ //
19
+ // shipTask honors the #538 `deps.pool` seam under NODE_ENV==='test', so we drive
20
+ // it with a SQL-aware fakeClient — no live DB.
21
+ //
22
+ // Run: node tests/pay_on_land.mjs
23
+
24
+ import { strict as assert } from 'node:assert';
25
+ import { createRequire } from 'node:module';
26
+ import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
27
+
28
+ process.env.NODE_ENV = 'test';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ // shipTask resolves the `reward` port for the credit grant + achievement
32
+ // reconcile. Register it; stub the gamification hooks to no-ops so the test
33
+ // isolates the credit path (the real credits.js SQL still runs against the fake).
34
+ const achievements = require('../modules/economy/achievements.js');
35
+ achievements.evaluateAfterShip = async () => [];
36
+ achievements.evaluateAfterCreditAward = async () => [];
37
+ achievements.reconcileBuilderAchievements = async () => [];
38
+ const seams = require('../src/module-seams.js');
39
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
40
+ const db = require('../modules/lifecycle/db.js');
41
+ const { shipTask } = db;
42
+
43
+ const { test, summary } = makeRunner();
44
+
45
+ // SQL-aware fake covering shipTask's transaction. Knobs:
46
+ // claimHolderId → who mostRecentClaimHolderId resolves (null = none)
47
+ // shippedBy → tasks.shipped_by on the returned shipped row
48
+ // creditsReward, kind → the task's reward inputs
49
+ // confirmed=false → the UPDATE tasks matches nothing (TASK_NOT_CONFIRMED)
50
+ // priorShipCredit → a 'task.shipped' credit_log row already exists (idempotent)
51
+ function fakePool({ claimHolderId, shippedBy, creditsReward = 15, kind = 'feature', confirmed = true, priorShipCredit = false }) {
52
+ return makeSqlAwareClient((s) => {
53
+ if (/^UPDATE tasks/.test(s)) {
54
+ return {
55
+ rows: confirmed
56
+ ? [{ id: TASK.id, title: TASK.title, status: 'shipped', shipped_at: new Date(0).toISOString(),
57
+ shipped_by: shippedBy, security_sensitive: false, value_summary: null,
58
+ credits_reward: creditsReward, kind }]
59
+ : [],
60
+ };
61
+ }
62
+ if (/^UPDATE claims/.test(s)) return { rows: [] }; // no open claims to heal
63
+ if (/UPDATE builders SET first_ship_at/.test(s)) return { rows: [] };
64
+ if (/SELECT rank FROM builders/.test(s)) return { rows: [{ rank: 'thetes' }] }; // not xenos → no graduation
65
+ if (/SELECT builder_id FROM claims/.test(s)) {
66
+ return { rows: claimHolderId == null ? [] : [{ builder_id: claimHolderId }] };
67
+ }
68
+ if (/SELECT 1 FROM credit_log .* 'task\.shipped'/.test(s)) {
69
+ return { rows: priorShipCredit ? [{ '?column?': 1 }] : [] };
70
+ }
71
+ if (/SELECT 1 FROM credit_log .* 'idea_promotion_bonus'/.test(s)) return { rows: [] };
72
+ if (/FROM idea_inbox/.test(s)) return { rows: [] }; // no idea lineage
73
+ if (/INSERT INTO credit_log/.test(s)) return { rows: [] };
74
+ return { rows: [] }; // BEGIN/COMMIT/SAVEPOINT/etc.
75
+ });
76
+ }
77
+
78
+ const find = (queries, re) => queries.find((q) => re.test(String(q.sql).replace(/\s+/g, ' ')));
79
+ const all = (queries, re) => queries.filter((q) => re.test(String(q.sql).replace(/\s+/g, ' ')));
80
+
81
+ const RECONCILER = 999; // the builder_id the reconciler passes to shipTask
82
+ const WORKER = 25; // the claim holder who actually did the work
83
+ const TASK = { id: 660, title: 'fixture task' };
84
+
85
+ // =========================================================================
86
+ await test('ship: the per-task credit lands at SHIP (reason=task.shipped), attributed to the claim holder', async () => {
87
+ // shipped_by was set to the claim holder at confirm; the reconciler triggers
88
+ // the ship with its own builderId — attribution must still be the worker.
89
+ const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, creditsReward: 15, kind: 'feature' });
90
+ const out = await shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool });
91
+
92
+ assert.equal(out.status, 'shipped', 'task flips to shipped');
93
+ assert.equal(out.creditedBuilderId, WORKER, 'credit attributed to the claim holder, not the reconciler');
94
+ assert.ok(out.creditsAwarded > 0, 'a positive per-task credit must be awarded at ship');
95
+
96
+ const insert = find(pool._client.queries, /INSERT INTO credit_log/);
97
+ assert.ok(insert, 'a credit_log INSERT must run at ship');
98
+ assert.equal(insert.params[0], WORKER, 'credit_log.builder_id = claim holder');
99
+ assert.notEqual(insert.params[0], RECONCILER, 'credit must NOT go to the ship trigger');
100
+ assert.equal(insert.params[1], TASK.id, 'credit_log.task_id = the task');
101
+ assert.equal(insert.params[3], 'task.shipped', "reason must be 'task.shipped' (ADR 0120), not 'task.confirmed'");
102
+ });
103
+
104
+ await test('ship: idempotent — a prior task.shipped credit means no second INSERT', async () => {
105
+ const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, priorShipCredit: true });
106
+ const out = await shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool });
107
+ assert.equal(out.creditsAwarded, 0, 'no fresh credit on a re-ship / reconciler re-sweep');
108
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'must not double-insert credit');
109
+ });
110
+
111
+ await test('ship: credits_reward = 0 → no credit_log INSERT', async () => {
112
+ const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, creditsReward: 0 });
113
+ const out = await shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool });
114
+ assert.equal(out.creditsAwarded, 0, 'a zero-reward task pays nothing');
115
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'no INSERT for a 0-reward task');
116
+ });
117
+
118
+ await test('ship: not in confirmed state → throws TASK_NOT_CONFIRMED, awards nothing', async () => {
119
+ const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, confirmed: false });
120
+ await assert.rejects(
121
+ () => shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool }),
122
+ (err) => err.code === 'TASK_NOT_CONFIRMED',
123
+ 'must throw TASK_NOT_CONFIRMED when the UPDATE matches no confirmed row'
124
+ );
125
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'must not award credit on a failed ship');
126
+ });
127
+
128
+ // -------------------------------------------------------------------------
129
+ summary();