@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
@@ -1,18 +1,22 @@
1
1
  // tests/confirm_attribution.mjs
2
2
  //
3
- // Regression guard for #661 — when an Archon manual-confirms (POST
4
- // /tasks/:id/confirm) ANOTHER builder's completed task, BOTH the credit_log
5
- // row AND tasks.shipped_by must land with the CLAIM HOLDER (the builder who
6
- // did the work), never the confirming Archon.
3
+ // Regression guard for #661 + ADR 0120 (pay-on-land).
7
4
  //
8
- // Before #661, confirmTask({ taskId, builderId }) used `builderId` (the
9
- // confirmer, = req.builder.id, the Archon) for both the credit and the
10
- // shipped_by COALESCE. Live repro 2026-06-04: Xenos (#25) shipped #660 from
11
- // Windows, the grader infra-errored so it stalled at `completed`, an Archon
12
- // (#1) manual-confirmed, and the 15c landed on #1 instead of #25. The fix
13
- // resolves the most-recent claim holder up front and threads it into both the
14
- // tasks UPDATE and the credit insert; it falls back to the confirmer only when
15
- // the task has no claim at all.
5
+ // #661: when an Archon manual-confirms (POST /tasks/:id/confirm) ANOTHER
6
+ // builder's completed task, tasks.shipped_by must land with the CLAIM HOLDER
7
+ // (the builder who did the work), never the confirming Archon. Live repro
8
+ // 2026-06-04: Xenos (#25) shipped #660, the grader infra-errored so it stalled
9
+ // at `completed`, an Archon (#1) manual-confirmed, and shipped_by must still be
10
+ // #25. confirmTask resolves the most-recent claim holder up front and threads
11
+ // it into the tasks UPDATE; it falls back to the confirmer only when the task
12
+ // has no claim at all.
13
+ //
14
+ // ADR 0120: confirm NO LONGER awards the per-task credit. The credit + idea
15
+ // bonus now land at the confirmed→shipped flip (shipTask → awardLandingCredit,
16
+ // reason='task.shipped'); a task that strands at 'confirmed' pays nothing until
17
+ // its code is on `main`. So confirm must run ZERO credit_log INSERTs and return
18
+ // creditsAwarded === 0 — but it must STILL attribute shipped_by to the claim
19
+ // holder. The credit-at-ship half is pinned by tests/pay_on_land.mjs.
16
20
  //
17
21
  // confirmTask opens its own transaction via pool.connect(); the #538 `deps.pool`
18
22
  // injection seam (honored ONLY when NODE_ENV==='test') lets us drive it with a
@@ -27,16 +31,13 @@ import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
27
31
  process.env.NODE_ENV = 'test';
28
32
 
29
33
  const require = createRequire(import.meta.url);
30
- // Credit allocation carved into modules/economy/ (BV1.R77 / ADR 0093 §1-2):
31
- // confirmTask resolves the `reward` kernel port for the credit grant, so the
32
- // port must be registered before driving it. reward.js delegates to the live
33
- // achievements/credits objects (stubs would bite if used; confirmTask uses only
34
- // the credit path).
34
+ // confirmTask resolves the `reward` kernel port for the achievement reconcile
35
+ // (the credit path moved to ship), so the port must be registered before
36
+ // driving it. Stub the achievement hooks to no-ops.
35
37
  const achievements = require('../modules/economy/achievements.js');
36
38
  achievements.evaluateAfterShip = async () => [];
37
39
  achievements.evaluateAfterCreditAward = async () => [];
38
40
  achievements.evaluateAfterClaim = async () => [];
39
- // task 1679: confirmTask (the skip-grade path) now reconciles achievements.
40
41
  achievements.reconcileBuilderAchievements = async () => [];
41
42
  const seams = require('../src/module-seams.js');
42
43
  if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
@@ -48,10 +49,9 @@ const { test, summary } = makeRunner();
48
49
  // SQL-aware fake: returns rows by matching the statement, records every call so
49
50
  // the test can inspect the exact params each query received. Order-independent.
50
51
  // claimHolderId === null → simulate a task with NO claims row (fallback path)
51
- // priorCredit === true → a 'task.confirmed' row already exists (idempotent)
52
52
  // taskCompleted === false → the tasks UPDATE matches nothing (wrong state)
53
- function fakePool({ claimHolderId, task, priorCredit = false, taskCompleted = true }) {
54
- return makeSqlAwareClient((s, params) => {
53
+ function fakePool({ claimHolderId, task, taskCompleted = true }) {
54
+ return makeSqlAwareClient((s) => {
55
55
  if (/SELECT builder_id FROM claims/.test(s)) {
56
56
  return { rows: claimHolderId == null ? [] : [{ builder_id: claimHolderId }] };
57
57
  }
@@ -62,13 +62,7 @@ function fakePool({ claimHolderId, task, priorCredit = false, taskCompleted = tr
62
62
  : [],
63
63
  };
64
64
  }
65
- if (/SELECT 1 FROM credit_log .* 'task\.confirmed'/.test(s)) {
66
- return { rows: priorCredit ? [{ '?column?': 1 }] : [] };
67
- }
68
- if (/SELECT 1 FROM credit_log .* 'idea_promotion_bonus'/.test(s)) return { rows: [] };
69
- if (/FROM idea_inbox/.test(s)) return { rows: [] }; // no idea lineage
70
- if (/INSERT INTO credit_log/.test(s)) return { rows: [] };
71
- return { rows: [] }; // BEGIN / COMMIT / etc.
65
+ return { rows: [] }; // BEGIN / COMMIT / SAVEPOINT / etc.
72
66
  });
73
67
  }
74
68
 
@@ -82,19 +76,15 @@ const WORKER = 25; // the claim holder who actually did the work
82
76
  const TASK = { id: 660, title: 'fixture task', credits_reward: 15, kind: 'feature' };
83
77
 
84
78
  // =========================================================================
85
- await test('confirm: credit_log.builder_id is the CLAIM HOLDER, not the confirming Archon', async () => {
79
+ await test('confirm: pay-on-land — NO credit awarded at confirm, but shipped_by is the claim holder', async () => {
86
80
  const pool = fakePool({ claimHolderId: WORKER, task: TASK });
87
81
  const out = await confirmTask({ taskId: TASK.id, builderId: ARCHON }, { pool });
88
82
 
89
83
  assert.equal(out.taskStatus, 'confirmed', 'task should flip to confirmed');
90
84
  assert.equal(out.creditedBuilderId, WORKER, 'creditedBuilderId must be the claim holder');
91
- assert.ok(out.creditsAwarded > 0, 'a positive credit must be awarded');
92
-
93
- const credit = find(pool._client.queries, /INSERT INTO credit_log/);
94
- assert.ok(credit, 'a credit_log INSERT must run');
95
- assert.equal(credit.params[0], WORKER, 'credit_log.builder_id must be the claim holder');
96
- assert.notEqual(credit.params[0], ARCHON, 'credit_log.builder_id must NOT be the Archon');
97
- assert.equal(credit.params[1], TASK.id, 'credit_log.task_id must be the task');
85
+ assert.equal(out.creditsAwarded, 0, 'ADR 0120: no per-task credit lands at confirm');
86
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0,
87
+ 'confirm must run ZERO credit_log INSERTs — the reward waits for the land');
98
88
  });
99
89
 
100
90
  await test('confirm: tasks.shipped_by ($2 of the UPDATE) is attributed to the claim holder', async () => {
@@ -117,19 +107,13 @@ await test('confirm: the claim-holder lookup happens BEFORE the tasks UPDATE', a
117
107
  assert.ok(idxClaim < idxUpdate, 'claim holder must be resolved before the tasks UPDATE binds shipped_by');
118
108
  });
119
109
 
120
- await test('confirm: NO claim on the task → falls back to the confirmer (never refuses a legit confirm)', async () => {
110
+ await test('confirm: NO claim on the task → shipped_by falls back to the confirmer (never refuses a legit confirm)', async () => {
121
111
  const pool = fakePool({ claimHolderId: null, task: TASK });
122
112
  const out = await confirmTask({ taskId: TASK.id, builderId: ARCHON }, { pool });
123
113
  assert.equal(out.creditedBuilderId, ARCHON, 'with no claim, the confirmer is the only candidate');
124
- const credit = find(pool._client.queries, /INSERT INTO credit_log/);
125
- assert.equal(credit.params[0], ARCHON, 'fallback credit goes to the confirmer');
126
- });
127
-
128
- await test('confirm: idempotent — a prior task.confirmed credit means no second credit insert', async () => {
129
- const pool = fakePool({ claimHolderId: WORKER, task: TASK, priorCredit: true });
130
- const out = await confirmTask({ taskId: TASK.id, builderId: ARCHON }, { pool });
131
- assert.equal(out.creditsAwarded, 0, 'no fresh credit on a re-confirm');
132
- assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'must not double-insert credit');
114
+ const taskUpdate = find(pool._client.queries, /^UPDATE tasks/);
115
+ assert.equal(taskUpdate.params[1], ARCHON, 'shipped_by binds to the confirmer when there is no claim');
116
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'still no credit at confirm');
133
117
  });
134
118
 
135
119
  await test('confirm: transaction commits on the happy path (no rollback)', async () => {
@@ -31,6 +31,9 @@ const {
31
31
  computeSessionTokenReward,
32
32
  awardSessionTokenReward,
33
33
  REWARD_MARGIN_PCT,
34
+ REWARD_MODE_DEFAULT,
35
+ REWARD_MODE_COST_PLUS_ONLY,
36
+ rewardPolicy,
34
37
  } = require('../modules/economy/credits.js');
35
38
 
36
39
  const { test, summary } = makeRunner();
@@ -139,9 +142,17 @@ await test('insertMultipliedConfirmCredit: feature pays exactly raw', async () =
139
142
  assert.equal(res.raw, 40);
140
143
  assert.equal(client.queries.length, 1);
141
144
  assert.match(client.queries[0].sql, /INSERT INTO credit_log/);
142
- assert.match(client.queries[0].sql, /'task\.confirmed'/);
143
- // params: [builderId, taskId, delta, description]
144
- assert.deepEqual(client.queries[0].params, [1, 100, 40, '40c × 1.0 = 40c (feature)']);
145
+ // ADR 0120: reason is now a BOUND PARAM (default 'task.confirmed'), not a SQL
146
+ // literal, so the pay-on-land move can book 'task.shipped' at the land.
147
+ // params: [builderId, taskId, delta, reason, description]
148
+ assert.deepEqual(client.queries[0].params, [1, 100, 40, 'task.confirmed', '40c × 1.0 = 40c (feature)']);
149
+ });
150
+
151
+ await test('insertMultipliedConfirmCredit: reason param books task.shipped (ADR 0120 pay-on-land)', async () => {
152
+ const client = fakeClient();
153
+ await insertMultipliedConfirmCredit(client, 1, 100, 40, 'feature', 'task.shipped');
154
+ assert.equal(client.queries[0].params[3], 'task.shipped', 'reason param must reach credit_log.reason');
155
+ assert.equal(client.queries[0].params[4], '40c × 1.0 = 40c (feature)', 'description follows reason');
145
156
  });
146
157
 
147
158
  await test('insertMultipliedConfirmCredit: learning-capture pays 1.5×', async () => {
@@ -149,7 +160,7 @@ await test('insertMultipliedConfirmCredit: learning-capture pays 1.5×', async (
149
160
  const res = await insertMultipliedConfirmCredit(client, 1, 100, 40, 'learning-capture');
150
161
  assert.equal(res.creditsAwarded, 60);
151
162
  assert.equal(client.queries[0].params[2], 60);
152
- assert.equal(client.queries[0].params[3], '40c × 1.5 = 60c (learning-capture)');
163
+ assert.equal(client.queries[0].params[4], '40c × 1.5 = 60c (learning-capture)');
153
164
  });
154
165
 
155
166
  await test('insertMultipliedConfirmCredit: cleanup pays 0.8×', async () => {
@@ -273,9 +284,14 @@ await test('computeSessionTokenReward: $1.65 × 1.20 = 2 drachmae ($1=1 drachma)
273
284
  assert.equal(computeSessionTokenReward(100).drachmae, 120, '$100 × 1.20 = 120 drachmae ≈ dollars');
274
285
  });
275
286
 
287
+ // The watermark SELECT's row shape (task 1002474). `paid` = drachmae already booked
288
+ // for this session; `legacy` = rows predating the top-up column (reward_basis = 0).
289
+ const ledger = (paid = 0, legacy = 0) => [{ paid, legacy }];
290
+ const NO_PRIOR = ledger(0, 0);
291
+
276
292
  await test('awardSessionTokenReward: prices opus tokens server-side + inserts ONE credit_log row', async () => {
277
293
  const client = fakeClient();
278
- client.enqueue([]); // idempotency SELECT → no prior row
294
+ client.enqueue(NO_PRIOR); // watermark SELECT → nothing paid yet
279
295
  client.enqueue([{ id: 1 }]); // INSERT ... RETURNING id
280
296
  const res = await awardSessionTokenReward(client, 7, 'sess-abc', {
281
297
  'claude-opus-4-8': { input: 110_000 }, // 110k × $5/M = $0.55 (Opus 4.8, task 1343)
@@ -286,14 +302,16 @@ await test('awardSessionTokenReward: prices opus tokens server-side + inserts ON
286
302
  const insert = client.queries[1];
287
303
  assert.match(insert.sql, /INSERT INTO credit_log/);
288
304
  assert.match(insert.sql, /session\.token_reward/);
305
+ // params gained reward_basis (the cumulative owed total) as the last positional.
289
306
  assert.deepEqual(insert.params, [
290
- 7, 1, '$0.55 true cost × 1.20 = 1 drachmae (token reward)', 'sess-abc',
307
+ 7, 1, '$0.55 true cost × 1.20 = 1 drachmae (token reward)', 'sess-abc', 1,
291
308
  ]);
292
309
  });
293
310
 
294
- await test('awardSessionTokenReward: idempotent — prior row → no INSERT, no double payout', async () => {
311
+ await test('awardSessionTokenReward: idempotent — nothing further owed → no INSERT, no double payout', async () => {
295
312
  const client = fakeClient();
296
- client.enqueue([{}]); // SELECT finds a prior reward row for this session
313
+ // The session already collected everything it is owed at this cost level.
314
+ client.enqueue(ledger(1, 0));
297
315
  const res = await awardSessionTokenReward(client, 7, 'sess-abc', {
298
316
  'claude-opus-4-8': { input: 110_000 },
299
317
  });
@@ -304,7 +322,7 @@ await test('awardSessionTokenReward: idempotent — prior row → no INSERT, no
304
322
 
305
323
  await test('awardSessionTokenReward: ON CONFLICT race → INSERT returns no row → clean no-op', async () => {
306
324
  const client = fakeClient();
307
- client.enqueue([]); // SELECT → no prior
325
+ client.enqueue(NO_PRIOR);
308
326
  client.enqueue([]); // INSERT swallowed by ON CONFLICT DO NOTHING (RETURNING empty)
309
327
  const res = await awardSessionTokenReward(client, 7, 'sess-x', {
310
328
  'claude-opus-4-8': { input: 110_000 },
@@ -315,7 +333,7 @@ await test('awardSessionTokenReward: ON CONFLICT race → INSERT returns no row
315
333
 
316
334
  await test('awardSessionTokenReward: unknown model still pays (opus rate) + flags unknownModels', async () => {
317
335
  const client = fakeClient();
318
- client.enqueue([]);
336
+ client.enqueue(NO_PRIOR);
319
337
  client.enqueue([{ id: 2 }]);
320
338
  const res = await awardSessionTokenReward(client, 7, 'sess-u', { 'gpt-4o': { input: 1_000_000 } });
321
339
  assert.equal(res.awarded, true);
@@ -323,6 +341,100 @@ await test('awardSessionTokenReward: unknown model still pays (opus rate) + flag
323
341
  assert.ok(res.unknownModels.includes('gpt-4o'));
324
342
  });
325
343
 
344
+ // =========================================================================
345
+ // task 1002474 — a multi-ship session must be paid for ALL of its cost.
346
+ //
347
+ // The reward was a hard no-op once ANY row existed for the session_id, so a
348
+ // session that shipped a second task was paid only for the cost it had accrued
349
+ // at its FIRST ship. Measured: three ships from one session priced at $14.30 /
350
+ // $27.76 / $40.32 paid 17 drachmae total, where the full session was worth 48 —
351
+ // about 65% of real shipped work unrewarded. Multi-ship sessions are normal
352
+ // (one claim at a time, but ship → new worktree → claim again).
353
+ //
354
+ // The cure is watermark accounting: book the DIFFERENCE between what the whole
355
+ // session is now worth and what it has already been paid.
356
+ // =========================================================================
357
+
358
+ await test('1002474 a later, more expensive ship books only the INCREMENT', async () => {
359
+ const client = fakeClient();
360
+ client.enqueue(ledger(6, 0)); // already paid 6 for this session
361
+ client.enqueue([{ id: 9 }]);
362
+ // $20 of opus input = 4M tokens × $5/M → 20 × 1.20 = 24 owed in total.
363
+ const res = await awardSessionTokenReward(client, 7, 'sess-multi', {
364
+ 'claude-opus-4-8': { input: 4_000_000 },
365
+ });
366
+ assert.equal(res.awarded, true);
367
+ assert.equal(res.drachmae, 18, '24 owed − 6 already paid = 18, not a second full 24');
368
+ const insert = client.queries[1];
369
+ assert.equal(insert.params[1], 18, 'the ledger row carries the INCREMENT');
370
+ assert.equal(insert.params[4], 24, 'reward_basis is the CUMULATIVE owed total');
371
+ assert.match(insert.params[2], /top-up/i, 'the audit string must show this is a top-up');
372
+ });
373
+
374
+ await test('1002474 the running total never exceeds the full-session value', async () => {
375
+ // Three ships of one session, exactly the shape that was measured.
376
+ const usageFor = (dollars) => ({ 'claude-opus-4-8': { input: (dollars / 5) * 1_000_000 } });
377
+ let paid = 0;
378
+ for (const dollars of [14.30, 27.76, 40.32]) {
379
+ const client = fakeClient();
380
+ client.enqueue(ledger(paid, 0));
381
+ client.enqueue([{ id: 1 }]);
382
+ const res = await awardSessionTokenReward(client, 7, 'sess-3ship', usageFor(dollars));
383
+ paid += res.drachmae;
384
+ }
385
+ assert.equal(paid, Math.round(40.32 * 1.2), 'the session ends up paid its full cost-plus value');
386
+ assert.equal(paid, 48);
387
+ });
388
+
389
+ await test('1002474 a cheaper re-upload never claws back or pays again', async () => {
390
+ const client = fakeClient();
391
+ client.enqueue(ledger(48, 0)); // already fully paid at the $40.32 level
392
+ const res = await awardSessionTokenReward(client, 7, 'sess-3ship', {
393
+ 'claude-opus-4-8': { input: 1_000_000 }, // a stale/smaller digest → owes 6
394
+ });
395
+ assert.equal(res.awarded, false, 'owed − paid is negative → nothing happens');
396
+ assert.equal(res.drachmae, 0, 'never a negative credit_log row');
397
+ assert.equal(client.queries.length, 1, 'no INSERT at all');
398
+ });
399
+
400
+ await test('1002474 a LEGACY session (pre-migration row) is left alone for the backfill to decide', async () => {
401
+ const client = fakeClient();
402
+ // reward_basis = 0 marks a row booked before this change. Topping it up would be
403
+ // an uncontrolled retroactive payout; the backfill (task 1002448) owns that call.
404
+ client.enqueue(ledger(17, 1));
405
+ const res = await awardSessionTokenReward(client, 7, 'sess-legacy', {
406
+ 'claude-opus-4-8': { input: 8_000_000 }, // would otherwise owe far more
407
+ });
408
+ assert.equal(res.awarded, false, 'no surprise retroactive payout on deploy');
409
+ assert.equal(res.drachmae, 0);
410
+ assert.equal(res.settled, 'legacy', 'and it says WHY, so this is diagnosable');
411
+ assert.equal(client.queries.length, 1, 'no INSERT');
412
+ });
413
+
414
+ await test('1002474 a single-ship session pays exactly what it paid before (no regression)', async () => {
415
+ const client = fakeClient();
416
+ client.enqueue(NO_PRIOR);
417
+ client.enqueue([{ id: 3 }]);
418
+ const res = await awardSessionTokenReward(client, 7, 'sess-single', {
419
+ 'claude-opus-4-8': { input: 4_000_000 }, // $20 → 24
420
+ });
421
+ assert.equal(res.awarded, true);
422
+ assert.equal(res.drachmae, 24);
423
+ assert.equal(client.queries[1].params[1], 24, 'first payment is the whole amount, unchanged');
424
+ assert.doesNotMatch(client.queries[1].params[2], /top-up/i, 'a first payment is not labelled a top-up');
425
+ });
426
+
427
+ await test('1002474 the INSERT still carries a DB-level double-pay backstop', async () => {
428
+ const client = fakeClient();
429
+ client.enqueue(NO_PRIOR);
430
+ client.enqueue([{ id: 4 }]);
431
+ await awardSessionTokenReward(client, 7, 'sess-guard', { 'claude-opus-4-8': { input: 4_000_000 } });
432
+ const insert = client.queries[1];
433
+ assert.match(insert.sql, /ON CONFLICT/, 'the race guard must survive the rewrite');
434
+ assert.match(insert.sql, /DO NOTHING/);
435
+ assert.match(insert.sql, /reward_basis/, 'and it must be keyed on (session_id, reward_basis)');
436
+ });
437
+
326
438
  // task 1676 — the cost-plus zero-out fix for the dev-box/CI agent ship path.
327
439
  // Symptom (ADR 0097): those sessions uploaded empty/unpriceable model_usage, so
328
440
  // priceModelUsage returned $0 and the reward rounded to 0 — even though real
@@ -370,6 +482,82 @@ await test('REWARD_MARGIN_PCT exported as 20', () => {
370
482
  assert.equal(REWARD_MARGIN_PCT, 20);
371
483
  });
372
484
 
485
+ // =========================================================================
486
+ // (5) reward MODE — cost-plus-only vs cost-plus-and-estimate (task 1002270,
487
+ // ADR 0146). rewardPolicy() maps a raw branding.reward object → the
488
+ // effective policy (pure, so no branding()-cache juggling); the derived
489
+ // perTaskCredit boolean is what insertMultipliedConfirmCredit gates the
490
+ // per-task ESTIMATE payout on. The cost-plus session reward is untouched.
491
+ // =========================================================================
492
+
493
+ await test('rewardPolicy: default (no mode) → pay both streams; margin/peg resolve', () => {
494
+ const p = rewardPolicy({});
495
+ assert.equal(p.mode, REWARD_MODE_DEFAULT);
496
+ assert.equal(p.perTaskCredit, true, 'a default instance pays the per-task estimate');
497
+ assert.equal(p.marginPct, 20);
498
+ assert.equal(p.usdPeg, 1);
499
+ });
500
+
501
+ await test('rewardPolicy: mode "cost-plus-only" → per-task estimate payout OFF, cost-plus untouched', () => {
502
+ const p = rewardPolicy({ mode: 'cost-plus-only', marginPct: 20, usdPeg: 1 });
503
+ assert.equal(p.mode, REWARD_MODE_COST_PLUS_ONLY);
504
+ assert.equal(p.perTaskCredit, false, 'cost-plus-only suppresses the per-task estimate payout');
505
+ assert.equal(p.marginPct, 20, 'the cost-plus margin is unaffected — that stream is still paid');
506
+ assert.equal(p.usdPeg, 1);
507
+ });
508
+
509
+ await test('rewardPolicy: unknown/garbage mode → default (a typo never silently zeroes rewards)', () => {
510
+ for (const bad of ['costplus', 'COST-PLUS-ONLY', 'only', '', null, undefined, 42]) {
511
+ const p = rewardPolicy({ mode: bad });
512
+ assert.equal(p.perTaskCredit, true, `garbage mode ${JSON.stringify(bad)} must default to paying`);
513
+ assert.equal(p.mode, REWARD_MODE_DEFAULT);
514
+ }
515
+ assert.equal(rewardPolicy(null).perTaskCredit, true, 'a missing reward object → default');
516
+ assert.equal(rewardPolicy(undefined).perTaskCredit, true);
517
+ });
518
+
519
+ await test('rewardPolicy: cost-plus-only still honors a custom margin + peg', () => {
520
+ const p = rewardPolicy({ mode: 'cost-plus-only', marginPct: 35, usdPeg: 2 });
521
+ assert.equal(p.perTaskCredit, false);
522
+ assert.equal(p.marginPct, 35);
523
+ assert.equal(p.usdPeg, 2);
524
+ });
525
+
526
+ await test('insertMultipliedConfirmCredit: cost-plus-only mode writes NO credit_log row', async () => {
527
+ const client = fakeClient();
528
+ const res = await insertMultipliedConfirmCredit(
529
+ client, 7, 100, 60, 'feature', 'task.shipped', { perTaskCredit: false }
530
+ );
531
+ assert.equal(res.creditsAwarded, 0, 'no per-task estimate credit paid');
532
+ assert.equal(res.skipped, REWARD_MODE_COST_PLUS_ONLY, 'reports why it was skipped');
533
+ assert.equal(client.queries.length, 0, 'no INSERT (nor SELECT) — the estimate payout is fully suppressed');
534
+ assert.equal(res.raw, 60, 'raw + multiplier still reported for the audit shape');
535
+ assert.equal(res.multiplier, KIND_MULTIPLIERS.feature);
536
+ });
537
+
538
+ await test('insertMultipliedConfirmCredit: default/both mode still writes the row (behavior preserved)', async () => {
539
+ const client = fakeClient();
540
+ client.enqueue([{ id: 1 }]); // the INSERT
541
+ const res = await insertMultipliedConfirmCredit(
542
+ client, 7, 100, 60, 'feature', 'task.shipped', { perTaskCredit: true }
543
+ );
544
+ assert.equal(res.creditsAwarded, 60, '60 × 1.0 (feature) still paid when both streams are on');
545
+ assert.equal(client.queries.length, 1, 'the credit_log INSERT runs');
546
+ assert.match(client.queries[0].sql, /INSERT INTO credit_log/);
547
+ assert.equal(client.queries[0].params[3], 'task.shipped', 'booked under the pay-on-land reason');
548
+ });
549
+
550
+ await test('cost-plus-only cascade: a 0 per-task credit → idea-promotion slice is 0 (no bonus row)', async () => {
551
+ // In cost-plus-only mode the lifecycle's awardLandingCredit passes the (now 0)
552
+ // creditsAwarded into maybeAwardIdeaPromotionBonus, whose 25% slice of the
553
+ // unpaid estimate must therefore also be 0 — no lineage lookup, no bonus row.
554
+ // This mirrors the live lifecycle cascade without importing that module.
555
+ const client = fakeClient();
556
+ const res = await maybeAwardIdeaPromotionBonus(client, 100, 1, 0);
557
+ assert.equal(res.ideaBonusAwarded, 0);
558
+ assert.equal(client.queries.length, 0, 'no lineage lookup, no bonus INSERT when the base credit is 0');
559
+ });
560
+
373
561
  // -------------------------------------------------------------------------
374
562
  // runner summary
375
563
  // -------------------------------------------------------------------------