@bongos/core 1.18.0 → 1.18.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (553) hide show
  1. package/.bongos-core.json +1740 -640
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +5 -3
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +11 -2
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  27. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  28. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  29. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  30. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  31. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  32. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  33. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  34. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  35. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  36. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  37. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  38. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  39. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  40. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  41. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  42. package/docs/adr/README.md +15 -2
  43. package/docs/api/openapi.json +2648 -483
  44. package/docs/api-reference.md +73 -13
  45. package/docs/architecture.md +4 -2
  46. package/docs/branding-contract.md +3 -1
  47. package/docs/canonical-permissions.md +21 -8
  48. package/docs/file-map.md +15 -7
  49. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  50. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  51. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  52. package/docs/onboarding/diagrams/assertions.json +4 -7
  53. package/docs/onboarding/drachmae-and-karma.md +3 -3
  54. package/docs/onboarding/primer.md +17 -16
  55. package/docs/onboarding/slash-commands.md +124 -0
  56. package/docs/recipes/builder-box-lifecycle.md +23 -0
  57. package/docs/recipes/devbox-app-release.md +14 -4
  58. package/docs/recipes/instance-first-run.md +16 -0
  59. package/docs/recipes/local-dev.md +81 -0
  60. package/docs/recipes/ops-gotchas.md +19 -1
  61. package/docs/recipes/overnight-builder.md +1 -1
  62. package/docs/recipes/packaging-the-core.md +13 -0
  63. package/docs/recipes/parallel-goal-waves.md +174 -0
  64. package/docs/recipes/ship-and-regrade.md +2 -2
  65. package/docs/recipes/standalone-live-docs.md +48 -0
  66. package/docs/recipes/upgrading-the-core.md +5 -2
  67. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  68. package/migrations/003_pms.sql +6 -7
  69. package/migrations/006_pms_v2.sql +3 -31
  70. package/migrations/039_newcomer_friendly_column.sql +5 -0
  71. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  72. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  73. package/migrations/core_193_task_needs_rebase.sql +28 -0
  74. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  75. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  76. package/migrations/core_196_session_reward_topup.sql +69 -0
  77. package/modules/autonomy/pollers/routine-timers.js +9 -8
  78. package/modules/builder-settings/wandering-prefs.js +61 -18
  79. package/modules/dev-box/app/README.md +73 -0
  80. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  81. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  82. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  83. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  84. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  85. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  86. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  87. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  88. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  89. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  90. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  91. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  92. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  93. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  94. package/modules/dev-box/app/build-resources/icon.png +0 -0
  95. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  96. package/modules/dev-box/app/electron-builder.yml +95 -0
  97. package/modules/dev-box/app/package-lock.json +5291 -0
  98. package/modules/dev-box/app/package.json +30 -0
  99. package/modules/dev-box/app/renderer/app.js +319 -0
  100. package/modules/dev-box/app/renderer/index.html +45 -0
  101. package/modules/dev-box/app/renderer/styles.css +246 -0
  102. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  103. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  104. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  105. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  106. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  107. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  108. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  109. package/modules/dev-box/app/src/box-controller.js +287 -0
  110. package/modules/dev-box/app/src/brand.generated.json +13 -0
  111. package/modules/dev-box/app/src/brand.js +16 -0
  112. package/modules/dev-box/app/src/config.js +19 -0
  113. package/modules/dev-box/app/src/gds-client.js +90 -0
  114. package/modules/dev-box/app/src/main.js +610 -0
  115. package/modules/dev-box/app/src/pairing.js +57 -0
  116. package/modules/dev-box/app/src/preload.js +29 -0
  117. package/modules/dev-box/app/src/session-store.js +58 -0
  118. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  119. package/modules/dev-box/app/src/task-picker.js +70 -0
  120. package/modules/dev-box/app/src/tray.js +99 -0
  121. package/modules/dev-box/app/src/updater.js +55 -0
  122. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  123. package/modules/dev-box/app/test/unit.mjs +526 -0
  124. package/modules/dev-box/box-access.js +121 -4
  125. package/modules/dev-box/boxes.js +20 -0
  126. package/modules/dev-box/module.json +1 -1
  127. package/modules/dev-box/routes/box.js +87 -3
  128. package/modules/economy/credits.js +116 -25
  129. package/modules/economy/reward.js +8 -0
  130. package/modules/economy/streak.js +2 -1
  131. package/modules/governance/CLAUDE.md +15 -0
  132. package/modules/governance/catalog.js +419 -0
  133. package/modules/governance/config.js +174 -0
  134. package/modules/governance/db.js +125 -0
  135. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  136. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  137. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  138. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  139. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  140. package/modules/governance/module.json +10 -0
  141. package/modules/governance/protected-surfaces.js +88 -0
  142. package/modules/governance/protected-surfaces.json +80 -0
  143. package/modules/governance/rank-roles.js +83 -0
  144. package/modules/governance/resolver.js +221 -0
  145. package/modules/governance/routes/governance.js +89 -0
  146. package/modules/governance/routes/role-sync.js +49 -0
  147. package/modules/governance/session-scopes.js +127 -0
  148. package/modules/governance/session-scopes.json +38 -0
  149. package/modules/hall-ui/public/atlas.css +117 -0
  150. package/modules/hall-ui/public/atlas.html +3 -114
  151. package/modules/hall-ui/public/brand-apply.js +34 -6
  152. package/modules/hall-ui/public/builders.js +187 -219
  153. package/modules/hall-ui/public/diagrams.html +2 -0
  154. package/modules/hall-ui/public/dom-utils.js +27 -0
  155. package/modules/hall-ui/public/drachmae.html +2 -0
  156. package/modules/hall-ui/public/drachmae.js +54 -1
  157. package/modules/hall-ui/public/gate.css +98 -0
  158. package/modules/hall-ui/public/gate.html +3 -95
  159. package/modules/hall-ui/public/goal-inbox.js +4 -1
  160. package/modules/hall-ui/public/goals-page.js +626 -114
  161. package/modules/hall-ui/public/goals.css +79 -1
  162. package/modules/hall-ui/public/goals.html +38 -11
  163. package/modules/hall-ui/public/hall-widgets.js +62 -52
  164. package/modules/hall-ui/public/harbor.css +123 -0
  165. package/modules/hall-ui/public/harbor.html +3 -120
  166. package/modules/hall-ui/public/index.html +11 -4
  167. package/modules/hall-ui/public/instances-panel.js +4 -1
  168. package/modules/hall-ui/public/modules.css +63 -0
  169. package/modules/hall-ui/public/modules.html +3 -60
  170. package/modules/hall-ui/public/my-projects.html +78 -0
  171. package/modules/hall-ui/public/my-projects.js +142 -0
  172. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  173. package/modules/hall-ui/public/not-ready.html +2 -0
  174. package/modules/hall-ui/public/pair.css +73 -0
  175. package/modules/hall-ui/public/pair.html +3 -70
  176. package/modules/hall-ui/public/palette.js +302 -0
  177. package/modules/hall-ui/public/primer.css +13 -0
  178. package/modules/hall-ui/public/primer.html +2 -0
  179. package/modules/hall-ui/public/profile.html +2 -0
  180. package/modules/hall-ui/public/ranks.html +2 -0
  181. package/modules/hall-ui/public/roadmap.css +179 -0
  182. package/modules/hall-ui/public/roadmap.html +9 -172
  183. package/modules/hall-ui/public/roadmap.js +329 -316
  184. package/modules/hall-ui/public/scouting.html +45 -0
  185. package/modules/hall-ui/public/scouting.js +73 -0
  186. package/modules/hall-ui/public/sessions.css +16 -0
  187. package/modules/hall-ui/public/sessions.html +3 -13
  188. package/modules/hall-ui/public/settings.html +2 -0
  189. package/modules/hall-ui/public/settings.js +4 -4
  190. package/modules/hall-ui/public/shell.js +143 -18
  191. package/modules/hall-ui/public/style.css +231 -39
  192. package/modules/hall-ui/public/task-detail.js +347 -26
  193. package/modules/hall-ui/public/task.html +6 -1
  194. package/modules/hall-ui/public/watch.html +2 -0
  195. package/modules/hall-ui/public/work.html +8 -6
  196. package/modules/hall-ui/public/work.js +1 -1
  197. package/modules/ideas/inbox.js +61 -11
  198. package/modules/ideas/routes/inbox.js +23 -0
  199. package/modules/lifecycle/claim-eligibility.js +72 -0
  200. package/modules/lifecycle/db.js +519 -139
  201. package/modules/lifecycle/done-when.js +18 -3
  202. package/modules/lifecycle/github-push.js +19 -2
  203. package/modules/lifecycle/goal-advisory.js +114 -0
  204. package/modules/lifecycle/goal-authz.js +62 -0
  205. package/modules/lifecycle/lifecycle.js +9 -0
  206. package/modules/lifecycle/publish-reconciler.js +109 -6
  207. package/modules/lifecycle/routes/claims.js +68 -23
  208. package/modules/lifecycle/routes/done-when.js +110 -15
  209. package/modules/lifecycle/routes/goals.js +8 -38
  210. package/modules/lifecycle/routes/tasks.js +38 -6
  211. package/modules/lifecycle/ship-card.js +6 -0
  212. package/modules/onboarding/onboarding-state.js +1 -1
  213. package/modules/platform-identity/CLAUDE.md +15 -0
  214. package/modules/platform-identity/hub-device.js +147 -0
  215. package/modules/platform-identity/hub-keys.js +151 -0
  216. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  217. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  218. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  219. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  220. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  221. package/modules/platform-identity/module.json +18 -0
  222. package/modules/platform-identity/platform-identity.js +531 -0
  223. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  224. package/modules/platform-identity/routes/my-projects.js +140 -0
  225. package/modules/platform-identity/routes/projects.js +107 -0
  226. package/modules/platform-identity/routes/scouting.js +35 -0
  227. package/modules/platform-identity/routes/sso.js +402 -0
  228. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  229. package/modules/platform-identity/ui/.gitkeep +0 -0
  230. package/modules/security/routes/reports.js +10 -0
  231. package/modules/sessions/db.js +166 -21
  232. package/modules/sessions/module.json +1 -1
  233. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  234. package/modules/sessions/routes/sessions.js +33 -0
  235. package/modules/status-ui/public/explore.html +54 -0
  236. package/modules/status-ui/public/explore.js +67 -0
  237. package/package-lock.json +2 -2
  238. package/package.json +1 -1
  239. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  270. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  271. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  272. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  273. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  274. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  275. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  276. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  277. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  278. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  279. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  280. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  281. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  282. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  283. package/public-landing/create.html +252 -62
  284. package/public-landing/gate/README.md +137 -0
  285. package/public-landing/gate/dispmaps/bar.png +0 -0
  286. package/public-landing/gate/dispmaps/flat.png +0 -0
  287. package/public-landing/gate/dispmaps/key.png +0 -0
  288. package/public-landing/gate/index.html +587 -0
  289. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  290. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  291. package/public-landing/gate/space.css +799 -0
  292. package/public-landing/gate/space.js +1826 -0
  293. package/public-landing/gate/sprites/enter.png +0 -0
  294. package/public-landing/gate/sprites/search.png +0 -0
  295. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  296. package/public-landing/gate/sprites/tile.png +0 -0
  297. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  298. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  299. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  300. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  301. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  302. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  303. package/scripts/gds/abandon.js +6 -4
  304. package/scripts/gds/api.js +10 -5
  305. package/scripts/gds/api.ps1 +26 -0
  306. package/scripts/gds/api.sh +6 -0
  307. package/scripts/gds/audit-ships.js +7 -1
  308. package/scripts/gds/backfill-sessions.js +78 -18
  309. package/scripts/gds/backup.js +23 -1
  310. package/scripts/gds/box-connect-lib.js +124 -31
  311. package/scripts/gds/box-sync.js +66 -0
  312. package/scripts/gds/box.js +225 -53
  313. package/scripts/gds/cf-tunnel.js +145 -6
  314. package/scripts/gds/claim.js +34 -3
  315. package/scripts/gds/cli-lib.js +161 -21
  316. package/scripts/gds/code.js +6 -2
  317. package/scripts/gds/deploy-divergence.js +209 -0
  318. package/scripts/gds/dev-lib.js +309 -0
  319. package/scripts/gds/dev.js +322 -0
  320. package/scripts/gds/doctor.js +90 -61
  321. package/scripts/gds/fetch-art-key.js +1 -1
  322. package/scripts/gds/fetch-sound-prefs.js +2 -2
  323. package/scripts/gds/fitness.js +228 -12
  324. package/scripts/gds/gds-db-backup.service +16 -1
  325. package/scripts/gds/gds-db-offsite.service +9 -1
  326. package/scripts/gds/gen-api-client.js +1 -1
  327. package/scripts/gds/gen-api-docs.js +36 -14
  328. package/scripts/gds/gen-diagrams.js +45 -28
  329. package/scripts/gds/gen-file-map.js +2 -1
  330. package/scripts/gds/gen-freshness.js +32 -0
  331. package/scripts/gds/gen-repo-map.js +3 -2
  332. package/scripts/gds/gen-session-index.js +14 -5
  333. package/scripts/gds/hooks/pre-push.js +2 -2
  334. package/scripts/gds/init.js +60 -3
  335. package/scripts/gds/install-git-hooks.js +1 -1
  336. package/scripts/gds/key-card.js +15 -6
  337. package/scripts/gds/key-status.js +1 -1
  338. package/scripts/gds/linkify-refs.js +35 -9
  339. package/scripts/gds/lint-migrations.js +102 -0
  340. package/scripts/gds/login.js +58 -11
  341. package/scripts/gds/mirror-redact.js +5 -0
  342. package/scripts/gds/package-core.js +263 -4
  343. package/scripts/gds/paste-token.js +7 -1
  344. package/scripts/gds/preflight.js +126 -0
  345. package/scripts/gds/provision.js +268 -19
  346. package/scripts/gds/publish-manifest.js +22 -1
  347. package/scripts/gds/reauth.js +7 -3
  348. package/scripts/gds/regen-instance-docs.js +47 -0
  349. package/scripts/gds/run-unit-tests.js +42 -16
  350. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  351. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  352. package/scripts/gds/sequence.js +425 -0
  353. package/scripts/gds/session-cost.js +10 -1
  354. package/scripts/gds/setup.js +72 -5
  355. package/scripts/gds/shell.js +5 -3
  356. package/scripts/gds/ship.js +433 -59
  357. package/scripts/gds/smoke-gds.js +139 -8
  358. package/scripts/gds/start.js +171 -22
  359. package/scripts/gds/status.js +1 -1
  360. package/scripts/gds/triage.js +10 -9
  361. package/scripts/gds/upgrade.js +271 -7
  362. package/scripts/migrate.sh +10 -0
  363. package/scripts/status-mirror-sync.js +114 -84
  364. package/src/bongos/api-errors.js +1 -0
  365. package/src/bongos/auth.js +809 -107
  366. package/src/bongos/db-kernel.js +27 -0
  367. package/src/bongos/db.js +105 -0
  368. package/src/bongos/github-repos.js +93 -0
  369. package/src/bongos/module-scope-map.js +27 -1
  370. package/src/bongos/permission-path-check.js +136 -102
  371. package/src/bongos/repo-info.js +47 -11
  372. package/src/bongos/route-rank-check.js +429 -46
  373. package/src/bongos/routes/_helpers.js +12 -1
  374. package/src/bongos/routes/auth.js +312 -13
  375. package/src/bongos/routes/backup.js +58 -2
  376. package/src/bongos/routes/github.js +90 -0
  377. package/src/bongos/routes/instance.js +15 -3
  378. package/src/bongos/routes/me.js +35 -1
  379. package/src/bongos/routes.js +2 -0
  380. package/src/bongos/serve-internal.js +84 -15
  381. package/src/branding.js +49 -2
  382. package/src/instance-config.js +46 -0
  383. package/src/module-api.js +140 -1
  384. package/src/platform-server.js +86 -9
  385. package/tests/abandon_task.mjs +59 -3
  386. package/tests/api_cli_flags.mjs +53 -0
  387. package/tests/auth_page_gate.mjs +13 -0
  388. package/tests/backfill_sessions.mjs +127 -1
  389. package/tests/backup_cli.mjs +54 -0
  390. package/tests/bongos_box_connect.mjs +77 -0
  391. package/tests/box_access.mjs +130 -1
  392. package/tests/box_credential.mjs +125 -0
  393. package/tests/box_host_keys.mjs +24 -0
  394. package/tests/box_infra_resolve.mjs +149 -0
  395. package/tests/box_scope_session.mjs +6 -2
  396. package/tests/box_sync.mjs +55 -0
  397. package/tests/box_task_scope.mjs +99 -0
  398. package/tests/branding.mjs +41 -23
  399. package/tests/cf_ruleset_preflight.mjs +142 -0
  400. package/tests/cf_tunnel.mjs +37 -0
  401. package/tests/claim_gate_rebase.mjs +98 -0
  402. package/tests/clawback_confirmed_backlog.mjs +103 -0
  403. package/tests/cli_lib_formatters.mjs +20 -0
  404. package/tests/confirm_attribution.mjs +30 -46
  405. package/tests/credit_grant.mjs +198 -10
  406. package/tests/criterion_authoring_gate.mjs +272 -0
  407. package/tests/criterion_progress.mjs +33 -1
  408. package/tests/dev_lib.mjs +253 -0
  409. package/tests/devbox_artifact_gate.mjs +75 -0
  410. package/tests/devbox_dmg_backdrop.mjs +137 -0
  411. package/tests/devbox_icons.mjs +304 -0
  412. package/tests/devbox_installer_brand.mjs +232 -0
  413. package/tests/devbox_key_register.mjs +234 -0
  414. package/tests/docs_openapi_route.mjs +110 -0
  415. package/tests/doctor_hooks.mjs +141 -0
  416. package/tests/extract_json.mjs +140 -0
  417. package/tests/fitness.mjs +89 -0
  418. package/tests/gen_freshness.mjs +96 -0
  419. package/tests/github_repos_helpers.mjs +91 -0
  420. package/tests/github_token_store.mjs +121 -0
  421. package/tests/goal_advisory.mjs +251 -0
  422. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  423. package/tests/goal_routes.mjs +59 -4
  424. package/tests/goal_tier.mjs +12 -3
  425. package/tests/governance_catalog.mjs +129 -0
  426. package/tests/governance_claim_eligibility.mjs +262 -0
  427. package/tests/governance_config.mjs +187 -0
  428. package/tests/governance_ownership_scope.mjs +410 -0
  429. package/tests/governance_page_gates.mjs +324 -0
  430. package/tests/governance_parity.mjs +336 -0
  431. package/tests/governance_principals.mjs +232 -0
  432. package/tests/governance_protected_surfaces.mjs +208 -0
  433. package/tests/governance_require_permission.mjs +223 -0
  434. package/tests/governance_resolver.mjs +84 -0
  435. package/tests/governance_role_sync.mjs +231 -0
  436. package/tests/governance_seed.mjs +127 -0
  437. package/tests/governance_session_scope.mjs +434 -0
  438. package/tests/governance_vanilla_boot.mjs +203 -0
  439. package/tests/grade_attribution.mjs +7 -7
  440. package/tests/hall_audit.mjs +223 -0
  441. package/tests/hall_nav.mjs +191 -0
  442. package/tests/hall_palette.mjs +206 -0
  443. package/tests/hall_signout.mjs +124 -0
  444. package/tests/hook_user_agents.mjs +10 -0
  445. package/tests/idea_triage.mjs +162 -0
  446. package/tests/idp_device_exchange.mjs +198 -0
  447. package/tests/idp_federation.mjs +100 -0
  448. package/tests/init.mjs +51 -0
  449. package/tests/instance_config.mjs +65 -0
  450. package/tests/instance_mismatch_guard.mjs +23 -6
  451. package/tests/instance_release_config.mjs +104 -0
  452. package/tests/linkify_refs.mjs +15 -2
  453. package/tests/migrations_forward_safe.mjs +86 -0
  454. package/tests/module_api.mjs +1 -1
  455. package/tests/module_contributions.mjs +55 -0
  456. package/tests/module_loader.mjs +10 -5
  457. package/tests/module_scope_active_claims.mjs +94 -0
  458. package/tests/needs_rebase.mjs +92 -0
  459. package/tests/one_active_claim_per_session.mjs +76 -0
  460. package/tests/override_attribution.mjs +10 -8
  461. package/tests/override_request_ownership.mjs +8 -4
  462. package/tests/package_core.mjs +246 -1
  463. package/tests/parse_cookie_decode.mjs +42 -0
  464. package/tests/pay_on_land.mjs +129 -0
  465. package/tests/platform_boot.mjs +100 -1
  466. package/tests/post_login_landing.mjs +42 -0
  467. package/tests/preflight_checks.mjs +72 -0
  468. package/tests/provision.mjs +289 -7
  469. package/tests/pruned_lockfile_integrity.mjs +79 -0
  470. package/tests/publish_branch_route.mjs +53 -0
  471. package/tests/publish_reconciler.mjs +187 -2
  472. package/tests/repo_info_binding.mjs +248 -0
  473. package/tests/reward_gate.mjs +11 -2
  474. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  475. package/tests/reward_mode_surfacing.mjs +168 -0
  476. package/tests/route_permission_check.mjs +301 -0
  477. package/tests/route_rank_check.mjs +254 -12
  478. package/tests/route_validation.mjs +30 -0
  479. package/tests/sequence.mjs +267 -0
  480. package/tests/session_digest.mjs +44 -0
  481. package/tests/session_index.mjs +18 -6
  482. package/tests/session_records.mjs +39 -1
  483. package/tests/session_reward_on_land.mjs +381 -0
  484. package/tests/session_reward_reconcile.mjs +229 -0
  485. package/tests/ship_api_client_pathspec.mjs +100 -0
  486. package/tests/ship_base_flag.mjs +93 -0
  487. package/tests/ship_ci_deploy.mjs +140 -3
  488. package/tests/ship_upstream_deletion.mjs +57 -0
  489. package/tests/smoke_target.mjs +233 -0
  490. package/tests/start_env_guard.mjs +87 -0
  491. package/tests/start_rebase_warning.mjs +103 -0
  492. package/tests/status_mirror_sync.mjs +122 -0
  493. package/tests/stop_refcheck.mjs +19 -1
  494. package/tests/task_goal_link.mjs +15 -6
  495. package/tests/upgrade.mjs +393 -5
  496. package/tests/web_admission_status.mjs +162 -0
  497. package/tests/write_validation_gaps.mjs +110 -0
  498. package/.gitignore +0 -91
  499. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  500. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  501. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  502. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  503. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  504. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  505. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  506. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  507. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  508. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  509. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  510. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  511. package/scripts/hall-preview/fixtures/goals.json +0 -502
  512. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  513. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  514. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  515. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  516. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  517. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  518. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  519. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  520. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  521. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  522. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  523. package/scripts/hall-preview/fixtures/instance.json +0 -124
  524. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  525. package/scripts/hall-preview/fixtures/me.json +0 -476
  526. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  527. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  528. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  529. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  530. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  531. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  532. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  533. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  534. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  535. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  536. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  537. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  538. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  539. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  540. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  541. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  542. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  543. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  544. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  545. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  546. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  547. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  548. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  549. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  550. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  551. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  552. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  553. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -31,6 +31,13 @@
31
31
  // # empty-diff guard; confirms directly (no diff to
32
32
  // # grade). REFUSED if the working tree is dirty —
33
33
  // # that means you forgot to commit (task 1363/1365).
34
+ // node scripts/gds/ship.js <task-id> --base <ref> # grade a task whose commits ALREADY landed on main
35
+ // # (e.g. a release task: package-core needs the merge
36
+ // # first). Pins <ref> as the pre-task baseline so the
37
+ // # grader reads the REAL diff <ref>...HEAD and confirms
38
+ // # via the RANK-OPEN graded path — the sub-Archon-safe
39
+ // # alternative to --allow-empty (task 1002422 / blocker
40
+ // # 1000082). Implies db-only (nothing left to merge).
34
41
  // node scripts/gds/ship.js <task-id> --regrade # re-grade a task STUCK at `completed` (a ship
35
42
  // # whose grade failed): re-runs ONLY the grade step
36
43
  // # against the committed diff, no claim required.
@@ -54,7 +61,20 @@ const fs = require('node:fs');
54
61
  const os = require('node:os');
55
62
  const path = require('node:path');
56
63
  const readline = require('node:readline');
57
- const { cliClient, requireSession, loadSession, printOnboardingNudge, arg } = require('./cli-lib');
64
+ const { cliClient, requireSession, loadSession, loadSessionSync, printOnboardingNudge, arg, rewardPolicy } = require('./cli-lib');
65
+
66
+ // The instance's reward policy, as reported by the server on GET /me (ADR 0146 /
67
+ // task 1002469). Cached module-level because the credits print + the completion
68
+ // card are reached through several call chains and the mode is a single
69
+ // instance-wide fact — threading it through each would be noise. Stays null
70
+ // until a /me lands; rewardPolicy(null) then degrades to the paying default, so
71
+ // a missed fetch under-explains rather than wrongly claiming "not paid".
72
+ let serverReward = null;
73
+ function noteServerReward(meRes) {
74
+ const r = meRes && meRes.ok && meRes.data && meRes.data.reward;
75
+ if (r && typeof r.mode === 'string') serverReward = r;
76
+ return meRes;
77
+ }
58
78
  // ship-preflight + merge-lock carved to modules/lifecycle/ (BV1.R86). A script may
59
79
  // require a module directly (it's outside the kernel boundary — the grading/economy
60
80
  // CLI precedent).
@@ -95,7 +115,11 @@ const { playSound } = (() => {
95
115
  return { playSound: () => {} };
96
116
  }
97
117
  })();
98
- const DROPLET = 'lars@104.236.254.243';
118
+ // The laptop-mode SSH deploy target is resolved at land time from config/deploy.json
119
+ // ("sshTarget") or <PREFIX>_DEPLOY_SSH_TARGET — see resolveDeployTarget(). There is NO
120
+ // hardcoded default: the core carries no deploy target of its own. The founder
121
+ // instance's old game-box SSH target used to be baked in here, which post-migration
122
+ // meant a laptop-mode ship deployed to the WRONG server (task 1002202).
99
123
  const BEHIND_MAIN_LIMIT = 5;
100
124
 
101
125
  // arg() — the =-aware flag-value parser now lives in cli-lib (the canonical
@@ -199,8 +223,14 @@ function claudeConfigBase() {
199
223
  }
200
224
 
201
225
  function encodeProjectPath(absProjectPath) {
202
- // Claude Code's encoding: replace every '/' and '.' with '-'.
203
- return absProjectPath.replace(/[/.]/g, '-');
226
+ // Claude Code's encoding: every NON-ALPHANUMERIC char becomes '-'. The old
227
+ // `[/.]` class silently no-op'd on Windows (separator '\', drive ':', spaces),
228
+ // so the transcript was never found, no session_record was written, and the
229
+ // cost-plus reward — the only stream a cost-plus-only instance pays (ADR 0146)
230
+ // — could never book: every Windows ship paid 0 (task 1002471). Same rule as
231
+ // scripts/gds/onboarding-config.js, which already had it right. POSIX output is
232
+ // unchanged for paths whose only specials are '/' and '.'.
233
+ return String(absProjectPath).replace(/[^a-zA-Z0-9]/g, '-');
204
234
  }
205
235
 
206
236
  // resolveTranscriptPath — find this session's transcript .jsonl. Claude Code
@@ -323,7 +353,11 @@ const {
323
353
  // snippetOf — short, single-line, scrubbed excerpt for footgun context.
324
354
  function snippetOf(text, extraSecrets, max = 160) {
325
355
  if (typeof text !== 'string' || text.length === 0) return undefined;
326
- const flat = text.replace(/\s+/g, ' ').trim();
356
+ // Drop NULs first: Postgres rejects a literal NUL in jsonb, so ONE of them anywhere in
357
+ // ONE snippet made the whole digest unstorable — POST /sessions answered 500 and
358
+ // the session never paid. UTF-16LE tool output (`wsl --list` is the observed
359
+ // case) is full of them, and \s below does NOT match NUL, so it must go here.
360
+ const flat = text.replace(/\0/g, '').replace(/\s+/g, ' ').trim();
327
361
  if (!flat) return undefined;
328
362
  const cut = flat.length > max ? flat.slice(0, max) : flat;
329
363
  const scrubbed = scrubSecrets(cut, extraSecrets);
@@ -762,9 +796,13 @@ function buildMergeArgv(branch, valueSummary) {
762
796
  // Two ways the confirmed→shipped land can run, chosen by a SINGLE committed
763
797
  // switch (config/deploy.json `mode`, env OTB_DEPLOY_MODE overrides):
764
798
  //
765
- // 'laptop' (default) — the operator's machine merges to main locally and
766
- // SSHes `~/deploy.sh` on the droplet. Today's behavior, byte-for-byte.
767
- // Requires the droplet SSH key, so only the operator can land.
799
+ // 'laptop' (default) — the operator's machine merges to main locally and, IFF a
800
+ // deploy target is configured (config/deploy.json "sshTarget" or
801
+ // <PREFIX>_DEPLOY_SSH_TARGET, resolveDeployTarget()), SSHes `~/deploy.sh` on it.
802
+ // The core carries NO target of its own, so by default this is a merge-to-main
803
+ // with no SSH deploy — a change reaches a live instance via package-core +
804
+ // `bongos upgrade` (pin-bump), NOT an SSH deploy (ADR 0108). When a target IS
805
+ // set it requires that host's SSH key, so only the operator can land.
768
806
  //
769
807
  // 'ci' — NO builder box holds the prod key. ship pushes the branch, opens a
770
808
  // PR, and enables GitHub auto-merge; once required checks pass, GitHub
@@ -815,12 +853,7 @@ function effectiveDeployMode(mode, { canPushLocally, envForced } = {}) {
815
853
  }
816
854
 
817
855
  function resolveDeployMode() {
818
- let configRaw = null;
819
- try {
820
- configRaw = fs.readFileSync(path.join(REPO_ROOT, 'config', 'deploy.json'), 'utf8');
821
- } catch (_) {
822
- /* no config file → safe default */
823
- }
856
+ const configRaw = readDeployConfigRaw();
824
857
  const mode = parseDeployMode(process.env.OTB_DEPLOY_MODE, configRaw);
825
858
  const envForced = ['ci', 'laptop'].includes(
826
859
  String(process.env.OTB_DEPLOY_MODE || '').trim().toLowerCase()
@@ -834,6 +867,111 @@ function resolveDeployMode() {
834
867
  return mode;
835
868
  }
836
869
 
870
+ // readDeployConfigRaw — raw contents of config/deploy.json (or null if absent /
871
+ // unreadable). One reader shared by resolveDeployMode (the mode switch) and
872
+ // resolveDeployTarget (the SSH target) so they never diverge on where the file is.
873
+ function readDeployConfigRaw() {
874
+ try {
875
+ return fs.readFileSync(path.join(REPO_ROOT, 'config', 'deploy.json'), 'utf8');
876
+ } catch (_) {
877
+ return null; // no config file → callers apply their safe default
878
+ }
879
+ }
880
+
881
+ // safeBranding — the instance brand pack, or null if it can't be read. Guarded the
882
+ // same way ship.js guards the sound player (line ~91): a deploy must never crash
883
+ // because the brand file is missing on a fresh / re-seeded checkout.
884
+ function safeBranding() {
885
+ try {
886
+ return require('../../src/branding').branding();
887
+ } catch (_) {
888
+ return null;
889
+ }
890
+ }
891
+
892
+ // readSessionApiBase — the signed-in session's persisted api_base (the instance the
893
+ // CLI is actually pointed at), or null when not signed in / unreadable. This is the
894
+ // session's OWN value only — deliberately NOT readSessionToken().apiBase, which
895
+ // already folds in the env override + branding default; resolveInstanceBase orders
896
+ // that precedence itself so it stays pure over its injected inputs.
897
+ function readSessionApiBase() {
898
+ try {
899
+ const s = loadSessionSync();
900
+ return (s && typeof s.api_base === 'string' && s.api_base.trim()) ? s.api_base.trim() : null;
901
+ } catch (_) {
902
+ return null;
903
+ }
904
+ }
905
+
906
+ // resolveInstanceBase — the ORIGIN of the instance this ship is landing on, used by
907
+ // the ci-land deployed-version probe (/version), the /healthz re-probe, the ci
908
+ // PR-body deploy note, and the manual-confirm curl guidance. Precedence mirrors the
909
+ // CLI's own base resolution: <PREFIX>_API_BASE env → the signed-in session's
910
+ // api_base → branding publicOrigin → null (unknowable). There is NO hardcoded
911
+ // founder host — 5 sites here baked in `https://amazonprimea.com`, so a ci-land /
912
+ // manual-confirm on a migrated instance pointed the builder at the WRONG box; the
913
+ // ci-land/guidance sibling of resolveDeployTarget()'s laptop-mode fix (task 1002232
914
+ // ⊃ task 1002202). Pure over its injected inputs (env / sessionApiBase / branding)
915
+ // so the precedence is unit-tested without fs/env/network; the defaults read the
916
+ // real session file + brand pack.
917
+ function resolveInstanceBase({
918
+ env = process.env,
919
+ sessionApiBase = readSessionApiBase(),
920
+ branding = safeBranding(),
921
+ } = {}) {
922
+ const ic = require('../../src/instance-config');
923
+ const trim = (s) => (typeof s === 'string' && s.trim() ? s.trim().replace(/\/+$/, '') : null);
924
+ const envBase = trim(ic.resolveEnv('API_BASE', { env }));
925
+ if (envBase) return envBase;
926
+ const sess = trim(sessionApiBase);
927
+ if (sess) return sess;
928
+ const origin = branding && branding.domains && branding.domains.publicOrigin;
929
+ return trim(origin);
930
+ }
931
+
932
+ // deployedVersionUrl — the instance's /version endpoint, or null when no instance
933
+ // origin is resolvable (then fetchDeployedVersion is a no-op). Derived from
934
+ // resolveInstanceBase — no hardcoded host (task 1002232).
935
+ function deployedVersionUrl() {
936
+ const base = resolveInstanceBase();
937
+ return base ? `${base}/version` : null;
938
+ }
939
+
940
+ // resolveDeployTarget — WHERE a laptop-mode ship SSHes `~/deploy.sh`, and which
941
+ // /healthz it re-probes when deploy.sh's own health check races the restart window.
942
+ // Pure (env + raw config + branding → target) and exported so the precedence and the
943
+ // safe "no target" default are unit-tested without touching fs/env/network.
944
+ //
945
+ // There is deliberately NO hardcoded default. The founder instance's old game-box
946
+ // SSH target was baked in here, so a laptop-mode ship from a migrated
947
+ // checkout SSHed the WRONG server (task 1002202). The core carries no deploy target
948
+ // of its own — under the standalone model a core change reaches a live instance via
949
+ // package-core + `bongos upgrade` (pin-bump), not an SSH deploy (ADR 0108). An
950
+ // instance whose OWN checkout is its live server opts back into laptop-SSH deploy by
951
+ // setting the target:
952
+ // config/deploy.json → { "sshTarget": "user@host", "healthzUrl": "https://…/healthz" }
953
+ // env → <PREFIX>_DEPLOY_SSH_TARGET, <PREFIX>_DEPLOY_HEALTHZ_URL
954
+ // healthzUrl, when unset, falls back to the instance's own publicOrigin + /healthz
955
+ // (never a hardcoded host); null when even that is unknown (re-probe then skipped).
956
+ function resolveDeployTarget({ env = process.env, configRaw = null, branding = null } = {}) {
957
+ const ic = require('../../src/instance-config');
958
+ let cfg = {};
959
+ if (configRaw) {
960
+ try { cfg = JSON.parse(configRaw) || {}; } catch (_) { cfg = {}; }
961
+ }
962
+ const str = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
963
+
964
+ const sshTarget = ic.resolveEnv('DEPLOY_SSH_TARGET', { env }) || str(cfg.sshTarget) || null;
965
+
966
+ let healthzUrl = ic.resolveEnv('DEPLOY_HEALTHZ_URL', { env }) || str(cfg.healthzUrl) || null;
967
+ const publicOrigin = branding && branding.domains && branding.domains.publicOrigin;
968
+ if (!healthzUrl && str(publicOrigin)) {
969
+ try { healthzUrl = new URL('/healthz', publicOrigin).toString(); }
970
+ catch (_) { /* malformed origin → leave healthzUrl null */ }
971
+ }
972
+ return { sshTarget, healthzUrl };
973
+ }
974
+
837
975
  // gh runners — mirror gitArgvOk/gitOk but for the GitHub CLI. No shell, ever:
838
976
  // branch names + DB-sourced value_summary reach `gh` as literal argv elements
839
977
  // (same injection discipline as #681). ghArgvOk = boolean side-effect; ghJson =
@@ -889,7 +1027,10 @@ function deployedCommitStatus(versionBody, mergeSha) {
889
1027
  }
890
1028
 
891
1029
  // Fetch + parse the prod /version JSON (#858). null on any failure — never throws.
892
- function fetchDeployedVersion(url = 'https://amazonprimea.com/version') {
1030
+ // Default target is the resolved instance origin (no hardcoded host — task 1002232);
1031
+ // null when none is resolvable, in which case the probe is skipped.
1032
+ function fetchDeployedVersion(url = deployedVersionUrl()) {
1033
+ if (!url) return null; // no resolvable instance origin → nothing to probe
893
1034
  // stdio override DISCARDS stderr (a curl progress/error line must not pollute
894
1035
  // the JSON parse) — the documented per-caller difference from ghJson.
895
1036
  const out = argvOut('curl', ['-fsS', '--max-time', '10', url], { stdio: ['ignore', 'pipe', 'ignore'] });
@@ -1032,11 +1173,15 @@ async function ciLandServer(taskId, branch, valueSummary) {
1032
1173
  // Pure + exported for unit tests (no git/network). Accepts the publish-status
1033
1174
  // response shape verbatim — getPublishStatus returns snake_case (pr_state,
1034
1175
  // deploy_status); camelCase aliases are tolerated for test convenience.
1176
+ // task 1002472: 'unconfigured' means the repo has no deploy-prod.yml to wait for
1177
+ // (ADR 0108 pin-bump publishing), so the merge IS the land. Mirrors
1178
+ // github-push.landConfirmed — kept in step by tests/ship_ci_deploy.mjs, since this
1179
+ // CLI cannot import the server module.
1035
1180
  function classifyLandOutcome(s) {
1036
1181
  const merged = !!(s && s.merged);
1037
1182
  const deployStatus = s && (s.deploy_status ?? s.deployStatus);
1038
1183
  const prState = s && (s.pr_state ?? s.prState);
1039
- if (merged && deployStatus === 'success') return 'shipped';
1184
+ if (merged && (deployStatus === 'success' || deployStatus === 'unconfigured')) return 'shipped';
1040
1185
  if (prState === 'closed' && !merged) return 'failed';
1041
1186
  return 'pending';
1042
1187
  }
@@ -1221,11 +1366,12 @@ async function ciLandLocal(taskId, branch, valueSummary) {
1221
1366
  // GitHub's PR/issue number space (dead links for GDS task ids — task 916),
1222
1367
  // so the task ref is written as "task NNN" and the summary is de-#-ed.
1223
1368
  const title = `task ${taskId}: ${sanitizeGitHubRefs(valueSummary) || branch}`.slice(0, 240);
1369
+ const deployTargetNote = resolveInstanceBase() || 'the configured instance';
1224
1370
  const body =
1225
1371
  `Auto-opened by ship.js (deploy mode: ci) for task ${taskId}.\n\n` +
1226
1372
  `${sanitizeGitHubRefs(valueSummary) || '(no value summary)'}\n\n` +
1227
1373
  `Merging this PR triggers .github/workflows/deploy-prod.yml to deploy to ` +
1228
- `https://amazonprimea.com. Auto-merge is enabled — GitHub lands it once the ` +
1374
+ `${deployTargetNote}. Auto-merge is enabled — GitHub lands it once the ` +
1229
1375
  `required checks pass.`;
1230
1376
  if (!existing || existing.state === 'CLOSED' || existing.state === 'MERGED') {
1231
1377
  console.log(' ci-land: opening PR -> main...');
@@ -1307,12 +1453,14 @@ function pollCiDeploy(taskId, branch, headSha) {
1307
1453
  // real deploy into a false negative on a transient miss or a concurrent
1308
1454
  // later deploy).
1309
1455
  const status = deployedCommitStatus(fetchDeployedVersion(), mergeSha);
1456
+ const instanceBase = resolveInstanceBase();
1457
+ const healthzUrl = instanceBase ? `${instanceBase}/healthz` : null;
1310
1458
  if (status === 'match') {
1311
1459
  console.log(' ci-land: /version confirms prod is live on the merge commit (commit-pinned).');
1312
1460
  } else if (status === 'mismatch') {
1313
1461
  console.log(' ci-land: deploy run succeeded; /version shows a different commit (a later deploy has since superseded it — fine, our change is in main).');
1314
- } else if (shellOk('curl -fsS --max-time 10 https://amazonprimea.com/healthz -o /dev/null')) {
1315
- console.log(' ci-land: /version unavailable; amazonprimea.com/healthz 200 — treating the successful deploy run as authoritative.');
1462
+ } else if (healthzUrl && shellOk(`curl -fsS --max-time 10 ${healthzUrl} -o /dev/null`)) {
1463
+ console.log(` ci-land: /version unavailable; ${healthzUrl} 200 — treating the successful deploy run as authoritative.`);
1316
1464
  } else {
1317
1465
  console.log(' ci-land: deploy run succeeded but /version + healthz probes both missed (likely transient). Treating the successful deploy run as authoritative.');
1318
1466
  }
@@ -1337,6 +1485,16 @@ function pollCiDeploy(taskId, branch, headSha) {
1337
1485
  // Also refuses when the branch is far behind origin/main — merging a stale
1338
1486
  // branch is a known regression vector.
1339
1487
 
1488
+ // parseDeletedUpstreamPaths — pure: turn `git log --diff-filter=D --name-only
1489
+ // --pretty=format:` output (one deleted path per line, blank lines between
1490
+ // commits) into a deduped, order-preserving list. Exported so preflight's
1491
+ // upstream-deletion warning (step (d)) is unit-tested without a live git repo.
1492
+ // Null/empty input (no matches, or the git call failed closed) → [].
1493
+ function parseDeletedUpstreamPaths(rawLogOutput) {
1494
+ if (!rawLogOutput) return [];
1495
+ return [...new Set(rawLogOutput.split('\n').map((s) => s.trim()).filter(Boolean))];
1496
+ }
1497
+
1340
1498
  function preflight(claim, opts = {}) {
1341
1499
  // (a) Working-tree check — refuse if any TRACKED file carries an uncommitted
1342
1500
  // modification or staged change. This fires post-commit, so a clean tree
@@ -1409,6 +1567,39 @@ function preflight(claim, opts = {}) {
1409
1567
  }
1410
1568
  }
1411
1569
 
1570
+ // (d) Upstream-deletion warning (WARN-ONLY — never blocks, never changes the
1571
+ // exit code). main can delete or relocate a surface between when a task was
1572
+ // claimed and when it ships (a cutover, a carve, a rename — the R89 hall-ui
1573
+ // cutover class); the branch's own diff still touches the OLD path, and an
1574
+ // oblivious ship can silently revert the upstream deletion or leave this
1575
+ // branch's work orphaned. Scoped to the paths THIS branch's diff actually
1576
+ // touches — the same baseline/diff computeGraderBaseline/computeChangedFiles
1577
+ // compute for the grader — so an unrelated upstream deletion elsewhere in the
1578
+ // repo never fires it. Silent no-op if origin/main isn't resolvable (offline /
1579
+ // never fetched) or this branch's diff is empty — the check itself must never
1580
+ // be why a ship breaks. parseDeletedUpstreamPaths is the pure parsing seam,
1581
+ // exported + unit-tested (tests/ship_upstream_deletion.mjs) without a live git
1582
+ // repo; this block is just the git I/O + presentation around it.
1583
+ const upstreamDelBaseline = computeGraderBaseline(claim);
1584
+ const upstreamDelTouched = upstreamDelBaseline ? computeChangedFiles(upstreamDelBaseline) : [];
1585
+ if (upstreamDelTouched.length > 0) {
1586
+ const deletedUpstreamStr = argvOut('git', [
1587
+ 'log', 'HEAD..origin/main', '--diff-filter=D', '--name-only', '--pretty=format:',
1588
+ '--', ...upstreamDelTouched,
1589
+ ]);
1590
+ const deletedUpstream = parseDeletedUpstreamPaths(deletedUpstreamStr);
1591
+ if (deletedUpstream.length > 0) {
1592
+ console.error('');
1593
+ console.error(`⚠ Upstream deletion warning: main has removed ${deletedUpstream.length} path(s) this branch still touches, since this task was claimed:`);
1594
+ for (const f of deletedUpstream) console.error(` - ${f}`);
1595
+ console.error(' This looks like the R89 hall-ui cutover class: a surface removed or relocated upstream while the claim was in flight.');
1596
+ console.error(' Verify these paths still exist on main (or that the removal/rename is intentional) before shipping —');
1597
+ console.error(" shipping anyway may silently revert the upstream deletion or leave this branch's work orphaned.");
1598
+ console.error(' Warning only — not blocking; the ship proceeds.');
1599
+ console.error('');
1600
+ }
1601
+ }
1602
+
1412
1603
  console.log(` preflight: working tree clean, branch ${behind} commits behind main, ship tooling current — OK`);
1413
1604
  return { ok: true };
1414
1605
  }
@@ -1585,13 +1776,80 @@ function autoLinkifyChangedDocs(ac) {
1585
1776
  // full unified diff against the baseline, and (for the audit-trail signals)
1586
1777
  // the shortstat summary.
1587
1778
  //
1779
+ // task 1002422 (blocker 1000082): a builder-settable pre-task baseline. When a
1780
+ // task's artifact reached main BEFORE the ship (a release task: package-core
1781
+ // archives committed HEAD, so the version bump must merge first), merge-base(
1782
+ // origin/main, HEAD) == HEAD → an EMPTY committed diff → the --allow-empty /
1783
+ // skip-grade / Archon-only /confirm dead-end that NO sub-Archon builder can
1784
+ // finish. --base <ref> pins the pre-task commit so the grader reads the REAL diff
1785
+ // <base>...HEAD and the ship confirms via the rank-open graded path instead. Set
1786
+ // once in main() after resolveBaseRef() validates it; null on a normal ship.
1787
+ let baseOverrideSha = null;
1788
+
1789
+ // resolveBaseRef — validate + resolve a --base <ref> to a commit sha. Injection-
1790
+ // safe: the ref reaches git as a literal argv element (execFile, no shell), never
1791
+ // a string interpolation. Pure over injected git runners so the unit test drives
1792
+ // it with fakes (no real repo). Returns { ok:true, sha } or { ok:false, reason }.
1793
+ function resolveBaseRef(rawBase, { revParse, isAncestor } = {}) {
1794
+ const ref = String(rawBase == null ? '' : rawBase).trim();
1795
+ if (!ref) return { ok: false, reason: 'empty ref' };
1796
+ // Conservative allowlist (defence-in-depth even though transport is argv): no
1797
+ // whitespace, no leading dash (git would read it as a flag), no shell metachar.
1798
+ if (!/^[A-Za-z0-9][A-Za-z0-9._/~^-]*$/.test(ref)) {
1799
+ return { ok: false, reason: `invalid ref syntax "${ref}"` };
1800
+ }
1801
+ const rp = revParse || ((r) => argvOut('git', ['rev-parse', '--verify', '--quiet', `${r}^{commit}`]));
1802
+ const sha = rp(ref);
1803
+ if (!sha) return { ok: false, reason: `"${ref}" is not a commit this repo knows` };
1804
+ const anc = isAncestor || ((s) => gitArgvOk(['merge-base', '--is-ancestor', s, 'HEAD']));
1805
+ if (!anc(sha)) {
1806
+ return { ok: false, reason: `"${ref}" (${sha.slice(0, 9)}) is not an ancestor of HEAD — --base must be a commit your work built on top of` };
1807
+ }
1808
+ return { ok: true, sha };
1809
+ }
1810
+
1811
+ // baseRefShipGuard — the SECURITY invariant behind --base. --base decouples the
1812
+ // GRADED diff (<base>...HEAD) from what the merge leg lands (the whole branch:
1813
+ // autoMerge runs `git merge <branch>`, bringing every commit ahead of main, not
1814
+ // just <base>...HEAD). So if HEAD has commits AHEAD of origin/main, honoring
1815
+ // --base would let the grader review only a SUBSET while the full branch merges —
1816
+ // an unreviewed-code path onto main. --base is therefore ONLY valid for work
1817
+ // ALREADY fully on main (its documented premise: "nothing left to merge"). Pure
1818
+ // over the ahead-count string (git rev-list --count origin/main..HEAD) so it is
1819
+ // unit-tested without a repo. '0' → allowed; anything else (or null) → refuse.
1820
+ function baseRefShipGuard(aheadCount) {
1821
+ if (aheadCount === '0') return { ok: true };
1822
+ const n = aheadCount == null ? 'an unknown number of' : aheadCount;
1823
+ return { ok: false, reason: `HEAD is ${n} commit(s) ahead of origin/main — --base is only for work already merged to main` };
1824
+ }
1825
+
1826
+ // applyBaseOverride — the SINGLE source of truth for --base validation, shared by
1827
+ // shipMain + regradeMain so the security guard can never diverge between entry
1828
+ // points. Resolves the ref, enforces the 0-ahead invariant, and sets the module-
1829
+ // level baseOverrideSha on success. Returns { ok:true, sha } | { ok:false, reason }
1830
+ // (the caller prints + exits). runGit is injectable for tests.
1831
+ function applyBaseOverride(baseRefRaw, { runGit = gitOk } = {}) {
1832
+ const r = resolveBaseRef(baseRefRaw);
1833
+ if (!r.ok) return { ok: false, reason: `--base ${baseRefRaw} is unusable (${r.reason})` };
1834
+ // Freshen origin/main (best-effort; a stale ref only OVER-counts → errs to refuse).
1835
+ runGit('git fetch origin main --quiet');
1836
+ const guard = baseRefShipGuard(runGit('git rev-list --count origin/main..HEAD'));
1837
+ if (!guard.ok) return { ok: false, reason: guard.reason };
1838
+ baseOverrideSha = r.sha;
1839
+ return { ok: true, sha: r.sha };
1840
+ }
1841
+
1588
1842
  // #536: the grader's baseline is computed INDEPENDENTLY of touches[] — always
1589
1843
  // from merge-base, never gated on the declared scope. (Historically the
1590
1844
  // touches-gated drift scanner returned a null baseline for empty touches[],
1591
1845
  // which once blinded the grader to a fully-committed diff and hard-FAILed
1592
1846
  // empty-touches newcomer ships; the scanner is gone as of task 879, but the
1593
1847
  // grader keeping its own merge-base baseline remains the correct invariant.)
1594
- function computeGraderBaseline(claim) {
1848
+ // baseSha (task 1002422): a validated --base override wins over merge-base, so a
1849
+ // task whose commits already landed on main still grades its real diff instead of
1850
+ // seeing an empty one.
1851
+ function computeGraderBaseline(claim, baseSha = baseOverrideSha) {
1852
+ if (baseSha) return baseSha;
1595
1853
  return (
1596
1854
  gitOk('git merge-base origin/main HEAD') ||
1597
1855
  gitOk('git merge-base main HEAD') ||
@@ -1679,7 +1937,7 @@ async function runSubagentGrader({ task, valueSummary, baseline, changedFiles: p
1679
1937
  // fail-closed layers).
1680
1938
  let builderRank;
1681
1939
  try {
1682
- const meRes = await (await cliClient()).me.getMe();
1940
+ const meRes = noteServerReward(await (await cliClient()).me.getMe());
1683
1941
  if (meRes.ok) builderRank = meRes.data?.builder?.rank;
1684
1942
  } catch (_) { /* unknown rank → pre-pass fails open; audit + hook still gate */ }
1685
1943
  const runOpts = {
@@ -1860,6 +2118,23 @@ function regenerateApiDocs() {
1860
2118
  // (not fail-if-forgotten): before this, ship regenerated the spec but not the client,
1861
2119
  // so a route change left the client stale until the api_client test caught it in CI.
1862
2120
  // Non-fatal + safe on every ship path; the api_client freshness test backstops main.
2121
+
2122
+ // Where the devbox Electron app vendors the CJS client (ADR 0045). It lives inside the
2123
+ // dev-box MODULE (ADR 0145) — an instance that ships without that module has no copy.
2124
+ const VENDORED_CLIENT_REL = 'modules/dev-box/app/src/vendor/bongos-client.cjs';
2125
+
2126
+ // The git pathspec for the regenerated client. Pure + exported so the rule is unit-tested
2127
+ // with no git/fs (task 1002417). `git status` tolerates an unmatched pathspec (exit 0) but
2128
+ // `git add` aborts the ENTIRE staging step (exit 128), which leaves the regenerated client
2129
+ // unstaged and the worktree dirty — and a dirty worktree makes the ship's auto-merge bail,
2130
+ // stranding the task at `confirmed` with no PR. So the vendored copy is named ONLY when it
2131
+ // is actually on disk.
2132
+ function apiClientPathspec({ worktreeRoot, existsSync = fs.existsSync } = {}) {
2133
+ const paths = ['clients/bongos-client'];
2134
+ if (existsSync(path.join(worktreeRoot, VENDORED_CLIENT_REL))) paths.push(VENDORED_CLIENT_REL);
2135
+ return paths;
2136
+ }
2137
+
1863
2138
  function regenerateApiClient() {
1864
2139
  console.log(' regenerating typed API client (clients/bongos-client) from the OpenAPI spec...');
1865
2140
  if (!shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'gen-api-client.js'))}`)) {
@@ -1868,23 +2143,28 @@ function regenerateApiClient() {
1868
2143
  }
1869
2144
  // The devbox Electron app can't require() clients/ at runtime, so it vendors the CJS
1870
2145
  // build (ADR 0045); keep the copy byte-identical (the api_client test asserts it).
1871
- // Present only in the core repo — a consumer instance has no devbox-app/, so skip.
2146
+ // Present only where the dev-box module ships — an instance without it has no vendored
2147
+ // copy, so both the vendor step and the git pathspec below drop out (task 1002417: the
2148
+ // app moved from top-level devbox-app/ to modules/dev-box/app/ per ADR 0145, and these
2149
+ // paths were not updated, so the guard below silently never fired).
1872
2150
  // Resolve it WORKTREE-LOCALLY (like the generator above, __dirname=scripts/gds), NOT
1873
2151
  // via REPO_ROOT: on a ship from a linked worktree REPO_ROOT is the MAIN checkout, so a
1874
2152
  // REPO_ROOT path would vendor the main tree while gen-api-client just wrote THIS tree,
1875
2153
  // leaving this worktree's vendored copy stale → the api_client byte-identity test reds
1876
2154
  // the PR (task 2060 fix). __dirname keeps the generator, the vendored file, and the
1877
2155
  // git add all on the same tree.
1878
- const vendorScript = path.join(__dirname, '..', '..', 'devbox-app', 'scripts', 'vendor-client.js');
2156
+ const worktreeRoot = path.join(__dirname, '..', '..');
2157
+ const vendorScript = path.join(worktreeRoot, 'modules', 'dev-box', 'app', 'scripts', 'vendor-client.js');
1879
2158
  if (fs.existsSync(vendorScript)) {
1880
2159
  shellOk(`"${process.execPath}" ${JSON.stringify(vendorScript)}`);
1881
2160
  }
1882
- const dirty = gitOk('git status --porcelain clients/bongos-client devbox-app/src/vendor/bongos-client.cjs');
2161
+ const clientPathspec = apiClientPathspec({ worktreeRoot }).join(' ');
2162
+ const dirty = gitOk(`git status --porcelain ${clientPathspec}`);
1883
2163
  if (!dirty) {
1884
2164
  console.log(' API client already current — nothing to commit');
1885
2165
  return 'current';
1886
2166
  }
1887
- shellOk('git add clients/bongos-client devbox-app/src/vendor/bongos-client.cjs');
2167
+ shellOk(`git add ${clientPathspec}`);
1888
2168
  if (shellOk('git commit -m "chore(api-client): auto-regenerate typed client + devbox vendor (task 2051)"')) {
1889
2169
  console.log(' committed auto-regenerated API client');
1890
2170
  return 'committed';
@@ -2129,26 +2409,48 @@ async function autoMerge(taskId, valueSummary, { dbOnly = false } = {}) {
2129
2409
  return false;
2130
2410
  }
2131
2411
 
2132
- console.log(' triggering deploy on droplet...');
2133
- // Quote ~/deploy.sh so the LOCAL shell doesn't tilde-expand it to
2134
- // /Users/<mac-user>/deploy.sh before ssh transmits the command. With
2135
- // the quotes, ssh sends the literal `~/deploy.sh` and the REMOTE shell
2136
- // expands it to /home/lars/deploy.sh. (Matches /merge-mode SKILL.md:81.)
2137
- if (!shellOk(`ssh -o ConnectTimeout=10 ${DROPLET} "~/deploy.sh"`)) {
2138
- // task 984 / idea 180: ~/deploy.sh's internal /healthz curl can race the
2139
- // service-restart window (Node not yet bound) and exit non-zero even though
2140
- // the deploy succeeded — which used to false-fail the ship and force a
2141
- // needless /merge-mode. Before concluding failure, re-probe the public
2142
- // /healthz with backoff (curl's own --retry; cross-platform, no sleep).
2143
- // --retry-all-errors retries connection-refused during the bind window;
2144
- // -f makes a non-200 an error so a 502/503 also retries. If it comes up
2145
- // within the window the deploy really succeeded; only then-still-down fails.
2146
- console.log(' auto-merge: ~/deploy.sh returned non-zero — re-probing /healthz with backoff before concluding...');
2147
- if (shellOk('curl -fsS --max-time 10 --retry 5 --retry-delay 3 --retry-all-errors https://amazonprimea.com/healthz -o /dev/null')) {
2148
- console.log(' auto-merge: /healthz 200 on re-probe — deploy actually succeeded (deploy.sh raced the restart window). Proceeding.');
2149
- } else {
2150
- console.log(' auto-merge: deploy failed (/healthz never returned 200 after retries); task stays at confirmed');
2151
- return false;
2412
+ // Deploy step. The core carries NO deploy target of its own — under the
2413
+ // standalone model a core change reaches a live instance via package-core +
2414
+ // `bongos upgrade` (pin-bump), NOT an SSH deploy (ADR 0108). So unless an
2415
+ // instance whose own checkout IS its live server has configured a target
2416
+ // (config/deploy.json "sshTarget" or <PREFIX>_DEPLOY_SSH_TARGET), the land is
2417
+ // complete at "merged + pushed to main" and the SSH deploy is skipped. (This
2418
+ // used to SSH a hardcoded target — the founder instance's old game box, i.e. the
2419
+ // WRONG server post-migration. task 1002202.)
2420
+ const { sshTarget, healthzUrl } = resolveDeployTarget({
2421
+ configRaw: readDeployConfigRaw(),
2422
+ branding: safeBranding(),
2423
+ });
2424
+ if (!sshTarget) {
2425
+ console.log(' merged + pushed to main. No deploy target configured, so no SSH deploy —');
2426
+ console.log(' expected for the core. To publish to a live instance run the standalone release');
2427
+ console.log(' flow: package-core → bongos upgrade (pin-bump), per ADR 0108. (Not an SSH deploy.)');
2428
+ } else {
2429
+ console.log(` triggering deploy on ${sshTarget}...`);
2430
+ // Quote ~/deploy.sh so the LOCAL shell doesn't tilde-expand it before ssh
2431
+ // transmits the command; with the quotes ssh sends the literal `~/deploy.sh`
2432
+ // and the REMOTE shell expands it. (Matches /merge-mode SKILL.md:81.)
2433
+ if (!shellOk(`ssh -o ConnectTimeout=10 ${sshTarget} "~/deploy.sh"`)) {
2434
+ // task 984 / idea 180: ~/deploy.sh's internal /healthz curl can race the
2435
+ // service-restart window (Node not yet bound) and exit non-zero even though
2436
+ // the deploy succeeded — which used to false-fail the ship and force a
2437
+ // needless /merge-mode. Before concluding failure, re-probe the CONFIGURED
2438
+ // /healthz with backoff (curl's own --retry; cross-platform, no sleep) —
2439
+ // never a hardcoded host. --retry-all-errors retries connection-refused
2440
+ // during the bind window; -f makes a non-200 an error so a 502/503 also
2441
+ // retries. If it comes up within the window the deploy really succeeded.
2442
+ if (healthzUrl) {
2443
+ console.log(' auto-merge: ~/deploy.sh returned non-zero — re-probing /healthz with backoff before concluding...');
2444
+ if (shellOk(`curl -fsS --max-time 10 --retry 5 --retry-delay 3 --retry-all-errors ${JSON.stringify(healthzUrl)} -o /dev/null`)) {
2445
+ console.log(' auto-merge: /healthz 200 on re-probe — deploy actually succeeded (deploy.sh raced the restart window). Proceeding.');
2446
+ } else {
2447
+ console.log(' auto-merge: deploy failed (/healthz never returned 200 after retries); task stays at confirmed');
2448
+ return false;
2449
+ }
2450
+ } else {
2451
+ console.log(' auto-merge: ~/deploy.sh returned non-zero and no healthzUrl configured to re-probe; task stays at confirmed');
2452
+ return false;
2453
+ }
2152
2454
  }
2153
2455
  }
2154
2456
 
@@ -2344,7 +2646,7 @@ function formatGradeOutcome(g, taskId) {
2344
2646
  out.push('When the grader is reachable again, re-grade:');
2345
2647
  out.push(` node scripts/gds/ship.js ${taskId} --regrade`);
2346
2648
  out.push('Or land it now via manual-confirm:');
2347
- out.push(` curl -X POST -H "Authorization: Bearer <token>" https://amazonprimea.com/api/gds/tasks/${taskId}/confirm`);
2649
+ out.push(` curl -X POST -H "Authorization: Bearer <token>" ${resolveInstanceBase() || '<instance-origin>'}/api/gds/tasks/${taskId}/confirm`);
2348
2650
  return out.join('\n');
2349
2651
  }
2350
2652
 
@@ -2371,7 +2673,7 @@ function formatGradeOutcome(g, taskId) {
2371
2673
  out.push('Grade failed — task stays at completed. Fix the issues above, commit, then re-grade:');
2372
2674
  out.push(` node scripts/gds/ship.js ${taskId} --regrade`);
2373
2675
  out.push('Last resort (only if the grade was wrong):');
2374
- out.push(` curl -X POST -H "Authorization: Bearer <token>" https://amazonprimea.com/api/gds/tasks/${taskId}/confirm`);
2676
+ out.push(` curl -X POST -H "Authorization: Bearer <token>" ${resolveInstanceBase() || '<instance-origin>'}/api/gds/tasks/${taskId}/confirm`);
2375
2677
  }
2376
2678
  return out.join('\n');
2377
2679
  }
@@ -2391,7 +2693,14 @@ function printCreditsAndAchievements({ postGradeStatus, credits, creditsRaw, cre
2391
2693
  if (postGradeStatus === 'completed') {
2392
2694
  console.log(`Credits awarded: ${credits} (none — task at completed, grade not passed)`);
2393
2695
  } else if (postGradeStatus === 'confirmed') {
2394
- console.log(`Credits awarded: ${credits} (landed at confirmed)`);
2696
+ console.log(`Credits awarded: ${credits} (none yet — credits land when the task merges to main)`);
2697
+ } else if (!rewardPolicy(serverReward).perTaskCredit) {
2698
+ // cost-plus-only (ADR 0146): the per-task estimate is never paid, so this is
2699
+ // 0 BY DESIGN. Left unexplained it reads as a broken payout — it sent a real
2700
+ // session diagnosing the reward gate on 2026-07-26 (task 1002469).
2701
+ console.log(`Credits awarded: ${credits} — expected: this instance pays cost-plus only.`);
2702
+ console.log(" The task's estimate is a guide, never a payout. You're paid for this session's");
2703
+ console.log(' real cost + 20%, booked separately from your token spend.');
2395
2704
  } else {
2396
2705
  console.log(`Credits awarded: ${credits}`);
2397
2706
  }
@@ -2526,7 +2835,7 @@ async function postShipSync(taskId, postGradeStatus) {
2526
2835
  // read make the script exit non-zero after a successful ship.
2527
2836
  async function onboardingNudgeBestEffort() {
2528
2837
  try {
2529
- const meRes = await (await cliClient()).me.getMe();
2838
+ const meRes = noteServerReward(await (await cliClient()).me.getMe());
2530
2839
  if (meRes.ok) {
2531
2840
  console.log('');
2532
2841
  printOnboardingNudge(meRes.data);
@@ -2671,7 +2980,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage }) {
2671
2980
  let total = null;
2672
2981
  let widgetsEnabled = true;
2673
2982
  try {
2674
- const me = await (await cliClient()).me.getMe();
2983
+ const me = noteServerReward(await (await cliClient()).me.getMe());
2675
2984
  if (me.ok && me.data) {
2676
2985
  if (me.data.builder) total = me.data.builder.total_credits;
2677
2986
  if (me.data.render) widgetsEnabled = me.data.render.widgets_enabled !== false;
@@ -2717,7 +3026,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage }) {
2717
3026
  added,
2718
3027
  removed,
2719
3028
  grade: card.grade || null,
2720
- credits: { awarded: card.creditsAwarded, total },
3029
+ credits: { awarded: card.creditsAwarded, total, perTaskCredit: rewardPolicy(serverReward).perTaskCredit },
2721
3030
  usage: { tokens, costUsd, newTokens, cachedTokens },
2722
3031
  deploy: { commit },
2723
3032
  };
@@ -2742,6 +3051,20 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage }) {
2742
3051
  } catch (_) {
2743
3052
  /* a summary card must never fail a ship */
2744
3053
  }
3054
+
3055
+ // task 1002521: the card above says SHIPPED, which means "merged" — it does
3056
+ // NOT mean users can see it. Publishing is manual and owner-gated, so a ship
3057
+ // can land on main and change nothing live, and until now nothing said so.
3058
+ // Blocker 1000097: 171 commits (a whole governance system) sat unreleased for
3059
+ // ten days while every layer reported success. Warn, never block — a shipped
3060
+ // task is still shipped, and an offline builder must not be punished for it.
3061
+ try {
3062
+ const { check, report } = require('./deploy-divergence');
3063
+ const r = await check();
3064
+ if (!r.undetermined && !r.ok) report(r);
3065
+ } catch (_) {
3066
+ /* a divergence warning must never fail a ship */
3067
+ }
2745
3068
  }
2746
3069
 
2747
3070
  // Decide whether a builder may re-grade a task via `--regrade` (#678). Pure so
@@ -2882,7 +3205,7 @@ async function shipMain() {
2882
3205
  const args = process.argv.slice(2);
2883
3206
  const taskId = Number(args[0]);
2884
3207
  if (!Number.isFinite(taskId)) {
2885
- console.error('usage: node scripts/gds/ship.js <task-id> --notes "..." --summary "..." [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty]');
3208
+ console.error('usage: node scripts/gds/ship.js <task-id> --notes "..." --summary "..." [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty] [--base <ref>]');
2886
3209
  process.exit(2);
2887
3210
  }
2888
3211
  const notes = arg('--notes', args) || '';
@@ -2891,7 +3214,12 @@ async function shipMain() {
2891
3214
  const force = args.includes('--force');
2892
3215
  // task 391 (idea #37): a deliberate zero-commit / direct-SQL ship. Without it,
2893
3216
  // a branch with no commits ahead of main is REFUSED (not silently shipped).
2894
- const dbOnly = args.includes('--db-only') || args.includes('--allow-empty');
3217
+ // task 1002422: --base <ref> also implies db-only — the work is already on main
3218
+ // (that's the whole point of --base), so the merge leg has nothing to merge and
3219
+ // advances straight to shipped. Unlike --allow-empty it does NOT skip the grade:
3220
+ // the pinned baseline gives the grader a real diff (see resolveBaseRef / main).
3221
+ const baseRefRaw = arg('--base', args);
3222
+ const dbOnly = args.includes('--db-only') || args.includes('--allow-empty') || Boolean(baseRefRaw);
2895
3223
  // task 1053 (F): explicit attestation that a verification task's verify
2896
3224
  // actually passed. Required to confirm a kind='verify' task (the server
2897
3225
  // enforces it too); a no-op for every other task.
@@ -2913,7 +3241,22 @@ async function shipMain() {
2913
3241
  const surpriseProvided = surpriseRaw !== null;
2914
3242
  const noSurprise = args.includes('--no-surprise');
2915
3243
 
2916
- const me = await (await cliClient()).me.getMe();
3244
+ // task 1002422 (blocker 1000082): resolve + validate --base BEFORE any heavy
3245
+ // work so a bad ref aborts with the claim STILL ACTIVE. On success it sets the
3246
+ // module-level baseOverrideSha that computeGraderBaseline() reads, so the grader
3247
+ // reads <base>...HEAD (a real diff) and the ship takes the rank-open graded path
3248
+ // instead of the --allow-empty → skip-grade → Archon-only /confirm dead-end.
3249
+ if (baseRefRaw) {
3250
+ const res = applyBaseOverride(baseRefRaw);
3251
+ if (!res.ok) {
3252
+ console.error(`Ship aborted: ${res.reason}. No claim resolved, no credits awarded.`);
3253
+ console.error('Omit --base and ship normally — the grader reviews ALL your commits and the merge lands them. Your claim is still active.');
3254
+ process.exit(2);
3255
+ }
3256
+ console.log(`--base: grading ${res.sha.slice(0, 9)}...HEAD — the task's real committed diff (work already on main). Graded via the rank-open path; no --allow-empty needed.`);
3257
+ }
3258
+
3259
+ const me = noteServerReward(await (await cliClient()).me.getMe());
2917
3260
  if (!me.ok) {
2918
3261
  console.error('failed to load profile:', me.data);
2919
3262
  process.exit(1);
@@ -3382,17 +3725,31 @@ async function regradeMain() {
3382
3725
  const args = process.argv.slice(2);
3383
3726
  const taskId = Number(args[0]);
3384
3727
  if (!Number.isFinite(taskId)) {
3385
- console.error('usage: node scripts/gds/ship.js <task-id> --regrade [--summary "..."] [--no-merge] [--force] [--verified]');
3728
+ console.error('usage: node scripts/gds/ship.js <task-id> --regrade [--summary "..."] [--no-merge] [--force] [--verified] [--base <ref>]');
3386
3729
  process.exit(2);
3387
3730
  }
3388
3731
  const summaryArg = arg('--summary', args) || '';
3389
3732
  const skipMerge = args.includes('--no-merge');
3390
3733
  const force = args.includes('--force');
3391
- const dbOnly = args.includes('--db-only') || args.includes('--allow-empty'); // task 391 — honor --db-only on a regrade too
3734
+ // task 1002422: honor --base on regrade/resume too, so `--regrade --base <ref>`
3735
+ // and shipMain's resume-grade dispatch grade the real <base>...HEAD diff and
3736
+ // advance already-on-main work, instead of aborting "nothing to grade". Same
3737
+ // 0-ahead security guard as shipMain (applyBaseOverride is the shared gate).
3738
+ const baseRefRaw = arg('--base', args);
3739
+ const dbOnly = args.includes('--db-only') || args.includes('--allow-empty') || Boolean(baseRefRaw); // task 391 — honor --db-only on a regrade too
3392
3740
  const verified = args.includes('--verified'); // task 1053 (F) — see gate below
3393
3741
 
3742
+ if (baseRefRaw) {
3743
+ const res = applyBaseOverride(baseRefRaw);
3744
+ if (!res.ok) {
3745
+ console.error(`Regrade aborted: ${res.reason}. Omit --base and re-grade normally.`);
3746
+ process.exit(2);
3747
+ }
3748
+ console.log(`--base: re-grading ${res.sha.slice(0, 9)}...HEAD (work already on main).`);
3749
+ }
3750
+
3394
3751
  // ---- Eligibility: completed + (owner | archon) ----
3395
- const me = await (await cliClient()).me.getMe();
3752
+ const me = noteServerReward(await (await cliClient()).me.getMe());
3396
3753
  if (!me.ok) {
3397
3754
  console.error('failed to load profile:', me.data);
3398
3755
  process.exit(1);
@@ -3517,6 +3874,14 @@ async function regradeMain() {
3517
3874
  // exported: it does I/O + network and is covered by the live smoke at ship.)
3518
3875
  module.exports = {
3519
3876
  scrubSecrets, snippetOf, buildSessionDigest, resolveTranscriptPath,
3877
+ // task 1002471: the project-path encoder, exported pure so the win32 shapes
3878
+ // (drive colon, backslash separators, spaces) are pinned cross-platform —
3879
+ // resolveTranscriptPath itself runs path.resolve, which can't carry a Windows
3880
+ // path through a Linux CI run.
3881
+ encodeProjectPath,
3882
+ // task 1002417: the regenerated-client pathspec rule, exported pure so the
3883
+ // "never name a path git add cannot match" invariant is tested without git/fs.
3884
+ apiClientPathspec, VENDORED_CLIENT_REL,
3520
3885
  // task 1003: transcript-grade scrubber internals, exported pure so the planted-
3521
3886
  // secret corpus test (tests/scrubber_corpus.mjs) can exercise each layer + tune.
3522
3887
  shannonEntropy, looksHighEntropySecret, redactHighEntropy, collectEnvSecrets,
@@ -3524,7 +3889,7 @@ module.exports = {
3524
3889
  // #678: re-grade path + the grade-step helpers it shares with shipMain.
3525
3890
  // Exporting these formalizes learning #43 (which had to "replicate ~10
3526
3891
  // lines" because runSubagentGrader wasn't reachable).
3527
- evaluateRegradeEligibility, computeGraderBaseline, computeChangedFiles,
3892
+ evaluateRegradeEligibility, computeGraderBaseline, computeChangedFiles, resolveBaseRef, baseRefShipGuard,
3528
3893
  runSubagentGrader, buildGradePayload,
3529
3894
  // V4.R08: diff fingerprint + grade-reuse helpers — exported pure so the
3530
3895
  // truth table (same hash+pass→reuse, fail→no reuse, mismatch→re-grade,
@@ -3548,7 +3913,12 @@ module.exports = {
3548
3913
  gitArgvOk, buildMergeArgv, sanitizeGitHubRefs,
3549
3914
  // #679 / ADR 0042: CI deploy mode — pure helpers exported for the regression
3550
3915
  // test (mode precedence/safe-default, no-shell PR argv, deploy-run match).
3551
- parseDeployMode, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded,
3916
+ // task 1002202: resolveDeployTarget — SSH target / healthz precedence + the safe
3917
+ // "no target" default (the core has none), exported for the same reason.
3918
+ // task 1002232: resolveInstanceBase — the ci-land/guidance origin precedence
3919
+ // (env → session api_base → branding publicOrigin → null), pure over injected
3920
+ // inputs so the precedence + the "no hardcoded host" guarantee are unit-tested.
3921
+ parseDeployMode, resolveDeployTarget, resolveInstanceBase, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded,
3552
3922
  // task 1049 (B): pure CI-land outcome classifier — a timed-out-but-armed PR is
3553
3923
  // 'pending' (success), not failure. Exported so the semantics are unit-tested.
3554
3924
  classifyLandOutcome,
@@ -3567,6 +3937,10 @@ module.exports = {
3567
3937
  pickStaleGateReviewRun,
3568
3938
  // task 1068: pure pre-stage-merge decision (skip on main/current, else merge).
3569
3939
  decidePreMerge,
3940
+ // task 1002475: pure `git log --diff-filter=D` output parser behind
3941
+ // preflight's upstream-deletion warning — exported so it's unit-tested
3942
+ // without a live git repo.
3943
+ parseDeletedUpstreamPaths,
3570
3944
  // task 1052 (E): box-aware "what to do next" for a confirmed-strand — a box
3571
3945
  // (via 'server') is pointed at the resume, never at gh-only /merge-mode.
3572
3946
  strandNextStep,