@bongos/core 1.18.3 → 1.18.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/.bongos-core.json +1575 -285
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +4 -2
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +6 -1
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/status-mirror-sync.js +114 -84
  347. package/src/bongos/api-errors.js +1 -0
  348. package/src/bongos/auth.js +447 -102
  349. package/src/bongos/db-kernel.js +27 -0
  350. package/src/bongos/db.js +39 -0
  351. package/src/bongos/module-scope-map.js +27 -1
  352. package/src/bongos/permission-path-check.js +136 -102
  353. package/src/bongos/repo-info.js +47 -11
  354. package/src/bongos/route-rank-check.js +429 -46
  355. package/src/bongos/routes/_helpers.js +12 -1
  356. package/src/bongos/routes/auth.js +219 -2
  357. package/src/bongos/routes/backup.js +58 -2
  358. package/src/bongos/routes/me.js +35 -1
  359. package/src/bongos/serve-internal.js +84 -15
  360. package/src/branding.js +32 -0
  361. package/src/instance-config.js +30 -0
  362. package/src/module-api.js +112 -1
  363. package/src/platform-server.js +86 -9
  364. package/tests/abandon_task.mjs +59 -3
  365. package/tests/api_cli_flags.mjs +53 -0
  366. package/tests/auth_page_gate.mjs +13 -0
  367. package/tests/backfill_sessions.mjs +127 -1
  368. package/tests/backup_cli.mjs +54 -0
  369. package/tests/bongos_box_connect.mjs +77 -0
  370. package/tests/box_access.mjs +130 -1
  371. package/tests/box_credential.mjs +125 -0
  372. package/tests/box_host_keys.mjs +24 -0
  373. package/tests/box_infra_resolve.mjs +149 -0
  374. package/tests/box_scope_session.mjs +6 -2
  375. package/tests/box_sync.mjs +55 -0
  376. package/tests/box_task_scope.mjs +99 -0
  377. package/tests/branding.mjs +12 -0
  378. package/tests/cf_ruleset_preflight.mjs +142 -0
  379. package/tests/cf_tunnel.mjs +37 -0
  380. package/tests/claim_gate_rebase.mjs +98 -0
  381. package/tests/clawback_confirmed_backlog.mjs +103 -0
  382. package/tests/cli_lib_formatters.mjs +20 -0
  383. package/tests/confirm_attribution.mjs +30 -46
  384. package/tests/credit_grant.mjs +198 -10
  385. package/tests/criterion_authoring_gate.mjs +272 -0
  386. package/tests/criterion_progress.mjs +33 -1
  387. package/tests/dev_lib.mjs +253 -0
  388. package/tests/devbox_artifact_gate.mjs +75 -0
  389. package/tests/devbox_dmg_backdrop.mjs +137 -0
  390. package/tests/devbox_icons.mjs +304 -0
  391. package/tests/devbox_installer_brand.mjs +232 -0
  392. package/tests/devbox_key_register.mjs +234 -0
  393. package/tests/docs_openapi_route.mjs +110 -0
  394. package/tests/doctor_hooks.mjs +141 -0
  395. package/tests/extract_json.mjs +140 -0
  396. package/tests/fitness.mjs +89 -0
  397. package/tests/gen_freshness.mjs +96 -0
  398. package/tests/goal_advisory.mjs +251 -0
  399. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  400. package/tests/goal_routes.mjs +59 -4
  401. package/tests/goal_tier.mjs +12 -3
  402. package/tests/governance_catalog.mjs +129 -0
  403. package/tests/governance_claim_eligibility.mjs +262 -0
  404. package/tests/governance_config.mjs +187 -0
  405. package/tests/governance_ownership_scope.mjs +410 -0
  406. package/tests/governance_page_gates.mjs +324 -0
  407. package/tests/governance_parity.mjs +336 -0
  408. package/tests/governance_principals.mjs +232 -0
  409. package/tests/governance_protected_surfaces.mjs +208 -0
  410. package/tests/governance_require_permission.mjs +223 -0
  411. package/tests/governance_resolver.mjs +84 -0
  412. package/tests/governance_role_sync.mjs +231 -0
  413. package/tests/governance_seed.mjs +127 -0
  414. package/tests/governance_session_scope.mjs +434 -0
  415. package/tests/governance_vanilla_boot.mjs +203 -0
  416. package/tests/grade_attribution.mjs +7 -7
  417. package/tests/hall_audit.mjs +223 -0
  418. package/tests/hall_nav.mjs +191 -0
  419. package/tests/hall_palette.mjs +206 -0
  420. package/tests/hall_signout.mjs +124 -0
  421. package/tests/hook_user_agents.mjs +10 -0
  422. package/tests/idea_triage.mjs +162 -0
  423. package/tests/idp_device_exchange.mjs +93 -0
  424. package/tests/init.mjs +51 -0
  425. package/tests/instance_config.mjs +65 -0
  426. package/tests/instance_mismatch_guard.mjs +23 -6
  427. package/tests/instance_release_config.mjs +104 -0
  428. package/tests/linkify_refs.mjs +15 -2
  429. package/tests/module_contributions.mjs +55 -0
  430. package/tests/module_loader.mjs +10 -5
  431. package/tests/module_scope_active_claims.mjs +94 -0
  432. package/tests/needs_rebase.mjs +92 -0
  433. package/tests/one_active_claim_per_session.mjs +76 -0
  434. package/tests/override_attribution.mjs +10 -8
  435. package/tests/override_request_ownership.mjs +8 -4
  436. package/tests/parse_cookie_decode.mjs +42 -0
  437. package/tests/pay_on_land.mjs +129 -0
  438. package/tests/platform_boot.mjs +100 -1
  439. package/tests/post_login_landing.mjs +42 -0
  440. package/tests/preflight_checks.mjs +72 -0
  441. package/tests/provision.mjs +132 -7
  442. package/tests/publish_branch_route.mjs +53 -0
  443. package/tests/publish_reconciler.mjs +187 -2
  444. package/tests/repo_info_binding.mjs +248 -0
  445. package/tests/reward_gate.mjs +11 -2
  446. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  447. package/tests/reward_mode_surfacing.mjs +168 -0
  448. package/tests/route_permission_check.mjs +301 -0
  449. package/tests/route_rank_check.mjs +254 -12
  450. package/tests/route_validation.mjs +30 -0
  451. package/tests/sequence.mjs +267 -0
  452. package/tests/session_digest.mjs +44 -0
  453. package/tests/session_index.mjs +18 -6
  454. package/tests/session_records.mjs +39 -1
  455. package/tests/session_reward_on_land.mjs +381 -0
  456. package/tests/session_reward_reconcile.mjs +229 -0
  457. package/tests/ship_api_client_pathspec.mjs +100 -0
  458. package/tests/ship_base_flag.mjs +93 -0
  459. package/tests/ship_ci_deploy.mjs +80 -3
  460. package/tests/ship_upstream_deletion.mjs +57 -0
  461. package/tests/smoke_target.mjs +233 -0
  462. package/tests/start_env_guard.mjs +87 -0
  463. package/tests/start_rebase_warning.mjs +103 -0
  464. package/tests/status_mirror_sync.mjs +122 -0
  465. package/tests/stop_refcheck.mjs +19 -1
  466. package/tests/task_goal_link.mjs +15 -6
  467. package/tests/upgrade.mjs +139 -0
  468. package/tests/web_admission_status.mjs +162 -0
  469. package/tests/write_validation_gaps.mjs +110 -0
@@ -68,7 +68,7 @@ async function main() {
68
68
  process.exit(1);
69
69
  }
70
70
 
71
- console.log('▸ Connecting to your Amazonprimea dev box…');
71
+ console.log(`▸ Connecting to your ${require('./cli-lib').productName()} dev box…`);
72
72
  const { box, keyPath } = await lib.prepareConnection({ log });
73
73
  const host = box.hostname;
74
74
 
@@ -81,8 +81,10 @@ async function main() {
81
81
  sshArgs.push(...remoteCmd);
82
82
  } else {
83
83
  firstRun = forceLogin || lib.isFirstLogin(host);
84
- sshArgs.push(lib.buildRemoteShellCommand({ firstRun }));
85
- console.log(`▸ Opening a shell on ${host} (${lib.WORKSPACE})…`);
84
+ // BV1.R101: re-scope /workspace to the active claim on connect, so an
85
+ // interactive shell always lands in a folder holding exactly that task's code.
86
+ sshArgs.push(lib.buildRemoteShellCommand({ firstRun, sync: true }));
87
+ console.log(`▸ Opening a shell on ${host} (${lib.WORKSPACE}, scoped to your active claim)…`);
86
88
  }
87
89
 
88
90
  const r = spawnSync('ssh', sshArgs, { stdio: 'inherit' });
@@ -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).
@@ -203,8 +223,14 @@ function claudeConfigBase() {
203
223
  }
204
224
 
205
225
  function encodeProjectPath(absProjectPath) {
206
- // Claude Code's encoding: replace every '/' and '.' with '-'.
207
- 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, '-');
208
234
  }
209
235
 
210
236
  // resolveTranscriptPath — find this session's transcript .jsonl. Claude Code
@@ -327,7 +353,11 @@ const {
327
353
  // snippetOf — short, single-line, scrubbed excerpt for footgun context.
328
354
  function snippetOf(text, extraSecrets, max = 160) {
329
355
  if (typeof text !== 'string' || text.length === 0) return undefined;
330
- 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();
331
361
  if (!flat) return undefined;
332
362
  const cut = flat.length > max ? flat.slice(0, max) : flat;
333
363
  const scrubbed = scrubSecrets(cut, extraSecrets);
@@ -859,6 +889,54 @@ function safeBranding() {
859
889
  }
860
890
  }
861
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
+
862
940
  // resolveDeployTarget — WHERE a laptop-mode ship SSHes `~/deploy.sh`, and which
863
941
  // /healthz it re-probes when deploy.sh's own health check races the restart window.
864
942
  // Pure (env + raw config + branding → target) and exported so the precedence and the
@@ -949,7 +1027,10 @@ function deployedCommitStatus(versionBody, mergeSha) {
949
1027
  }
950
1028
 
951
1029
  // Fetch + parse the prod /version JSON (#858). null on any failure — never throws.
952
- 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
953
1034
  // stdio override DISCARDS stderr (a curl progress/error line must not pollute
954
1035
  // the JSON parse) — the documented per-caller difference from ghJson.
955
1036
  const out = argvOut('curl', ['-fsS', '--max-time', '10', url], { stdio: ['ignore', 'pipe', 'ignore'] });
@@ -1092,11 +1173,15 @@ async function ciLandServer(taskId, branch, valueSummary) {
1092
1173
  // Pure + exported for unit tests (no git/network). Accepts the publish-status
1093
1174
  // response shape verbatim — getPublishStatus returns snake_case (pr_state,
1094
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.
1095
1180
  function classifyLandOutcome(s) {
1096
1181
  const merged = !!(s && s.merged);
1097
1182
  const deployStatus = s && (s.deploy_status ?? s.deployStatus);
1098
1183
  const prState = s && (s.pr_state ?? s.prState);
1099
- if (merged && deployStatus === 'success') return 'shipped';
1184
+ if (merged && (deployStatus === 'success' || deployStatus === 'unconfigured')) return 'shipped';
1100
1185
  if (prState === 'closed' && !merged) return 'failed';
1101
1186
  return 'pending';
1102
1187
  }
@@ -1281,11 +1366,12 @@ async function ciLandLocal(taskId, branch, valueSummary) {
1281
1366
  // GitHub's PR/issue number space (dead links for GDS task ids — task 916),
1282
1367
  // so the task ref is written as "task NNN" and the summary is de-#-ed.
1283
1368
  const title = `task ${taskId}: ${sanitizeGitHubRefs(valueSummary) || branch}`.slice(0, 240);
1369
+ const deployTargetNote = resolveInstanceBase() || 'the configured instance';
1284
1370
  const body =
1285
1371
  `Auto-opened by ship.js (deploy mode: ci) for task ${taskId}.\n\n` +
1286
1372
  `${sanitizeGitHubRefs(valueSummary) || '(no value summary)'}\n\n` +
1287
1373
  `Merging this PR triggers .github/workflows/deploy-prod.yml to deploy to ` +
1288
- `https://amazonprimea.com. Auto-merge is enabled — GitHub lands it once the ` +
1374
+ `${deployTargetNote}. Auto-merge is enabled — GitHub lands it once the ` +
1289
1375
  `required checks pass.`;
1290
1376
  if (!existing || existing.state === 'CLOSED' || existing.state === 'MERGED') {
1291
1377
  console.log(' ci-land: opening PR -> main...');
@@ -1367,12 +1453,14 @@ function pollCiDeploy(taskId, branch, headSha) {
1367
1453
  // real deploy into a false negative on a transient miss or a concurrent
1368
1454
  // later deploy).
1369
1455
  const status = deployedCommitStatus(fetchDeployedVersion(), mergeSha);
1456
+ const instanceBase = resolveInstanceBase();
1457
+ const healthzUrl = instanceBase ? `${instanceBase}/healthz` : null;
1370
1458
  if (status === 'match') {
1371
1459
  console.log(' ci-land: /version confirms prod is live on the merge commit (commit-pinned).');
1372
1460
  } else if (status === 'mismatch') {
1373
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).');
1374
- } else if (shellOk('curl -fsS --max-time 10 https://amazonprimea.com/healthz -o /dev/null')) {
1375
- 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.`);
1376
1464
  } else {
1377
1465
  console.log(' ci-land: deploy run succeeded but /version + healthz probes both missed (likely transient). Treating the successful deploy run as authoritative.');
1378
1466
  }
@@ -1397,6 +1485,16 @@ function pollCiDeploy(taskId, branch, headSha) {
1397
1485
  // Also refuses when the branch is far behind origin/main — merging a stale
1398
1486
  // branch is a known regression vector.
1399
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
+
1400
1498
  function preflight(claim, opts = {}) {
1401
1499
  // (a) Working-tree check — refuse if any TRACKED file carries an uncommitted
1402
1500
  // modification or staged change. This fires post-commit, so a clean tree
@@ -1469,6 +1567,39 @@ function preflight(claim, opts = {}) {
1469
1567
  }
1470
1568
  }
1471
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
+
1472
1603
  console.log(` preflight: working tree clean, branch ${behind} commits behind main, ship tooling current — OK`);
1473
1604
  return { ok: true };
1474
1605
  }
@@ -1645,13 +1776,80 @@ function autoLinkifyChangedDocs(ac) {
1645
1776
  // full unified diff against the baseline, and (for the audit-trail signals)
1646
1777
  // the shortstat summary.
1647
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
+
1648
1842
  // #536: the grader's baseline is computed INDEPENDENTLY of touches[] — always
1649
1843
  // from merge-base, never gated on the declared scope. (Historically the
1650
1844
  // touches-gated drift scanner returned a null baseline for empty touches[],
1651
1845
  // which once blinded the grader to a fully-committed diff and hard-FAILed
1652
1846
  // empty-touches newcomer ships; the scanner is gone as of task 879, but the
1653
1847
  // grader keeping its own merge-base baseline remains the correct invariant.)
1654
- 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;
1655
1853
  return (
1656
1854
  gitOk('git merge-base origin/main HEAD') ||
1657
1855
  gitOk('git merge-base main HEAD') ||
@@ -1739,7 +1937,7 @@ async function runSubagentGrader({ task, valueSummary, baseline, changedFiles: p
1739
1937
  // fail-closed layers).
1740
1938
  let builderRank;
1741
1939
  try {
1742
- const meRes = await (await cliClient()).me.getMe();
1940
+ const meRes = noteServerReward(await (await cliClient()).me.getMe());
1743
1941
  if (meRes.ok) builderRank = meRes.data?.builder?.rank;
1744
1942
  } catch (_) { /* unknown rank → pre-pass fails open; audit + hook still gate */ }
1745
1943
  const runOpts = {
@@ -1920,6 +2118,23 @@ function regenerateApiDocs() {
1920
2118
  // (not fail-if-forgotten): before this, ship regenerated the spec but not the client,
1921
2119
  // so a route change left the client stale until the api_client test caught it in CI.
1922
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
+
1923
2138
  function regenerateApiClient() {
1924
2139
  console.log(' regenerating typed API client (clients/bongos-client) from the OpenAPI spec...');
1925
2140
  if (!shellOk(`"${process.execPath}" ${JSON.stringify(path.join(__dirname, 'gen-api-client.js'))}`)) {
@@ -1928,23 +2143,28 @@ function regenerateApiClient() {
1928
2143
  }
1929
2144
  // The devbox Electron app can't require() clients/ at runtime, so it vendors the CJS
1930
2145
  // build (ADR 0045); keep the copy byte-identical (the api_client test asserts it).
1931
- // 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).
1932
2150
  // Resolve it WORKTREE-LOCALLY (like the generator above, __dirname=scripts/gds), NOT
1933
2151
  // via REPO_ROOT: on a ship from a linked worktree REPO_ROOT is the MAIN checkout, so a
1934
2152
  // REPO_ROOT path would vendor the main tree while gen-api-client just wrote THIS tree,
1935
2153
  // leaving this worktree's vendored copy stale → the api_client byte-identity test reds
1936
2154
  // the PR (task 2060 fix). __dirname keeps the generator, the vendored file, and the
1937
2155
  // git add all on the same tree.
1938
- 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');
1939
2158
  if (fs.existsSync(vendorScript)) {
1940
2159
  shellOk(`"${process.execPath}" ${JSON.stringify(vendorScript)}`);
1941
2160
  }
1942
- 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}`);
1943
2163
  if (!dirty) {
1944
2164
  console.log(' API client already current — nothing to commit');
1945
2165
  return 'current';
1946
2166
  }
1947
- shellOk('git add clients/bongos-client devbox-app/src/vendor/bongos-client.cjs');
2167
+ shellOk(`git add ${clientPathspec}`);
1948
2168
  if (shellOk('git commit -m "chore(api-client): auto-regenerate typed client + devbox vendor (task 2051)"')) {
1949
2169
  console.log(' committed auto-regenerated API client');
1950
2170
  return 'committed';
@@ -2426,7 +2646,7 @@ function formatGradeOutcome(g, taskId) {
2426
2646
  out.push('When the grader is reachable again, re-grade:');
2427
2647
  out.push(` node scripts/gds/ship.js ${taskId} --regrade`);
2428
2648
  out.push('Or land it now via manual-confirm:');
2429
- 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`);
2430
2650
  return out.join('\n');
2431
2651
  }
2432
2652
 
@@ -2453,7 +2673,7 @@ function formatGradeOutcome(g, taskId) {
2453
2673
  out.push('Grade failed — task stays at completed. Fix the issues above, commit, then re-grade:');
2454
2674
  out.push(` node scripts/gds/ship.js ${taskId} --regrade`);
2455
2675
  out.push('Last resort (only if the grade was wrong):');
2456
- 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`);
2457
2677
  }
2458
2678
  return out.join('\n');
2459
2679
  }
@@ -2473,7 +2693,14 @@ function printCreditsAndAchievements({ postGradeStatus, credits, creditsRaw, cre
2473
2693
  if (postGradeStatus === 'completed') {
2474
2694
  console.log(`Credits awarded: ${credits} (none — task at completed, grade not passed)`);
2475
2695
  } else if (postGradeStatus === 'confirmed') {
2476
- 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.');
2477
2704
  } else {
2478
2705
  console.log(`Credits awarded: ${credits}`);
2479
2706
  }
@@ -2608,7 +2835,7 @@ async function postShipSync(taskId, postGradeStatus) {
2608
2835
  // read make the script exit non-zero after a successful ship.
2609
2836
  async function onboardingNudgeBestEffort() {
2610
2837
  try {
2611
- const meRes = await (await cliClient()).me.getMe();
2838
+ const meRes = noteServerReward(await (await cliClient()).me.getMe());
2612
2839
  if (meRes.ok) {
2613
2840
  console.log('');
2614
2841
  printOnboardingNudge(meRes.data);
@@ -2753,7 +2980,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage }) {
2753
2980
  let total = null;
2754
2981
  let widgetsEnabled = true;
2755
2982
  try {
2756
- const me = await (await cliClient()).me.getMe();
2983
+ const me = noteServerReward(await (await cliClient()).me.getMe());
2757
2984
  if (me.ok && me.data) {
2758
2985
  if (me.data.builder) total = me.data.builder.total_credits;
2759
2986
  if (me.data.render) widgetsEnabled = me.data.render.widgets_enabled !== false;
@@ -2799,7 +3026,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage }) {
2799
3026
  added,
2800
3027
  removed,
2801
3028
  grade: card.grade || null,
2802
- credits: { awarded: card.creditsAwarded, total },
3029
+ credits: { awarded: card.creditsAwarded, total, perTaskCredit: rewardPolicy(serverReward).perTaskCredit },
2803
3030
  usage: { tokens, costUsd, newTokens, cachedTokens },
2804
3031
  deploy: { commit },
2805
3032
  };
@@ -2824,6 +3051,20 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage }) {
2824
3051
  } catch (_) {
2825
3052
  /* a summary card must never fail a ship */
2826
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
+ }
2827
3068
  }
2828
3069
 
2829
3070
  // Decide whether a builder may re-grade a task via `--regrade` (#678). Pure so
@@ -2964,7 +3205,7 @@ async function shipMain() {
2964
3205
  const args = process.argv.slice(2);
2965
3206
  const taskId = Number(args[0]);
2966
3207
  if (!Number.isFinite(taskId)) {
2967
- 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>]');
2968
3209
  process.exit(2);
2969
3210
  }
2970
3211
  const notes = arg('--notes', args) || '';
@@ -2973,7 +3214,12 @@ async function shipMain() {
2973
3214
  const force = args.includes('--force');
2974
3215
  // task 391 (idea #37): a deliberate zero-commit / direct-SQL ship. Without it,
2975
3216
  // a branch with no commits ahead of main is REFUSED (not silently shipped).
2976
- 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);
2977
3223
  // task 1053 (F): explicit attestation that a verification task's verify
2978
3224
  // actually passed. Required to confirm a kind='verify' task (the server
2979
3225
  // enforces it too); a no-op for every other task.
@@ -2995,7 +3241,22 @@ async function shipMain() {
2995
3241
  const surpriseProvided = surpriseRaw !== null;
2996
3242
  const noSurprise = args.includes('--no-surprise');
2997
3243
 
2998
- 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());
2999
3260
  if (!me.ok) {
3000
3261
  console.error('failed to load profile:', me.data);
3001
3262
  process.exit(1);
@@ -3464,17 +3725,31 @@ async function regradeMain() {
3464
3725
  const args = process.argv.slice(2);
3465
3726
  const taskId = Number(args[0]);
3466
3727
  if (!Number.isFinite(taskId)) {
3467
- 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>]');
3468
3729
  process.exit(2);
3469
3730
  }
3470
3731
  const summaryArg = arg('--summary', args) || '';
3471
3732
  const skipMerge = args.includes('--no-merge');
3472
3733
  const force = args.includes('--force');
3473
- 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
3474
3740
  const verified = args.includes('--verified'); // task 1053 (F) — see gate below
3475
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
+
3476
3751
  // ---- Eligibility: completed + (owner | archon) ----
3477
- const me = await (await cliClient()).me.getMe();
3752
+ const me = noteServerReward(await (await cliClient()).me.getMe());
3478
3753
  if (!me.ok) {
3479
3754
  console.error('failed to load profile:', me.data);
3480
3755
  process.exit(1);
@@ -3599,6 +3874,14 @@ async function regradeMain() {
3599
3874
  // exported: it does I/O + network and is covered by the live smoke at ship.)
3600
3875
  module.exports = {
3601
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,
3602
3885
  // task 1003: transcript-grade scrubber internals, exported pure so the planted-
3603
3886
  // secret corpus test (tests/scrubber_corpus.mjs) can exercise each layer + tune.
3604
3887
  shannonEntropy, looksHighEntropySecret, redactHighEntropy, collectEnvSecrets,
@@ -3606,7 +3889,7 @@ module.exports = {
3606
3889
  // #678: re-grade path + the grade-step helpers it shares with shipMain.
3607
3890
  // Exporting these formalizes learning #43 (which had to "replicate ~10
3608
3891
  // lines" because runSubagentGrader wasn't reachable).
3609
- evaluateRegradeEligibility, computeGraderBaseline, computeChangedFiles,
3892
+ evaluateRegradeEligibility, computeGraderBaseline, computeChangedFiles, resolveBaseRef, baseRefShipGuard,
3610
3893
  runSubagentGrader, buildGradePayload,
3611
3894
  // V4.R08: diff fingerprint + grade-reuse helpers — exported pure so the
3612
3895
  // truth table (same hash+pass→reuse, fail→no reuse, mismatch→re-grade,
@@ -3632,7 +3915,10 @@ module.exports = {
3632
3915
  // test (mode precedence/safe-default, no-shell PR argv, deploy-run match).
3633
3916
  // task 1002202: resolveDeployTarget — SSH target / healthz precedence + the safe
3634
3917
  // "no target" default (the core has none), exported for the same reason.
3635
- parseDeployMode, resolveDeployTarget, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded,
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,
3636
3922
  // task 1049 (B): pure CI-land outcome classifier — a timed-out-but-armed PR is
3637
3923
  // 'pending' (success), not failure. Exported so the semantics are unit-tested.
3638
3924
  classifyLandOutcome,
@@ -3651,6 +3937,10 @@ module.exports = {
3651
3937
  pickStaleGateReviewRun,
3652
3938
  // task 1068: pure pre-stage-merge decision (skip on main/current, else merge).
3653
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,
3654
3944
  // task 1052 (E): box-aware "what to do next" for a confirmed-strand — a box
3655
3945
  // (via 'server') is pointed at the resume, never at gh-only /merge-mode.
3656
3946
  strandNextStep,