@bongos/core 1.18.3 → 1.18.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.bongos-core.json +1576 -286
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +4 -2
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +6 -1
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/migrate.sh +99 -14
  347. package/scripts/status-mirror-sync.js +114 -84
  348. package/src/bongos/api-errors.js +1 -0
  349. package/src/bongos/auth.js +447 -102
  350. package/src/bongos/db-kernel.js +27 -0
  351. package/src/bongos/db.js +39 -0
  352. package/src/bongos/module-scope-map.js +27 -1
  353. package/src/bongos/permission-path-check.js +136 -102
  354. package/src/bongos/repo-info.js +47 -11
  355. package/src/bongos/route-rank-check.js +429 -46
  356. package/src/bongos/routes/_helpers.js +12 -1
  357. package/src/bongos/routes/auth.js +219 -2
  358. package/src/bongos/routes/backup.js +58 -2
  359. package/src/bongos/routes/me.js +35 -1
  360. package/src/bongos/serve-internal.js +84 -15
  361. package/src/branding.js +32 -0
  362. package/src/instance-config.js +30 -0
  363. package/src/module-api.js +124 -1
  364. package/src/platform-server.js +86 -9
  365. package/tests/abandon_task.mjs +59 -3
  366. package/tests/api_cli_flags.mjs +53 -0
  367. package/tests/auth_page_gate.mjs +13 -0
  368. package/tests/backfill_sessions.mjs +127 -1
  369. package/tests/backup_cli.mjs +54 -0
  370. package/tests/bongos_box_connect.mjs +77 -0
  371. package/tests/box_access.mjs +130 -1
  372. package/tests/box_credential.mjs +125 -0
  373. package/tests/box_host_keys.mjs +24 -0
  374. package/tests/box_infra_resolve.mjs +149 -0
  375. package/tests/box_scope_session.mjs +6 -2
  376. package/tests/box_sync.mjs +55 -0
  377. package/tests/box_task_scope.mjs +99 -0
  378. package/tests/branding.mjs +12 -0
  379. package/tests/cf_ruleset_preflight.mjs +142 -0
  380. package/tests/cf_tunnel.mjs +37 -0
  381. package/tests/claim_gate_rebase.mjs +98 -0
  382. package/tests/clawback_confirmed_backlog.mjs +103 -0
  383. package/tests/cli_lib_formatters.mjs +20 -0
  384. package/tests/confirm_attribution.mjs +30 -46
  385. package/tests/credit_grant.mjs +198 -10
  386. package/tests/criterion_authoring_gate.mjs +272 -0
  387. package/tests/criterion_progress.mjs +33 -1
  388. package/tests/dev_lib.mjs +253 -0
  389. package/tests/devbox_artifact_gate.mjs +75 -0
  390. package/tests/devbox_dmg_backdrop.mjs +137 -0
  391. package/tests/devbox_icons.mjs +304 -0
  392. package/tests/devbox_installer_brand.mjs +232 -0
  393. package/tests/devbox_key_register.mjs +234 -0
  394. package/tests/docs_openapi_route.mjs +110 -0
  395. package/tests/doctor_hooks.mjs +141 -0
  396. package/tests/extract_json.mjs +140 -0
  397. package/tests/fitness.mjs +89 -0
  398. package/tests/gen_freshness.mjs +96 -0
  399. package/tests/goal_advisory.mjs +251 -0
  400. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  401. package/tests/goal_routes.mjs +59 -4
  402. package/tests/goal_tier.mjs +12 -3
  403. package/tests/governance_catalog.mjs +129 -0
  404. package/tests/governance_claim_eligibility.mjs +262 -0
  405. package/tests/governance_config.mjs +187 -0
  406. package/tests/governance_ownership_scope.mjs +410 -0
  407. package/tests/governance_page_gates.mjs +324 -0
  408. package/tests/governance_parity.mjs +336 -0
  409. package/tests/governance_principals.mjs +232 -0
  410. package/tests/governance_protected_surfaces.mjs +208 -0
  411. package/tests/governance_require_permission.mjs +223 -0
  412. package/tests/governance_resolver.mjs +84 -0
  413. package/tests/governance_role_sync.mjs +231 -0
  414. package/tests/governance_seed.mjs +127 -0
  415. package/tests/governance_session_scope.mjs +434 -0
  416. package/tests/governance_vanilla_boot.mjs +203 -0
  417. package/tests/grade_attribution.mjs +7 -7
  418. package/tests/hall_audit.mjs +223 -0
  419. package/tests/hall_nav.mjs +191 -0
  420. package/tests/hall_palette.mjs +206 -0
  421. package/tests/hall_signout.mjs +124 -0
  422. package/tests/hook_user_agents.mjs +10 -0
  423. package/tests/idea_triage.mjs +162 -0
  424. package/tests/idp_device_exchange.mjs +93 -0
  425. package/tests/init.mjs +51 -0
  426. package/tests/instance_config.mjs +65 -0
  427. package/tests/instance_mismatch_guard.mjs +23 -6
  428. package/tests/instance_release_config.mjs +104 -0
  429. package/tests/linkify_refs.mjs +15 -2
  430. package/tests/migrate_root_resolution.mjs +124 -1
  431. package/tests/module_contributions.mjs +55 -0
  432. package/tests/module_loader.mjs +10 -5
  433. package/tests/module_scope_active_claims.mjs +94 -0
  434. package/tests/needs_rebase.mjs +92 -0
  435. package/tests/one_active_claim_per_session.mjs +76 -0
  436. package/tests/override_attribution.mjs +10 -8
  437. package/tests/override_request_ownership.mjs +8 -4
  438. package/tests/parse_cookie_decode.mjs +42 -0
  439. package/tests/pay_on_land.mjs +129 -0
  440. package/tests/platform_boot.mjs +100 -1
  441. package/tests/post_login_landing.mjs +42 -0
  442. package/tests/preflight_checks.mjs +72 -0
  443. package/tests/provision.mjs +132 -7
  444. package/tests/publish_branch_route.mjs +53 -0
  445. package/tests/publish_reconciler.mjs +187 -2
  446. package/tests/repo_info_binding.mjs +248 -0
  447. package/tests/reward_gate.mjs +11 -2
  448. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  449. package/tests/reward_mode_surfacing.mjs +168 -0
  450. package/tests/route_permission_check.mjs +301 -0
  451. package/tests/route_rank_check.mjs +254 -12
  452. package/tests/route_validation.mjs +30 -0
  453. package/tests/sequence.mjs +267 -0
  454. package/tests/session_digest.mjs +44 -0
  455. package/tests/session_index.mjs +18 -6
  456. package/tests/session_records.mjs +39 -1
  457. package/tests/session_reward_on_land.mjs +381 -0
  458. package/tests/session_reward_reconcile.mjs +229 -0
  459. package/tests/ship_api_client_pathspec.mjs +100 -0
  460. package/tests/ship_base_flag.mjs +93 -0
  461. package/tests/ship_ci_deploy.mjs +80 -3
  462. package/tests/ship_upstream_deletion.mjs +57 -0
  463. package/tests/smoke_target.mjs +233 -0
  464. package/tests/start_env_guard.mjs +87 -0
  465. package/tests/start_rebase_warning.mjs +103 -0
  466. package/tests/status_mirror_sync.mjs +122 -0
  467. package/tests/stop_refcheck.mjs +19 -1
  468. package/tests/task_goal_link.mjs +15 -6
  469. package/tests/upgrade.mjs +139 -0
  470. package/tests/web_admission_status.mjs +162 -0
  471. package/tests/write_validation_gaps.mjs +110 -0
@@ -0,0 +1,229 @@
1
+ // tests/session_reward_reconcile.mjs
2
+ //
3
+ // Guards reconcileUnbookedSessionRewards — the STATE-BASED safety net for the
4
+ // deferred cost-plus session-token reward (ADR 0120). The two live triggers
5
+ // (upload-time gate + the in-process task.shipped listener) miss a task that
6
+ // reaches status='shipped' OUT OF BAND (direct-SQL / hotpatch / another process),
7
+ // stranding that session's drachmae forever. This sweep finds every session whose
8
+ // task is now genuinely 'shipped' but carries no session.token_reward row, and
9
+ // books it — idempotently, and STRICTLY only for shipped tasks (never 'confirmed').
10
+ //
11
+ // Same DB-free harness as session_reward_on_land.mjs: patch api.withTx to hand the
12
+ // fn a SQL-aware fake client, register the REAL economy `reward` port so the real
13
+ // awardSessionTokenReward SQL runs against the fake, and a guarded real-DB block
14
+ // proves the partial-unique index enforces exactly-once end-to-end.
15
+ //
16
+ // Run: node tests/session_reward_reconcile.mjs
17
+
18
+ import { strict as assert } from 'node:assert';
19
+ import { createRequire } from 'node:module';
20
+ import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
21
+
22
+ process.env.NODE_ENV = 'test';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const seams = require('../src/module-seams.js');
26
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
27
+ const api = require('../src/module-api.js');
28
+ const db = require('../modules/sessions/db.js');
29
+
30
+ const { test, summary } = makeRunner();
31
+
32
+ // Opus 110k input tokens → $0.55 true cost → $0.55 × 1.20 = 1 drachma (the same
33
+ // priceable fixture the reward tests use). A non-zero payout is what lets the
34
+ // idempotency SELECT + INSERT even run (awardSessionTokenReward short-circuits $0).
35
+ const MODEL_USAGE = { 'claude-opus-4-8': { input: 110_000 } };
36
+
37
+ const norm = (q) => String(q.sql).replace(/\s+/g, ' ').trim();
38
+ const find = (queries, re) => queries.find((q) => re.test(norm(q)));
39
+ const all = (queries, re) => queries.filter((q) => re.test(norm(q)));
40
+
41
+ // Drive reconcileUnbookedSessionRewards (which opens api.withTx(fn)) against a
42
+ // fake client whose query() answers come from `handler(normalizedSql)`.
43
+ async function withFakeTx(handler, run) {
44
+ const fake = makeSqlAwareClient(handler);
45
+ const prev = api.withTx;
46
+ api.withTx = (fn) => fn(fake._client);
47
+ try {
48
+ const result = await run();
49
+ return { result, queries: fake.queries };
50
+ } finally {
51
+ api.withTx = prev;
52
+ }
53
+ }
54
+
55
+ const SCAN_RE = /SELECT DISTINCT sr\.session_id.*JOIN tasks t.*status = 'shipped'/;
56
+ // task 1002474: the existence probe became a watermark SUM, so the fake answers
57
+ // with { paid, legacy } instead of row-present / row-absent.
58
+ const IDEMP_RE = /COALESCE\(SUM\(delta\)/;
59
+ const INSERT_RE = /INSERT INTO credit_log/;
60
+
61
+ // =========================================================================
62
+ // (1) Books a stranded reward for an already-shipped, unbooked session.
63
+ // =========================================================================
64
+
65
+ await test('reconcile: shipped session with no reward row → books exactly one reward', async () => {
66
+ const { result, queries } = await withFakeTx(
67
+ (s) => {
68
+ if (SCAN_RE.test(s)) {
69
+ return { rows: [{ session_id: 'sess-oob', builder_id: 7, model_usage: MODEL_USAGE, total_tokens: 5000 }] };
70
+ }
71
+ if (IDEMP_RE.test(s)) return { rows: [{ paid: 0, legacy: 0 }] }; // not yet booked
72
+ if (INSERT_RE.test(s)) return { rows: [{ id: 555 }] }; // insert won the row
73
+ return null; // UPDATE session_records mirror → {rows:[]}
74
+ },
75
+ () => db.reconcileUnbookedSessionRewards()
76
+ );
77
+
78
+ assert.ok(find(queries, SCAN_RE), 'the state-based scan (JOIN tasks status=shipped) must run');
79
+ assert.equal(all(queries, INSERT_RE).length, 1, 'exactly one session.token_reward INSERT booked');
80
+ assert.equal(result.scanned, 1, 'one unbooked shipped session scanned');
81
+ assert.equal(result.awarded, 1, 'one reward freshly booked');
82
+ // The mirror UPDATE ran (observability column), keyed on the same session.
83
+ assert.ok(find(queries, /UPDATE session_records\s+SET token_reward_drachmae/), 'the drachmae is mirrored onto the row');
84
+ });
85
+
86
+ // =========================================================================
87
+ // (2) Nothing to do — no unbooked shipped sessions.
88
+ // =========================================================================
89
+
90
+ await test('reconcile: no unbooked shipped sessions → no INSERT, awarded 0', async () => {
91
+ const { result, queries } = await withFakeTx(
92
+ (s) => (SCAN_RE.test(s) ? { rows: [] } : null),
93
+ () => db.reconcileUnbookedSessionRewards()
94
+ );
95
+ assert.equal(all(queries, INSERT_RE).length, 0, 'nothing shipped-and-unbooked → no reward booked');
96
+ assert.equal(result.scanned, 0);
97
+ assert.equal(result.awarded, 0);
98
+ });
99
+
100
+ // =========================================================================
101
+ // (3) Idempotency backstop — a stale scan that races a live trigger must not
102
+ // double-pay. Even if the scan hands back a session, the per-session_id
103
+ // guard inside awardSessionTokenReward sees the existing row and no-ops.
104
+ // =========================================================================
105
+
106
+ await test('reconcile: scan races a live award (row already exists) → no second INSERT', async () => {
107
+ const { result, queries } = await withFakeTx(
108
+ (s) => {
109
+ if (SCAN_RE.test(s)) {
110
+ return { rows: [{ session_id: 'sess-race', builder_id: 7, model_usage: MODEL_USAGE, total_tokens: 5000 }] };
111
+ }
112
+ if (IDEMP_RE.test(s)) return { rows: [{ paid: 999_999, legacy: 0 }] }; // ALREADY booked between scan + award
113
+ return null;
114
+ },
115
+ () => db.reconcileUnbookedSessionRewards()
116
+ );
117
+ assert.equal(all(queries, INSERT_RE).length, 0, 'the per-session_id guard blocks a double payout');
118
+ assert.equal(result.scanned, 1, 'the stale row was still scanned');
119
+ assert.equal(result.awarded, 0, 'but nothing was freshly booked');
120
+ });
121
+
122
+ // =========================================================================
123
+ // (4) The scan is bounded — a garbage/huge limit is clamped, never an
124
+ // unbounded table scan (params[0] is the clamped LIMIT).
125
+ // =========================================================================
126
+
127
+ await test('reconcile: limit is clamped to [1, 1000]', async () => {
128
+ for (const [input, expected] of [[99999, 1000], [0, 1], [-5, 1], [undefined, 200], ['50', 50]]) {
129
+ const { queries } = await withFakeTx(
130
+ (s) => (SCAN_RE.test(s) ? { rows: [] } : null),
131
+ () => db.reconcileUnbookedSessionRewards(input === undefined ? undefined : { limit: input })
132
+ );
133
+ const scan = find(queries, SCAN_RE);
134
+ assert.ok(scan, 'the scan ran');
135
+ assert.equal(scan.params[0], expected, `limit ${input} → clamped to ${expected}`);
136
+ }
137
+ });
138
+
139
+ // =========================================================================
140
+ // (5) real-DB — proves the sweep books once, is idempotent (partial index),
141
+ // and NEVER pays a task that is only 'confirmed' (ADR 0120 pay-on-land).
142
+ // Requires DATABASE_URL; skipped cleanly otherwise.
143
+ // =========================================================================
144
+
145
+ if (process.env.DATABASE_URL) {
146
+ const pool = api.pool;
147
+ const suffix = `srr_${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
148
+ const ctx = { builderId: null, versionId: `ver_${suffix}`, shippedTaskId: null, confirmedTaskId: null };
149
+ let setupOk = false;
150
+ try {
151
+ const b = await pool.query(
152
+ `INSERT INTO builders (github_id, github_login, display_name) VALUES ($1,$2,$3) RETURNING id`,
153
+ [`gh_${suffix}`, `login_${suffix}`, `Test ${suffix}`]
154
+ );
155
+ ctx.builderId = Number(b.rows[0].id);
156
+ await pool.query(
157
+ `INSERT INTO versions (id, name, track, status) VALUES ($1,$2,'internal','planning')`,
158
+ [ctx.versionId, `V ${suffix}`]
159
+ );
160
+ const shipped = await pool.query(
161
+ `INSERT INTO tasks (version_id, title, status) VALUES ($1,$2,'shipped') RETURNING id`,
162
+ [ctx.versionId, `shipped ${suffix}`]
163
+ );
164
+ ctx.shippedTaskId = Number(shipped.rows[0].id);
165
+ const confirmed = await pool.query(
166
+ `INSERT INTO tasks (version_id, title, status) VALUES ($1,$2,'confirmed') RETURNING id`,
167
+ [ctx.versionId, `confirmed ${suffix}`]
168
+ );
169
+ ctx.confirmedTaskId = Number(confirmed.rows[0].id);
170
+ setupOk = true;
171
+ } catch (e) {
172
+ console.log(` SKIP real-DB block (setup failed: ${e.message})`);
173
+ }
174
+
175
+ if (setupOk) {
176
+ const rewardRows = (sessionId) => pool.query(
177
+ `SELECT delta FROM credit_log WHERE session_id = $1 AND reason = 'session.token_reward'`,
178
+ [sessionId]
179
+ );
180
+ const sidShipped = `sess-recon-shipped-${suffix}`;
181
+ const sidConfirmed = `sess-recon-confirmed-${suffix}`;
182
+ try {
183
+ await test('real-DB: reconcile books a stranded shipped session once, idempotently', async () => {
184
+ // A session whose task is shipped but which never booked (the out-of-band case).
185
+ await pool.query(
186
+ `INSERT INTO session_records (session_id, builder_id, task_ids, total_tokens, model_usage)
187
+ VALUES ($1, $2, ARRAY[$3]::bigint[], $4, $5::jsonb)`,
188
+ [sidShipped, ctx.builderId, ctx.shippedTaskId, 5000, JSON.stringify(MODEL_USAGE)]
189
+ );
190
+ assert.equal((await rewardRows(sidShipped)).rows.length, 0, 'precondition: no reward yet');
191
+
192
+ const first = await db.reconcileUnbookedSessionRewards();
193
+ assert.ok(first.awarded >= 1, 'the sweep booked at least the shipped session');
194
+ assert.equal((await rewardRows(sidShipped)).rows.length, 1, 'exactly one reward row for the shipped session');
195
+
196
+ const second = await db.reconcileUnbookedSessionRewards();
197
+ // second sweep may scan 0 (already booked) — the invariant is no double-pay.
198
+ assert.equal((await rewardRows(sidShipped)).rows.length, 1, 'still exactly one — the partial index held');
199
+ assert.ok(second.awarded === 0 || second.scanned === 0, 're-sweep never double-pays');
200
+ });
201
+
202
+ await test('real-DB: reconcile NEVER pays a task that is only confirmed (pay-on-land)', async () => {
203
+ await pool.query(
204
+ `INSERT INTO session_records (session_id, builder_id, task_ids, total_tokens, model_usage)
205
+ VALUES ($1, $2, ARRAY[$3]::bigint[], $4, $5::jsonb)`,
206
+ [sidConfirmed, ctx.builderId, ctx.confirmedTaskId, 5000, JSON.stringify(MODEL_USAGE)]
207
+ );
208
+ await db.reconcileUnbookedSessionRewards();
209
+ assert.equal((await rewardRows(sidConfirmed)).rows.length, 0,
210
+ 'a confirmed-only task is never paid — the reward waits for a genuine shipped');
211
+ });
212
+ } finally {
213
+ try {
214
+ await pool.query(`DELETE FROM credit_log WHERE builder_id = $1`, [ctx.builderId]);
215
+ await pool.query(`DELETE FROM session_records WHERE builder_id = $1`, [ctx.builderId]);
216
+ await pool.query(`DELETE FROM tasks WHERE version_id = $1`, [ctx.versionId]);
217
+ await pool.query(`DELETE FROM versions WHERE id = $1`, [ctx.versionId]);
218
+ await pool.query(`DELETE FROM builders WHERE id = $1`, [ctx.builderId]);
219
+ } catch (e) {
220
+ console.log(` WARN real-DB teardown left rows (${e.message})`);
221
+ }
222
+ }
223
+ }
224
+ } else {
225
+ console.log(' SKIP real-DB block (no DATABASE_URL)');
226
+ }
227
+
228
+ // -------------------------------------------------------------------------
229
+ summary();
@@ -0,0 +1,100 @@
1
+ // tests/ship_api_client_pathspec.mjs
2
+ //
3
+ // Unit tests for task 1002417 — ship.js named a git path that could not exist,
4
+ // and it broke EVERY ship.
5
+ //
6
+ // regenerateApiClient() stages the regenerated typed client plus the devbox
7
+ // Electron app's vendored CJS copy. After the app moved from a top-level
8
+ // devbox-app/ to modules/dev-box/app/ (ADR 0145) the vendored path went stale.
9
+ // The failure was asymmetric and therefore silent for a long time:
10
+ //
11
+ // git status --porcelain <missing path> → exit 0 (tolerated, so no signal)
12
+ // git add <missing path> → exit 128 (aborts the WHOLE stage)
13
+ //
14
+ // Because the `git add` aborts, the freshly regenerated clients/bongos-client is
15
+ // never staged, the worktree stays dirty, and the ship's auto-merge then bails
16
+ // with "main worktree busy" — stranding the task at `confirmed` with no PR.
17
+ //
18
+ // apiClientPathspec is the pure rule behind that: never name a path `git add`
19
+ // cannot match. Proven here with an injected existsSync — no git, no fs.
20
+ //
21
+ // Run: node tests/ship_api_client_pathspec.mjs
22
+
23
+ import { strict as assert } from 'node:assert';
24
+ import { createRequire } from 'node:module';
25
+ import path from 'node:path';
26
+
27
+ const require = createRequire(import.meta.url);
28
+ const ship = require('../scripts/gds/ship.js');
29
+ const { apiClientPathspec, VENDORED_CLIENT_REL } = ship;
30
+
31
+ let passed = 0;
32
+ let failed = 0;
33
+ function test(name, fn) {
34
+ try {
35
+ fn();
36
+ passed++;
37
+ console.log(` ok ${name}`);
38
+ } catch (err) {
39
+ failed++;
40
+ console.log(` FAIL ${name}`);
41
+ console.log(` ${err.message}`);
42
+ }
43
+ }
44
+
45
+ const ROOT = path.join('/repo');
46
+ const never = () => false;
47
+ const always = () => true;
48
+
49
+ test('the vendored copy lives inside the dev-box module, not at the old top level', () => {
50
+ assert.equal(VENDORED_CLIENT_REL, 'modules/dev-box/app/src/vendor/bongos-client.cjs');
51
+ assert.ok(!VENDORED_CLIENT_REL.startsWith('devbox-app/'), 'must not use the pre-ADR-0145 path');
52
+ });
53
+
54
+ test('the generated client is always staged', () => {
55
+ assert.ok(apiClientPathspec({ worktreeRoot: ROOT, existsSync: never }).includes('clients/bongos-client'));
56
+ assert.ok(apiClientPathspec({ worktreeRoot: ROOT, existsSync: always }).includes('clients/bongos-client'));
57
+ });
58
+
59
+ test('vendored copy present → it is staged too', () => {
60
+ const p = apiClientPathspec({ worktreeRoot: ROOT, existsSync: always });
61
+ assert.deepEqual(p, ['clients/bongos-client', VENDORED_CLIENT_REL]);
62
+ });
63
+
64
+ // The regression. Before the fix this returned the vendored path unconditionally,
65
+ // so `git add` exited 128 and left the worktree dirty.
66
+ test('vendored copy ABSENT → it is omitted (git add would abort on it)', () => {
67
+ const p = apiClientPathspec({ worktreeRoot: ROOT, existsSync: never });
68
+ assert.deepEqual(p, ['clients/bongos-client']);
69
+ assert.ok(!p.some((x) => x.includes('vendor')), 'no unmatched pathspec may reach git add');
70
+ });
71
+
72
+ test('existence is probed at the vendored path under the given worktree root', () => {
73
+ const seen = [];
74
+ apiClientPathspec({
75
+ worktreeRoot: ROOT,
76
+ existsSync: (p) => { seen.push(p); return false; },
77
+ });
78
+ assert.equal(seen.length, 1);
79
+ assert.equal(seen[0], path.join(ROOT, VENDORED_CLIENT_REL));
80
+ });
81
+
82
+ // A ship can run from a linked worktree, where the main checkout is a different
83
+ // tree (the task-2060 rationale). The probe must follow the root it is handed.
84
+ test('probes the worktree it was handed, not a fixed root', () => {
85
+ const seen = [];
86
+ apiClientPathspec({
87
+ worktreeRoot: path.join('/linked-worktree'),
88
+ existsSync: (p) => { seen.push(p); return false; },
89
+ });
90
+ assert.equal(seen[0], path.join('/linked-worktree', VENDORED_CLIENT_REL));
91
+ });
92
+
93
+ test('every returned entry is repo-relative (git pathspecs, not absolute paths)', () => {
94
+ for (const p of apiClientPathspec({ worktreeRoot: ROOT, existsSync: always })) {
95
+ assert.ok(!path.isAbsolute(p), `${p} must be repo-relative`);
96
+ }
97
+ });
98
+
99
+ console.log(`\n${passed} passed, ${failed} failed`);
100
+ process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,93 @@
1
+ // tests/ship_base_flag.mjs — task 1002422 (blocker 1000082).
2
+ //
3
+ // ship.js --base <ref>: grade a task whose commits ALREADY landed on main via the
4
+ // RANK-OPEN graded path, instead of dead-ending at --allow-empty (which forces
5
+ // skip-grade → the Archon-only /confirm no sub-Archon builder can finish). These
6
+ // are DB-free unit tests of the two pure pieces: the ref validator (resolveBaseRef)
7
+ // and the baseline override (computeGraderBaseline).
8
+
9
+ import assert from 'node:assert/strict';
10
+ import { test } from 'node:test';
11
+ import { createRequire } from 'node:module';
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const ship = require('../scripts/gds/ship.js');
15
+ const { resolveBaseRef, computeGraderBaseline, baseRefShipGuard } = ship;
16
+
17
+ test('resolveBaseRef: rejects empty / whitespace-only refs before touching git', () => {
18
+ // No git runner passed → if these reached git the test would hit a real repo.
19
+ // They must short-circuit on validation instead.
20
+ assert.equal(resolveBaseRef('').ok, false);
21
+ assert.equal(resolveBaseRef(' ').ok, false);
22
+ assert.equal(resolveBaseRef(null).ok, false);
23
+ assert.equal(resolveBaseRef(undefined).ok, false);
24
+ });
25
+
26
+ test('resolveBaseRef: rejects injection / flag-like / malformed refs (no shell reaches git)', () => {
27
+ for (const bad of ['-rf', '--upload-pack=evil', 'a b', 'a;rm -rf /', '$(evil)', '`evil`', 'a|b', 'a&&b', 'a>b', "a'b", 'a"b', '.hidden']) {
28
+ const r = resolveBaseRef(bad);
29
+ assert.equal(r.ok, false, `expected reject for ${JSON.stringify(bad)}`);
30
+ assert.match(r.reason, /invalid ref syntax/);
31
+ }
32
+ });
33
+
34
+ test('resolveBaseRef: a syntactically-valid ref git cannot resolve is rejected', () => {
35
+ const r = resolveBaseRef('deadbeef', { revParse: () => null, isAncestor: () => true });
36
+ assert.equal(r.ok, false);
37
+ assert.match(r.reason, /not a commit/);
38
+ });
39
+
40
+ test('resolveBaseRef: a valid commit that is NOT an ancestor of HEAD is rejected', () => {
41
+ // Guards against grading against a sibling/unrelated branch tip.
42
+ let sawSha = null;
43
+ const r = resolveBaseRef('some-branch', {
44
+ revParse: () => 'abc123def4567890',
45
+ isAncestor: (s) => { sawSha = s; return false; },
46
+ });
47
+ assert.equal(r.ok, false);
48
+ assert.match(r.reason, /not an ancestor of HEAD/);
49
+ assert.equal(sawSha, 'abc123def4567890', 'ancestry is checked against the RESOLVED sha, not the raw ref');
50
+ });
51
+
52
+ test('resolveBaseRef: a valid ancestor commit resolves to its sha', () => {
53
+ const r = resolveBaseRef('HEAD~1', {
54
+ revParse: () => 'fedcba9876543210',
55
+ isAncestor: () => true,
56
+ });
57
+ assert.deepEqual(r, { ok: true, sha: 'fedcba9876543210' });
58
+ });
59
+
60
+ test('resolveBaseRef: accepts normal ref shapes (sha, branch/slash, HEAD~n)', () => {
61
+ const stub = { revParse: () => 'cafebabecafebabe', isAncestor: () => true };
62
+ for (const good of ['ddace22', 'ddace226dcf13111', 'main', 'origin/main', 'HEAD~3', 'v1.18.15', 'release/1.18']) {
63
+ assert.equal(resolveBaseRef(good, stub).ok, true, `expected accept for ${good}`);
64
+ }
65
+ });
66
+
67
+ test('baseRefShipGuard: --base allowed ONLY when HEAD is 0 commits ahead of origin/main', () => {
68
+ // The security invariant. --base grades <base>...HEAD, but autoMerge lands the
69
+ // WHOLE branch — so if HEAD is ahead of main, unreviewed commits before <base>
70
+ // could reach main. Anything but exactly '0' ahead must refuse.
71
+ assert.equal(baseRefShipGuard('0').ok, true);
72
+ for (const ahead of ['1', '2', '17', '']) {
73
+ const g = baseRefShipGuard(ahead);
74
+ assert.equal(g.ok, false, `expected refuse at ahead=${JSON.stringify(ahead)}`);
75
+ assert.match(g.reason, /ahead of origin\/main/);
76
+ }
77
+ // Fail-safe: an unknown count (git rev-list failed / origin/main absent) refuses,
78
+ // never default-allows.
79
+ assert.equal(baseRefShipGuard(null).ok, false);
80
+ assert.equal(baseRefShipGuard(undefined).ok, false);
81
+ });
82
+
83
+ test('computeGraderBaseline: an explicit baseSha overrides merge-base (no git call)', () => {
84
+ // The --base override path: given a resolved sha, the baseline is that sha —
85
+ // computeGraderBaseline must NOT fall through to git merge-base. Passing a
86
+ // bogus claim proves it never reads claim.head_sha when baseSha is set. This is
87
+ // the whole point of --base: a real, non-empty <base>...HEAD diff for the grader.
88
+ assert.equal(computeGraderBaseline({ head_sha: 'IGNORED' }, 'MYBASESHA'), 'MYBASESHA');
89
+ // A falsy override does NOT short-circuit — it falls through to the normal
90
+ // merge-base logic (exercised against the live repo, so we only assert it did
91
+ // not wrongly echo the override sentinel back).
92
+ assert.notEqual(computeGraderBaseline({ head_sha: 'CLAIMSHA' }, null), 'MYBASESHA');
93
+ });
@@ -131,6 +131,60 @@ test('resolveDeployTarget: blank/whitespace config values are treated as unset',
131
131
  assert.equal(r.healthzUrl, null);
132
132
  });
133
133
 
134
+ // ---- 1c. resolveInstanceBase precedence + the "no hardcoded host" guarantee (task 1002232) ----
135
+ // The ci-land /version + /healthz probes, the ci PR-body deploy note, and the
136
+ // manual-confirm curl guidance all baked in `https://amazonprimea.com`, so a
137
+ // ci-land / manual-confirm on a migrated instance pointed at the WRONG box. The
138
+ // helper derives the origin: env → session api_base → branding publicOrigin → null.
139
+ // Every case injects env/sessionApiBase/branding so the machine's own env + session
140
+ // file can't leak in (the resolveDeployTarget test convention).
141
+
142
+ const { resolveInstanceBase } = ship;
143
+
144
+ test('resolveInstanceBase: env <PREFIX>_API_BASE wins over session + branding', () => {
145
+ assert.equal(
146
+ resolveInstanceBase({
147
+ env: { GDS_API_BASE: 'https://env.example' },
148
+ sessionApiBase: 'https://session.example',
149
+ branding: { domains: { publicOrigin: 'https://brand.example' } },
150
+ }),
151
+ 'https://env.example');
152
+ });
153
+
154
+ test('resolveInstanceBase: session api_base wins over branding when no env override', () => {
155
+ assert.equal(
156
+ resolveInstanceBase({
157
+ env: {},
158
+ sessionApiBase: 'https://session.example',
159
+ branding: { domains: { publicOrigin: 'https://brand.example' } },
160
+ }),
161
+ 'https://session.example');
162
+ });
163
+
164
+ test('resolveInstanceBase: falls back to branding publicOrigin when no env + no session', () => {
165
+ assert.equal(
166
+ resolveInstanceBase({ env: {}, sessionApiBase: null, branding: { domains: { publicOrigin: 'https://cloudbongos.com' } } }),
167
+ 'https://cloudbongos.com');
168
+ });
169
+
170
+ test('resolveInstanceBase: NO hardcoded host — nothing resolvable → null (never a founder default)', () => {
171
+ assert.equal(resolveInstanceBase({ env: {}, sessionApiBase: null, branding: null }), null);
172
+ assert.equal(resolveInstanceBase({ env: {}, sessionApiBase: null, branding: {} }), null);
173
+ });
174
+
175
+ test('resolveInstanceBase: trailing slashes are trimmed to a bare origin', () => {
176
+ assert.equal(
177
+ resolveInstanceBase({ env: {}, sessionApiBase: 'https://session.example/', branding: null }),
178
+ 'https://session.example');
179
+ assert.equal(
180
+ resolveInstanceBase({ env: {}, sessionApiBase: null, branding: { domains: { publicOrigin: 'https://brand.example//' } } }),
181
+ 'https://brand.example');
182
+ });
183
+
184
+ test('resolveInstanceBase: blank/whitespace session + branding values are treated as unset', () => {
185
+ assert.equal(resolveInstanceBase({ env: {}, sessionApiBase: ' ', branding: { domains: { publicOrigin: '' } } }), null);
186
+ });
187
+
134
188
  test('parseDeployMode: SAFE DEFAULT laptop on malformed config JSON', () => {
135
189
  assert.equal(parseDeployMode(undefined, '{not json'), 'laptop');
136
190
  });
@@ -243,10 +297,20 @@ test('deployRunSucceeded: false on empty/missing inputs', () => {
243
297
 
244
298
  // ---- 5. the committed switch is LIVE on 'ci' (cutover landed: task 859, ADR 0042) ----
245
299
 
246
- test('config/deploy.json ships as ci (builder self-deploy cutover is ON)', () => {
247
- const raw = fs.readFileSync(path.join(__dirname, '..', 'config', 'deploy.json'), 'utf8');
300
+ test('config/deploy.json — when present ships as ci; absent is fine on the neutral core', () => {
301
+ // The neutral core carries NO config/deploy.json (it's an instance-owned switch;
302
+ // in the PUBLISH_ALLOWLIST so an instance CAN ship one, but the core has none).
303
+ // Reading it unconditionally ENOENT-failed on a clean core checkout (task 1002232);
304
+ // tolerate absence — when the file is missing, deploy mode resolves from env /
305
+ // effectiveDeployMode at runtime, which is correct, not a failure.
306
+ const p = path.join(__dirname, '..', 'config', 'deploy.json');
307
+ if (!fs.existsSync(p)) {
308
+ console.log(' (skip — no config/deploy.json in this checkout; neutral-core default)');
309
+ return;
310
+ }
311
+ const raw = fs.readFileSync(p, 'utf8');
248
312
  assert.equal(parseDeployMode(undefined, raw), 'ci',
249
- 'config/deploy.json ships as ci — builders self-deploy via PR auto-merge + Actions (task 859). ' +
313
+ 'config/deploy.json, when present, ships as ci — builders self-deploy via PR auto-merge + Actions (task 859). ' +
250
314
  'To roll back to operator-laptop deploys, set mode "laptop" AND disarm the PROD_DEPLOY_VIA_CI repo var.');
251
315
  });
252
316
 
@@ -309,6 +373,19 @@ test('classifyLandOutcome: empty/none status → pending (never a false failure)
309
373
  assert.equal(classifyLandOutcome({ pr_state: 'none', merged: false, deploy_status: 'none' }), 'pending');
310
374
  });
311
375
 
376
+ // task 1002472 — an instance with no deploy-prod.yml reports deploy_status
377
+ // 'unconfigured'. Without this, the ship's own in-session poll reports 'pending'
378
+ // forever on such an instance even after the server-side reconciler was fixed.
379
+ // Must stay in step with github-push.landConfirmed (this CLI can't import it).
380
+ test('classifyLandOutcome: merged + deploy unconfigured → shipped (no deploy workflow to wait for)', () => {
381
+ assert.equal(classifyLandOutcome({ pr_state: 'closed', merged: true, deploy_status: 'unconfigured' }), 'shipped');
382
+ });
383
+
384
+ test('classifyLandOutcome: NOT merged + deploy unconfigured → pending, never shipped', () => {
385
+ assert.equal(classifyLandOutcome({ pr_state: 'open', merged: false, deploy_status: 'unconfigured' }), 'pending');
386
+ assert.equal(classifyLandOutcome({ pr_state: 'closed', merged: false, deploy_status: 'unconfigured' }), 'failed');
387
+ });
388
+
312
389
  // ---- 8. strandNextStep: box-aware recovery guidance, never gh-only /merge-mode on a box (task 1052) ----
313
390
  // The #1027 retrospective: a confirmed-strand on a dev box pointed the builder at
314
391
  // /merge-mode, which needs gh + a main checkout a box lacks — a dead end.
@@ -0,0 +1,57 @@
1
+ // tests/ship_upstream_deletion.mjs
2
+ //
3
+ // task 1002475 — preflight's WARN-ONLY upstream-deletion step (step (d)): main
4
+ // can delete or relocate a surface a claim branch's diff still touches (the R89
5
+ // hall-ui-cutover class). The git I/O (fetch, `git log --diff-filter=D`) is
6
+ // covered by the live ship smoke; here we pin down the pure seam that turns the
7
+ // raw `git log --diff-filter=D --name-only --pretty=format:` output into the
8
+ // deduped path list the warning is built from — the only part that can
9
+ // silently drop or duplicate a hit.
10
+ //
11
+ // Pure function — no git, no filesystem, no network.
12
+ //
13
+ // Run: node tests/ship_upstream_deletion.mjs
14
+
15
+ import { strict as assert } from 'node:assert';
16
+ import { createRequire } from 'node:module';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const { parseDeletedUpstreamPaths } = require('../scripts/gds/ship.js');
20
+
21
+ let passed = 0;
22
+ let failed = 0;
23
+ function t(name, fn) {
24
+ try { fn(); passed++; console.log(` PASS ${name}`); }
25
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
26
+ }
27
+
28
+ t('null / empty input → [] (no fetch, or nothing deleted)', () => {
29
+ assert.deepEqual(parseDeletedUpstreamPaths(null), []);
30
+ assert.deepEqual(parseDeletedUpstreamPaths(''), []);
31
+ });
32
+
33
+ t('a single deleted path', () => {
34
+ assert.deepEqual(parseDeletedUpstreamPaths('public-builders/index.html'), ['public-builders/index.html']);
35
+ });
36
+
37
+ t('multiple commits: blank lines between them are dropped', () => {
38
+ const raw = '\npublic-builders/index.html\npublic-builders/style.css\n\nold/thing.js\n';
39
+ assert.deepEqual(parseDeletedUpstreamPaths(raw), [
40
+ 'public-builders/index.html',
41
+ 'public-builders/style.css',
42
+ 'old/thing.js',
43
+ ]);
44
+ });
45
+
46
+ t('the same path deleted across multiple commits dedupes, order preserved', () => {
47
+ const raw = 'a.js\nb.js\na.js\n';
48
+ assert.deepEqual(parseDeletedUpstreamPaths(raw), ['a.js', 'b.js']);
49
+ });
50
+
51
+ t('whitespace-only lines are dropped alongside blanks', () => {
52
+ const raw = 'a.js\n \nb.js\n';
53
+ assert.deepEqual(parseDeletedUpstreamPaths(raw), ['a.js', 'b.js']);
54
+ });
55
+
56
+ console.log(`\nship.js upstream-deletion preflight (task 1002475) tests: ${passed} passed, ${failed} failed`);
57
+ process.exit(failed > 0 ? 1 : 0);