@bongos/core 1.18.3 → 1.18.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.bongos-core.json +1576 -286
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +4 -2
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +6 -1
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/migrate.sh +99 -14
  347. package/scripts/status-mirror-sync.js +114 -84
  348. package/src/bongos/api-errors.js +1 -0
  349. package/src/bongos/auth.js +447 -102
  350. package/src/bongos/db-kernel.js +27 -0
  351. package/src/bongos/db.js +39 -0
  352. package/src/bongos/module-scope-map.js +27 -1
  353. package/src/bongos/permission-path-check.js +136 -102
  354. package/src/bongos/repo-info.js +47 -11
  355. package/src/bongos/route-rank-check.js +429 -46
  356. package/src/bongos/routes/_helpers.js +12 -1
  357. package/src/bongos/routes/auth.js +219 -2
  358. package/src/bongos/routes/backup.js +58 -2
  359. package/src/bongos/routes/me.js +35 -1
  360. package/src/bongos/serve-internal.js +84 -15
  361. package/src/branding.js +32 -0
  362. package/src/instance-config.js +30 -0
  363. package/src/module-api.js +124 -1
  364. package/src/platform-server.js +86 -9
  365. package/tests/abandon_task.mjs +59 -3
  366. package/tests/api_cli_flags.mjs +53 -0
  367. package/tests/auth_page_gate.mjs +13 -0
  368. package/tests/backfill_sessions.mjs +127 -1
  369. package/tests/backup_cli.mjs +54 -0
  370. package/tests/bongos_box_connect.mjs +77 -0
  371. package/tests/box_access.mjs +130 -1
  372. package/tests/box_credential.mjs +125 -0
  373. package/tests/box_host_keys.mjs +24 -0
  374. package/tests/box_infra_resolve.mjs +149 -0
  375. package/tests/box_scope_session.mjs +6 -2
  376. package/tests/box_sync.mjs +55 -0
  377. package/tests/box_task_scope.mjs +99 -0
  378. package/tests/branding.mjs +12 -0
  379. package/tests/cf_ruleset_preflight.mjs +142 -0
  380. package/tests/cf_tunnel.mjs +37 -0
  381. package/tests/claim_gate_rebase.mjs +98 -0
  382. package/tests/clawback_confirmed_backlog.mjs +103 -0
  383. package/tests/cli_lib_formatters.mjs +20 -0
  384. package/tests/confirm_attribution.mjs +30 -46
  385. package/tests/credit_grant.mjs +198 -10
  386. package/tests/criterion_authoring_gate.mjs +272 -0
  387. package/tests/criterion_progress.mjs +33 -1
  388. package/tests/dev_lib.mjs +253 -0
  389. package/tests/devbox_artifact_gate.mjs +75 -0
  390. package/tests/devbox_dmg_backdrop.mjs +137 -0
  391. package/tests/devbox_icons.mjs +304 -0
  392. package/tests/devbox_installer_brand.mjs +232 -0
  393. package/tests/devbox_key_register.mjs +234 -0
  394. package/tests/docs_openapi_route.mjs +110 -0
  395. package/tests/doctor_hooks.mjs +141 -0
  396. package/tests/extract_json.mjs +140 -0
  397. package/tests/fitness.mjs +89 -0
  398. package/tests/gen_freshness.mjs +96 -0
  399. package/tests/goal_advisory.mjs +251 -0
  400. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  401. package/tests/goal_routes.mjs +59 -4
  402. package/tests/goal_tier.mjs +12 -3
  403. package/tests/governance_catalog.mjs +129 -0
  404. package/tests/governance_claim_eligibility.mjs +262 -0
  405. package/tests/governance_config.mjs +187 -0
  406. package/tests/governance_ownership_scope.mjs +410 -0
  407. package/tests/governance_page_gates.mjs +324 -0
  408. package/tests/governance_parity.mjs +336 -0
  409. package/tests/governance_principals.mjs +232 -0
  410. package/tests/governance_protected_surfaces.mjs +208 -0
  411. package/tests/governance_require_permission.mjs +223 -0
  412. package/tests/governance_resolver.mjs +84 -0
  413. package/tests/governance_role_sync.mjs +231 -0
  414. package/tests/governance_seed.mjs +127 -0
  415. package/tests/governance_session_scope.mjs +434 -0
  416. package/tests/governance_vanilla_boot.mjs +203 -0
  417. package/tests/grade_attribution.mjs +7 -7
  418. package/tests/hall_audit.mjs +223 -0
  419. package/tests/hall_nav.mjs +191 -0
  420. package/tests/hall_palette.mjs +206 -0
  421. package/tests/hall_signout.mjs +124 -0
  422. package/tests/hook_user_agents.mjs +10 -0
  423. package/tests/idea_triage.mjs +162 -0
  424. package/tests/idp_device_exchange.mjs +93 -0
  425. package/tests/init.mjs +51 -0
  426. package/tests/instance_config.mjs +65 -0
  427. package/tests/instance_mismatch_guard.mjs +23 -6
  428. package/tests/instance_release_config.mjs +104 -0
  429. package/tests/linkify_refs.mjs +15 -2
  430. package/tests/migrate_root_resolution.mjs +124 -1
  431. package/tests/module_contributions.mjs +55 -0
  432. package/tests/module_loader.mjs +10 -5
  433. package/tests/module_scope_active_claims.mjs +94 -0
  434. package/tests/needs_rebase.mjs +92 -0
  435. package/tests/one_active_claim_per_session.mjs +76 -0
  436. package/tests/override_attribution.mjs +10 -8
  437. package/tests/override_request_ownership.mjs +8 -4
  438. package/tests/parse_cookie_decode.mjs +42 -0
  439. package/tests/pay_on_land.mjs +129 -0
  440. package/tests/platform_boot.mjs +100 -1
  441. package/tests/post_login_landing.mjs +42 -0
  442. package/tests/preflight_checks.mjs +72 -0
  443. package/tests/provision.mjs +132 -7
  444. package/tests/publish_branch_route.mjs +53 -0
  445. package/tests/publish_reconciler.mjs +187 -2
  446. package/tests/repo_info_binding.mjs +248 -0
  447. package/tests/reward_gate.mjs +11 -2
  448. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  449. package/tests/reward_mode_surfacing.mjs +168 -0
  450. package/tests/route_permission_check.mjs +301 -0
  451. package/tests/route_rank_check.mjs +254 -12
  452. package/tests/route_validation.mjs +30 -0
  453. package/tests/sequence.mjs +267 -0
  454. package/tests/session_digest.mjs +44 -0
  455. package/tests/session_index.mjs +18 -6
  456. package/tests/session_records.mjs +39 -1
  457. package/tests/session_reward_on_land.mjs +381 -0
  458. package/tests/session_reward_reconcile.mjs +229 -0
  459. package/tests/ship_api_client_pathspec.mjs +100 -0
  460. package/tests/ship_base_flag.mjs +93 -0
  461. package/tests/ship_ci_deploy.mjs +80 -3
  462. package/tests/ship_upstream_deletion.mjs +57 -0
  463. package/tests/smoke_target.mjs +233 -0
  464. package/tests/start_env_guard.mjs +87 -0
  465. package/tests/start_rebase_warning.mjs +103 -0
  466. package/tests/status_mirror_sync.mjs +122 -0
  467. package/tests/stop_refcheck.mjs +19 -1
  468. package/tests/task_goal_link.mjs +15 -6
  469. package/tests/upgrade.mjs +139 -0
  470. package/tests/web_admission_status.mjs +162 -0
  471. package/tests/write_validation_gaps.mjs +110 -0
@@ -89,6 +89,128 @@ function ipv4Dispatcher() {
89
89
 
90
90
  const CfApiError = makeApiError('CfApiError', 'CF_API_ERROR');
91
91
 
92
+ // The Cloudflare token permission each ruleset phase is gated behind, named exactly
93
+ // as it reads in the dashboard's token editor so the guidance is click-followable.
94
+ const RULESET_PHASE_PERMISSION = {
95
+ [COOKIE_STRIP_PHASE]: 'Transform Rules',
96
+ [CACHE_BYPASS_PHASE]: 'Cache Rules',
97
+ };
98
+
99
+ // The one fix for every not-readable verdict: widen the token. Both edit scopes are
100
+ // named because provisioning applies BOTH ruleset legs (cookie-strip + cache-bypass)
101
+ // and an operator in the token editor should add them in one pass.
102
+ function rulesetScopeGuidance(zone) {
103
+ return `Widen CLOUDFLARE_API_TOKEN on the control plane: Cloudflare dashboard → My Profile → API Tokens → `
104
+ + `edit the token → Permissions → add "Zone → Transform Rules → Edit" (the ADR 0053 cookie-strip) and `
105
+ + `"Zone → Cache Rules → Edit" (the sandbox cache bypass) → Zone Resources → Include → Specific zone → ${zone} `
106
+ + `→ Continue → Save. Only the account owner can do this. Until then, apply the rule by hand in the dashboard `
107
+ + `(see docs/recipes/builder-box-lifecycle.md).`;
108
+ }
109
+
110
+ // PURE. Given the outcome of the READ-ONLY phase-entrypoint probe (HTTP status +
111
+ // parsed Cloudflare JSON), say what we actually know about the token's scope.
112
+ //
113
+ // HONESTY — THIS IS A NEGATIVE DETECTOR, NOT A POSITIVE ONE. The probe is a GET, so
114
+ // a 200 proves the token can READ the phase entrypoint; it does NOT prove the token
115
+ // can EDIT it. Cloudflare exposes no read-only way to prove edit scope — only the
116
+ // real PUT does. So:
117
+ // - 401/403 → DEFINITIVELY no scope. This is the actionable case the preflight
118
+ // exists to catch, and the only verdict we assert about the token.
119
+ // - 200/404 → readable. Edit scope stays UNPROVEN until the rule PUT runs.
120
+ // - anything else → inconclusive, reported as not-readable so the operator is
121
+ // warned rather than falsely reassured.
122
+ // The field is named `readable` and never `editable` for exactly this reason, and no
123
+ // verdict here may render as "scope confirmed".
124
+ function classifyRulesetScope({ status, body, phase, zone }) {
125
+ const z = String(zone || '');
126
+ const perm = RULESET_PHASE_PERMISSION[phase] || 'Transform Rules';
127
+ if (status === 401 || status === 403) {
128
+ return {
129
+ readable: false,
130
+ reason: `Cloudflare refused to read the "${phase}" ruleset on ${z} (HTTP ${status}) — the token definitively lacks zone "${perm}" scope there`,
131
+ guidance: rulesetScopeGuidance(z),
132
+ };
133
+ }
134
+ // A zone with no rules in this phase yet has no entrypoint ruleset. That is a
135
+ // legitimate empty state (ensureCookieStripRule's PUT creates it), not a denial.
136
+ if (status === 404) {
137
+ return {
138
+ readable: true,
139
+ reason: `the "${phase}" phase has no entrypoint ruleset on ${z} yet (HTTP 404) — readable; edit scope is NOT proven until the rule is applied`,
140
+ guidance: '',
141
+ };
142
+ }
143
+ if (status !== 200 || !body || body.success !== true) {
144
+ const errs = body && Array.isArray(body.errors) ? body.errors.map((e) => e && e.message).filter(Boolean).join('; ') : '';
145
+ return {
146
+ readable: false,
147
+ reason: `the "${phase}" ruleset probe on ${z} failed and is inconclusive (HTTP ${status}${errs ? `: ${errs}` : ''})`,
148
+ guidance: `Re-run once network/Cloudflare recovers. If it keeps failing, ${rulesetScopeGuidance(z)}`,
149
+ };
150
+ }
151
+ return {
152
+ readable: true,
153
+ reason: `the token can READ the "${phase}" ruleset on ${z} — edit scope is NOT proven until the rule is applied`,
154
+ guidance: '',
155
+ };
156
+ }
157
+
158
+ // Read-only preflight for a zone ruleset phase. Does its OWN raw fetch rather than
159
+ // routing through CfTunnelApi._req, because _req THROWS on failure and is dry-run
160
+ // gated — a preflight must return a verdict (never throw) and must run on the
161
+ // dry-run path too. Mirrors provision.js's checkDnsTokenScope.
162
+ //
163
+ // Deliberately does NOT use `GET /user/tokens/verify`: an IP-locked token answers
164
+ // "Invalid API Token" there even when it is valid (docs/recipes/builder-box-
165
+ // lifecycle.md). Probe the real operation instead.
166
+ //
167
+ // Returns { zone, phase, readable, reason, guidance[, zoneId] }. Never throws.
168
+ async function checkRulesetScope({
169
+ zone, phase = COOKIE_STRIP_PHASE, token = process.env.CLOUDFLARE_API_TOKEN,
170
+ fetchImpl, zoneId = null, dispatcher,
171
+ } = {}) {
172
+ const base = { zone, phase };
173
+ if (!zone) return { ...base, readable: false, reason: 'no Cloudflare zone given', guidance: 'set CLOUDFLARE_ZONE on the control plane' };
174
+ if (!token) {
175
+ return { ...base, readable: false, reason: `no CLOUDFLARE_API_TOKEN configured`,
176
+ guidance: `Set CLOUDFLARE_API_TOKEN in the control-plane box.env. ${rulesetScopeGuidance(zone)}` };
177
+ }
178
+ const doFetch = fetchImpl || (typeof fetch !== 'undefined' ? fetch : null);
179
+ if (!doFetch) return { ...base, readable: false, reason: 'no fetch implementation available', guidance: 'run on a Node with global fetch, or inject fetchImpl' };
180
+ const init = { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } };
181
+ if (dispatcher !== undefined) init.dispatcher = dispatcher;
182
+ else { const d = ipv4Dispatcher(); if (d) init.dispatcher = d; }
183
+
184
+ let id = zoneId;
185
+ if (!id) {
186
+ let res;
187
+ try {
188
+ res = await doFetch(`${CF_API_BASE}/zones?name=${encodeURIComponent(zone)}`, init);
189
+ } catch (e) {
190
+ return { ...base, readable: false, reason: `Cloudflare zone lookup failed: ${e.message}`, guidance: 'check control-plane network + token, then retry' };
191
+ }
192
+ const body = await res.json().catch(() => null);
193
+ const match = (body && Array.isArray(body.result) ? body.result : []).find((r) => r && r.id);
194
+ if (!match) {
195
+ const errs = body && Array.isArray(body.errors) ? body.errors.map((e) => e && e.message).filter(Boolean).join('; ') : '';
196
+ return { ...base, readable: false,
197
+ reason: `could not resolve zone "${zone}" (HTTP ${res.status}${errs ? `: ${errs}` : ''})`,
198
+ guidance: `Check that CLOUDFLARE_API_TOKEN is valid and its Zone Resources include ${zone}. ${rulesetScopeGuidance(zone)}` };
199
+ }
200
+ id = match.id;
201
+ }
202
+
203
+ let status = 0; let bodyJson = null;
204
+ try {
205
+ const res = await doFetch(`${CF_API_BASE}/zones/${id}/rulesets/phases/${phase}/entrypoint`, init);
206
+ status = res.status;
207
+ bodyJson = await res.json().catch(() => null);
208
+ } catch (e) {
209
+ return { ...base, zoneId: id, readable: false, reason: `Cloudflare ruleset probe failed: ${e.message}`, guidance: 'check control-plane network + token, then retry' };
210
+ }
211
+ return { ...base, zoneId: id, ...classifyRulesetScope({ status, body: bodyJson, phase, zone }) };
212
+ }
213
+
92
214
  // Deterministic per-builder names so teardown can find what provision created
93
215
  // WITHOUT persisting an id in the DB (no migration): the tunnel name and the DNS
94
216
  // hostname are both pure functions of the login.
@@ -359,25 +481,38 @@ class CfTunnelApi extends JsonHttpApi {
359
481
  const zoneId = await this.getZoneId(zone);
360
482
  await this.upsertDnsRoute({ zoneId, name: hostname, tunnelId });
361
483
  await this.upsertDnsRoute({ zoneId, name: previewHost, tunnelId });
484
+ // Both edge-rule legs are BEST-EFFORT and report their outcome STRUCTURALLY so
485
+ // the caller can surface a failure in its summary instead of leaving it buried
486
+ // in a mid-run log line (task 1002511). `applied` is false in dry-run because
487
+ // nothing WAS applied — these are security/caching controls, and a plan must
488
+ // never read as though the rule is in place.
489
+ const edgeRules = {
490
+ cookieStrip: { applied: false, error: null },
491
+ cacheBypass: { applied: false, error: null },
492
+ };
362
493
  // SECURITY (#919 / SR#11): strip the apex cookie at the edge for the box
363
- // subdomains. BEST-EFFORT — a CF token without Transform Rules scope must not
364
- // block provisioning (the box still works; the rule is defense-in-depth). Warn
494
+ // subdomains. A CF token without Transform Rules scope must not block
495
+ // provisioning (the box still works; the rule is defense-in-depth). Warn
365
496
  // loudly so the operator adds the scope or applies the rule once by hand.
366
497
  try {
367
498
  await this.ensureCookieStripRule({ zone });
499
+ edgeRules.cookieStrip.applied = !this.dryRun;
368
500
  } catch (e) {
501
+ edgeRules.cookieStrip.error = e.message;
369
502
  this.log(`⚠ #919 cookie-strip Transform Rule NOT applied (${e.message}) — add zone "Transform Rules" scope to CLOUDFLARE_API_TOKEN or apply it once in the CF dashboard (ADR 0053)`);
370
503
  }
371
504
  // #1065: stop Cloudflare's 4h Browser Cache TTL from serving stale sandbox
372
- // game code. BEST-EFFORT — a CF token without Cache-Rules scope must not block
373
- // provisioning. Warn loudly so the operator adds the scope or applies the rule
374
- // once by hand (operator blocker, like the cookie-strip rule).
505
+ // game code. A CF token without Cache-Rules scope must not block provisioning.
506
+ // Warn loudly so the operator adds the scope or applies the rule once by hand
507
+ // (operator blocker, like the cookie-strip rule).
375
508
  try {
376
509
  await this.ensureCacheBypassRule({ zone });
510
+ edgeRules.cacheBypass.applied = !this.dryRun;
377
511
  } catch (e) {
512
+ edgeRules.cacheBypass.error = e.message;
378
513
  this.log(`⚠ #1065 sandbox cache-bypass Cache Rule NOT applied (${e.message}) — add zone "Cache Rules" (Account Rulesets) scope to CLOUDFLARE_API_TOKEN or add it once in the CF dashboard`);
379
514
  }
380
- return { tunnelId, token, hostname, previewHostname: previewHost, created };
515
+ return { tunnelId, token, hostname, previewHostname: previewHost, created, edgeRules };
381
516
  }
382
517
 
383
518
  // Tear down the per-builder tunnel + its DNS route. Best-effort: each leg is
@@ -416,4 +551,8 @@ module.exports = {
416
551
  tunnelName, terminalHostname, previewHostname,
417
552
  // #1065: the sandbox edge cache-bypass rule constants, exported for the test.
418
553
  CACHE_BYPASS_PHASE, CACHE_BYPASS_RULE_DESC, CACHE_BYPASS_EXPRESSION,
554
+ // #919 / SR#11: the cookie-strip rule constants + the read-only scope preflight
555
+ // box.js runs before any droplet spend (task 1002511).
556
+ COOKIE_STRIP_PHASE, COOKIE_STRIP_RULE_DESC, COOKIE_STRIP_EXPRESSION,
557
+ classifyRulesetScope, checkRulesetScope,
419
558
  };
@@ -10,7 +10,7 @@
10
10
  const { execSync } = require('node:child_process');
11
11
  const fs = require('node:fs');
12
12
  const path = require('node:path');
13
- const { cliClient, requireSession, fmtTouches, printOnboardingNudge } = require('./cli-lib');
13
+ const { cliClient, requireSession, fmtTouches, printOnboardingNudge, rewardPolicy, costPlusOnlyNote } = require('./cli-lib');
14
14
  const { nextNumbers, padAdr, padMig } = require('./next-number');
15
15
  const { CORE_NS } = require('./migration-namespace');
16
16
  const { contextPack, formatContextPack } = require('./context-pack');
@@ -85,6 +85,28 @@ function captureHeadSha() {
85
85
  }
86
86
  }
87
87
 
88
+ // Auto-detect this claim's worktree name when --worktree is omitted, so the
89
+ // server's one-active-claim guard (keyed on the WORKING TREE, not the shared auth
90
+ // session) can tell genuinely-parallel worktrees apart. Only names a LINKED git
91
+ // worktree — where git-dir != git-common-dir, i.e. a real `.claude/worktrees/<name>`
92
+ // checkout — using its top-level directory basename (the `<name>` the convention
93
+ // already uses). The MAIN checkout returns null (unchanged behaviour: one claim at
94
+ // a time there — parallelism is what worktrees are for). Best-effort: any git
95
+ // hiccup yields null, so this can never break a claim.
96
+ function detectWorktreeName() {
97
+ try {
98
+ const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
99
+ const commonDir = execSync('git rev-parse --git-common-dir', { encoding: 'utf8' }).trim();
100
+ // Same dir → the main checkout, not a linked worktree → leave unnamed.
101
+ if (path.resolve(gitDir) === path.resolve(commonDir)) return null;
102
+ const top = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
103
+ const name = path.basename(top);
104
+ return name || null;
105
+ } catch (_) {
106
+ return null;
107
+ }
108
+ }
109
+
88
110
  // task 1278: nudge (don't block) when this session's build scripts are behind
89
111
  // origin/main. The completion cards + skills are CLI-script features that ride
90
112
  // the LOCAL checkout, not the deployed server — a worktree branched from a stale
@@ -233,7 +255,10 @@ async function main() {
233
255
  process.exit(2);
234
256
  }
235
257
  const wIdx = args.indexOf('--worktree');
236
- const worktree = wIdx >= 0 ? args[wIdx + 1] : null;
258
+ // Explicit --worktree always wins; otherwise auto-detect a linked worktree so
259
+ // parallel worktrees off one login each hold their own claim (the server guard
260
+ // keys on this). null in the main checkout → unchanged one-claim behaviour.
261
+ const worktree = (wIdx >= 0 ? args[wIdx + 1] : null) || detectWorktreeName();
237
262
  // touches[] cleanup [5/8] (task 880): touches[] overlap with another active
238
263
  // claim is ADVISORY for an interactive human claim (just a warning — git merge
239
264
  // is the real collision detector). The autonomous --parallel runner passes
@@ -255,7 +280,13 @@ async function main() {
255
280
  const { claim, task } = r.data;
256
281
  console.log(`Claimed task #${task.id}: ${task.title}`);
257
282
  console.log(`Claim id: ${claim.id}`);
258
- console.log(`Version: ${task.version_id} · Priority: P${task.priority ?? '?'} · Credit reward: ${task.credits_reward}`);
283
+ // The reward figure is the task's ADVERTISED estimate. On a cost-plus-only
284
+ // instance it is never paid, so label it as an estimate and name the real
285
+ // payer right here — this line is where the expectation forms (task 1002469).
286
+ const rewardLabel = rewardPolicy(r.data.reward).perTaskCredit ? 'Credit reward' : 'Credit reward (est. only)';
287
+ console.log(`Version: ${task.version_id} · Priority: P${task.priority ?? '?'} · ${rewardLabel}: ${task.credits_reward}`);
288
+ const cpNote = costPlusOnlyNote(r.data.reward);
289
+ if (cpNote) console.log(` ${cpNote}`);
259
290
  if (task.parallel_safe === true) console.log('Parallel-safe: yes (eligible for a parallel autonomous slot)');
260
291
  // BV1.R53 (task 1422): module_key is the primary scope signal. touches[] is
261
292
  // demoted to derived/advisory for module-keyed tasks; fall back to it for
@@ -27,16 +27,105 @@ const SESSION_READ_PATHS = [
27
27
  ...ic.configReadPaths('gds-session.json'),
28
28
  ...ic.configReadPaths('pms-session.json'),
29
29
  ];
30
- // The last-resort default API base — THIS instance's own publicOrigin from the
31
- // branding pack (task 1002201). One source of truth in instance-config so no CLI
32
- // script hard-codes another instance's URL; pre-ADR-0139 the literal
33
- // 'https://amazonprimea.com' was baked in at four sites here, so an unconfigured
34
- // consumer targeted the founder's (game-only) box. Override with <PREFIX>_API_BASE
35
- // for local dev / staging:
30
+ // The API base every CLI call targets, resolved ONCE through the single source of
31
+ // truth in instance-config: <PREFIX>_API_BASE (then legacy GDS_/OTB_/PMS_) → the
32
+ // persisted session's api_base → THIS instance's publicOrigin from the branding
33
+ // pack. Override for local dev / staging:
36
34
  // CLOUDBONGOS_API_BASE=http://localhost:3000 node scripts/gds/start.js
37
- // Resolves the configured prefix first, then legacy GDS_/OTB_/PMS_API_BASE.
38
- const DEFAULT_API_BASE = ic.defaultApiBase();
39
- const API_BASE = ic.resolveEnv('API_BASE') || DEFAULT_API_BASE;
35
+ // No CLI script hard-codes another instance's URL (task 1002201; pre-ADR-0139 the
36
+ // literal 'https://amazonprimea.com' was baked in at four sites here). The session
37
+ // leg was added at task 1002427 — this const previously stopped at the branding
38
+ // default, so module-level consumers (reauth's Settings URL, the hall links
39
+ // hallBase() prints) named the checkout's own instance instead of the one the
40
+ // builder is signed into.
41
+ const API_BASE = ic.resolveApiBase();
42
+
43
+ // User-Agent for the GDS CLI's HTTP calls — the branding-derived neutral platform UA
44
+ // from the single source, src/branding.js userAgent() (`<repo.name|envPrefix>-gds/1.0`,
45
+ // neutral fallback `cloud-bongos-gds/1.0`), matching the `cloud-bongos-` convention the
46
+ // hooks use (tests/hook_user_agents.mjs). Was the hardcoded 'amazonprimea-gds-cli/1.0'
47
+ // at both fetch sites — task 1002234. Resolved once so apiCall + cliClient never diverge
48
+ // and branding is read a single time; falls back neutrally if branding is unreadable.
49
+ const CLI_USER_AGENT = (() => {
50
+ try { return require('../../src/branding').userAgent(); }
51
+ catch (_) { return 'cloud-bongos-gds/1.0'; }
52
+ })();
53
+
54
+ // productName() — the instance's product name for user-facing CLI copy, from the
55
+ // branding pack (task 1002428). Was hardcoded as the founder brand at several
56
+ // print sites, so a non-OTB instance told ITS builders about someone else's
57
+ // product. Neutral fallback, never a foreign brand.
58
+ function productName() {
59
+ try {
60
+ const id = require('../../src/branding').branding().identity || {};
61
+ if (id.productName) return id.productName;
62
+ } catch (_) { /* fall through */ }
63
+ return 'Cloud Bongos';
64
+ }
65
+
66
+ // rewardPolicy() — the instance's RESOLVED reward policy (ADR 0146), for CLI copy
67
+ // that prints a task's `credits_reward`. On a `cost-plus-only` instance that
68
+ // number is an advertised GUIDE and is never paid; the cost-plus session reward
69
+ // is the sole stream. Printing it bare reads as a promise, and a builder who
70
+ // then ships and sees `+0` reasonably concludes the payout broke (it cost a real
71
+ // session on 2026-07-26 — task 1002469).
72
+ //
73
+ // PREFER THE SERVER'S ANSWER. The mode is INSTANCE config, and an instance's
74
+ // config/branding.json is NOT in the builder's checkout — a laptop resolving
75
+ // locally sees only config/branding.neutral.json and would report the paying
76
+ // default on the very instance that withholds. So callers pass the `reward`
77
+ // block the server sends (GET /me, POST /claims); the local read is a fallback
78
+ // for an older server that doesn't send one, and the ADR default is the floor.
79
+ // Resolution itself is always economy's (ADR 0146) — never re-derived here.
80
+ function rewardPolicy(fromServer) {
81
+ if (fromServer && typeof fromServer.mode === 'string') {
82
+ return { mode: fromServer.mode, perTaskCredit: fromServer.perTaskCredit !== false };
83
+ }
84
+ try {
85
+ const p = require('../../modules/economy/credits').rewardConfig();
86
+ if (p && typeof p.mode === 'string') return p;
87
+ } catch (_) { /* fall through */ }
88
+ return { mode: 'cost-plus-and-estimate', perTaskCredit: true };
89
+ }
90
+
91
+ // The one-line builder-facing explanation shown wherever an estimate is
92
+ // displayed on a cost-plus-only instance. Returns null on a paying instance so
93
+ // every call site is a no-op there. Single source for the wording so claim /
94
+ // start / ship say the SAME thing (task 1002469).
95
+ function costPlusOnlyNote(fromServer) {
96
+ if (rewardPolicy(fromServer).perTaskCredit) return null;
97
+ return 'This instance pays cost-plus only: you earn real session cost + 20%, not the estimate above.';
98
+ }
99
+
100
+ // hallBase(base) — the instance's builders' hall root for a given API base, e.g.
101
+ // https://cloudbongos.com/builders. Derived from the RESOLVED base (env → session →
102
+ // branding default), never a hardcoded host — so a CLI-printed link always points at
103
+ // the instance the command is actually talking to, not the founder's amazonprimea box
104
+ // (task 1002233; the printed-link twin of the task-1002201 default-base fix). Falls
105
+ // back to `${base}/builders` when base isn't a parseable absolute URL.
106
+ function hallBase(base = API_BASE) {
107
+ try { return `${new URL(base).origin}/builders`; }
108
+ catch { return `${String(base || '').replace(/\/+$/, '')}/builders`; }
109
+ }
110
+
111
+ // Node-24-on-Windows workaround: every CLI invocation here is a short-lived
112
+ // process that makes one or a handful of requests, but a kept-alive socket can
113
+ // still be mid-close when the caller's process.exit() runs, tripping a
114
+ // UV_HANDLE_CLOSING assertion that corrupts piped JSON stdout. Connection reuse
115
+ // buys these one-shot processes nothing, so route requests through a dispatcher
116
+ // with keep-alive effectively disabled (mirrors cf-tunnel.js's dispatcher
117
+ // pattern). Degrades to the default dispatcher if undici can't be required.
118
+ let _noKeepAliveDispatcher;
119
+ let _noKeepAliveTried = false;
120
+ function noKeepAliveDispatcher() {
121
+ if (_noKeepAliveTried) return _noKeepAliveDispatcher;
122
+ _noKeepAliveTried = true;
123
+ try {
124
+ const { Agent } = require('undici');
125
+ _noKeepAliveDispatcher = new Agent({ keepAliveTimeout: 1, keepAliveMaxTimeout: 1 });
126
+ } catch { _noKeepAliveDispatcher = undefined; }
127
+ return _noKeepAliveDispatcher;
128
+ }
40
129
 
41
130
  async function readSessionFile(p) {
42
131
  try {
@@ -97,18 +186,37 @@ function hasFlag(name, args = process.argv.slice(2)) {
97
186
  return args.includes(name);
98
187
  }
99
188
 
189
+ // stripBom(s) — remove a leading UTF-8 BOM (U+FEFF), the artifact PowerShell's
190
+ // `Out-File`/`>` redirection writes. JSON.parse does not strip it, so a
191
+ // --body-file written from PS 5.1 fails to parse unless this runs first.
192
+ // Non-string input passes through unchanged. Extracted (rather than left
193
+ // inline in api.js) so it's unit-testable — api.js itself has no exports.
194
+ function stripBom(s) {
195
+ if (typeof s !== 'string') return s;
196
+ return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
197
+ }
198
+
199
+ // resolveQuiet(args, env) — true when api.js's stderr status banner should be
200
+ // suppressed: the --quiet flag or the GDS_QUIET env var. Both default to the
201
+ // live process, matching hasFlag()'s own default-args contract. Default OFF —
202
+ // existing pipelines that scrape the banner off stderr are unaffected.
203
+ function resolveQuiet(args = process.argv.slice(2), env = process.env) {
204
+ return hasFlag('--quiet', args) || !!env.GDS_QUIET;
205
+ }
206
+
100
207
  // readSessionToken() — the { token, apiBase } a token-only raw fetch needs, read
101
- // synchronously from the session file. Distinct from API_BASE: apiBase here also
102
- // honors the PERSISTED session api_base (written by setup.js) AFTER the env
103
- // overrides — <PREFIX>_API_BASE (incl. legacy GDS_/PMS_) || session.api_base ||
104
- // default. Returns null when not signed in. Callers that must FAIL OPEN (e.g. the
105
- // sound player) use this + their own fetch rather than apiCall (which exits on 401).
208
+ // synchronously from the session file. apiBase resolves the one way every base
209
+ // resolution does — <PREFIX>_API_BASE (incl. legacy GDS_/PMS_) || session.api_base
210
+ // || the branding default — via ic.resolveApiBase, passing the session we already
211
+ // loaded so it needs no second disk read (task 1002427). Returns null when not
212
+ // signed in. Callers that must FAIL OPEN (e.g. the sound player) use this + their
213
+ // own fetch rather than apiCall (which exits on 401).
106
214
  function readSessionToken() {
107
215
  const j = loadSessionSync();
108
216
  if (!j || !j.token) return null;
109
217
  return {
110
218
  token: j.token,
111
- apiBase: ic.resolveEnv('API_BASE') || j.api_base || DEFAULT_API_BASE,
219
+ apiBase: ic.resolveApiBase({ session: j }),
112
220
  };
113
221
  }
114
222
 
@@ -121,7 +229,7 @@ function printUnauthGuidanceAndExit() {
121
229
  // Stable, single-screen guidance. Builders should see exactly this when
122
230
  // their CLI session breaks — no raw API JSON, no stack trace.
123
231
  console.error('');
124
- console.error('✖ Your GDS CLI session is no longer valid.');
232
+ console.error('✖ Your Bongos CLI session is no longer valid.');
125
233
  console.error('');
126
234
  console.error(' Recover the fast (UI-first) way — run /builder-reauth, or:');
127
235
  console.error(' node scripts/gds/reauth.js');
@@ -164,7 +272,7 @@ function printBoxScopeGuidanceAndExit() {
164
272
  console.error('');
165
273
  console.error(' /builder-reauth (or: node scripts/gds/reauth.js)');
166
274
  console.error('');
167
- console.error(' It opens your Settings page (https://amazonprimea.com/builders/settings) on');
275
+ console.error(` It opens your Settings page (${hallBase()}/settings) on`);
168
276
  console.error(' YOUR machine — under "CLI Access" click "Re-issue CLI token", copy the one-liner, paste it back.');
169
277
  console.error(' No terminal Device Flow. Once re-authed, re-run whichever command you were');
170
278
  console.error(' trying — if it had already done work, it resumes from where it stopped (task 941).');
@@ -189,6 +297,18 @@ function safeBranding() {
189
297
  catch { return {}; }
190
298
  }
191
299
 
300
+ // True when THIS checkout declares an instance identity — i.e. config/branding.json
301
+ // exists. A NEUTRAL checkout (the portable core, or a fresh template carrying only
302
+ // branding.neutral.json) returns false: its resolved publicOrigin is just the neutral
303
+ // placeholder, not a real declared instance, so there is nothing to enforce a session
304
+ // against. Injectable so the pure guard stays unit-testable. (task 1002399, area 2)
305
+ function defaultHasInstanceConfig() {
306
+ try {
307
+ const { INSTANCE_PATH } = require('../../src/branding');
308
+ return !!(INSTANCE_PATH && fs.existsSync(INSTANCE_PATH));
309
+ } catch { return false; }
310
+ }
311
+
192
312
  // Host of an origin string, or null if unparseable. Comparing HOSTS (not full
193
313
  // origins) tolerates scheme / trailing-slash noise between branding + session.
194
314
  function instanceOriginHost(origin) {
@@ -200,12 +320,18 @@ function instanceOriginHost(origin) {
200
320
  // DIFFERENT instance than this repo is configured for. Fails OPEN (no mismatch)
201
321
  // when: an explicit <PREFIX>_API_BASE override is set (dev/staging — the operator
202
322
  // is deliberately steering the base); OR there is no session (nothing to borrow —
203
- // the no-session/401 path handles that); OR the repo declares no publicOrigin
323
+ // the no-session/401 path handles that); OR this checkout is NEUTRAL (no
324
+ // config/branding.json — no declared instance to enforce, so trust the session:
325
+ // this is what lets a neutral core checkout run every /builder-* command with NO
326
+ // <PREFIX>_API_BASE prefix — task 1002399); OR the repo declares no publicOrigin
204
327
  // (unknown identity — you can't assert a mismatch you can't define). Otherwise the
205
- // mismatch is expectedHost !== sessionHost.
206
- function detectInstanceMismatch({ session, brand = safeBranding(), env = process.env } = {}) {
328
+ // mismatch is expectedHost !== sessionHost — the guard that still fires for a
329
+ // BRANDED checkout whose session points at a different instance (the 2026-07-08
330
+ // hazard).
331
+ function detectInstanceMismatch({ session, brand = safeBranding(), env = process.env, hasInstanceConfig = defaultHasInstanceConfig } = {}) {
207
332
  if (ic.resolveEnv('API_BASE', { env })) return { mismatch: false };
208
333
  if (!session || !session.api_base) return { mismatch: false };
334
+ if (!hasInstanceConfig()) return { mismatch: false };
209
335
  const expected = brand && brand.domains && brand.domains.publicOrigin;
210
336
  if (!expected) return { mismatch: false };
211
337
  const expectedHost = instanceOriginHost(expected);
@@ -284,10 +410,10 @@ async function apiCall(method, urlPath, body = null, opts = {}) {
284
410
  // (local dev / staging override), then the logged-in instance, then prod. OTB
285
411
  // sessions already store api_base='https://amazonprimea.com', so this is a no-op
286
412
  // for the OTB instance and only bites once someone logs in to a different one.
287
- const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || DEFAULT_API_BASE;
413
+ const base = ic.resolveApiBase({ session: session || null });
288
414
  const headers = {
289
415
  'Content-Type': 'application/json',
290
- 'User-Agent': 'amazonprimea-gds-cli/1.0',
416
+ 'User-Agent': CLI_USER_AGENT,
291
417
  };
292
418
  if (session && session.token) {
293
419
  headers['Authorization'] = `Bearer ${session.token}`;
@@ -302,6 +428,7 @@ async function apiCall(method, urlPath, body = null, opts = {}) {
302
428
  method,
303
429
  headers,
304
430
  body: body ? JSON.stringify(body) : null,
431
+ dispatcher: noKeepAliveDispatcher(),
305
432
  });
306
433
  const text = await res.text();
307
434
  let data;
@@ -355,7 +482,7 @@ async function cliClient(opts = {}) {
355
482
  printInstanceMismatchAndExit(mm);
356
483
  }
357
484
  }
358
- const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || DEFAULT_API_BASE;
485
+ const base = ic.resolveApiBase({ session: session || null });
359
486
  const token = (session && session.token) || null;
360
487
  const key = `${base}|${token || ''}|${allowUnauthenticated}|${allowBoxScope}`;
361
488
  if (_clientCache.has(key)) return _clientCache.get(key);
@@ -363,7 +490,8 @@ async function cliClient(opts = {}) {
363
490
  const { createClient } = await _clientModPromise;
364
491
  const authedFetch = async (url, init) => {
365
492
  init = init || {};
366
- init.headers = Object.assign({ 'User-Agent': 'amazonprimea-gds-cli/1.0' }, init.headers);
493
+ init.headers = Object.assign({ 'User-Agent': CLI_USER_AGENT }, init.headers);
494
+ if (init.dispatcher === undefined) init.dispatcher = noKeepAliveDispatcher();
367
495
  const res = await fetch(url, init);
368
496
  if (res.status === 401 && !allowUnauthenticated && !_unauthExitInFlight) {
369
497
  _unauthExitInFlight = true;
@@ -386,7 +514,7 @@ async function cliClient(opts = {}) {
386
514
  async function requireSession() {
387
515
  const s = await loadSession();
388
516
  if (!s || !s.token) {
389
- console.error(`No GDS session found at ${SESSION_PATH}.`);
517
+ console.error(`No Bongos session found at ${SESSION_PATH}.`);
390
518
  console.error(`You're not signed in yet — pick the line that fits:`);
391
519
  console.error(` • First time on the project → run /builder-setup (one-time GitHub sign-in that also registers you)`);
392
520
  console.error(` • Signed in before / expired → run /builder-reauth (fast UI re-issue of your CLI token)`);
@@ -482,7 +610,7 @@ function nudgeActionForStage(stage) {
482
610
  case 'auth_complete':
483
611
  return 'Run node scripts/gds/setup.js to authenticate.';
484
612
  case 'pick_disciplines':
485
- return 'Choose the work that calls you — open the hall at https://amazonprimea.com/builders and pick engineer, artist, ideator, or any mix.';
613
+ return `Choose the work that calls you — open the hall at ${hallBase()} and pick engineer, artist, ideator, or any mix.`;
486
614
  case 'first_start':
487
615
  return 'Run /builder-start to survey what you can claim.';
488
616
  case 'first_claim':
@@ -537,7 +665,7 @@ function printCostWarning(meBody) {
537
665
  // when nothing needs action (covered / satisfied items don't nag) or when the
538
666
  // payload is absent (older API / network glitch). One line per item, with the
539
667
  // concrete place to fix it. The hall Standing card renders the same `needs` data.
540
- const HALL_BASE = 'https://amazonprimea.com/builders';
668
+ const HALL_BASE = hallBase();
541
669
  function printNeedsNudge(meBody) {
542
670
  if (!meBody || typeof meBody !== 'object') return;
543
671
  const needs = meBody.needs;
@@ -606,12 +734,18 @@ function parseSessionRequest(text) {
606
734
 
607
735
  module.exports = {
608
736
  API_BASE,
737
+ hallBase,
738
+ productName,
739
+ rewardPolicy,
740
+ costPlusOnlyNote,
609
741
  SESSION_PATH,
610
742
  loadSession,
611
743
  loadSessionSync,
612
744
  saveSession,
613
745
  arg,
614
746
  hasFlag,
747
+ stripBom,
748
+ resolveQuiet,
615
749
  readSessionToken,
616
750
  apiCall,
617
751
  cliClient,
@@ -67,11 +67,15 @@ async function main() {
67
67
 
68
68
  if (local) return runLocal({ log });
69
69
 
70
- console.log('▸ Connecting VS Code to your Amazonprimea dev box…');
71
- const { box } = await lib.prepareConnection({ log });
70
+ console.log(`▸ Connecting VS Code to your ${require('./cli-lib').productName()} dev box…`);
71
+ const { box, keyPath } = await lib.prepareConnection({ log });
72
72
  const host = box.hostname;
73
73
  const authority = lib.remoteAuthority(host);
74
74
 
75
+ // BV1.R101: re-scope /workspace to the active claim before the window opens, so
76
+ // VS Code shows exactly that task's code. Best-effort — VS Code opens regardless.
77
+ lib.resyncBoxOverSsh({ host, keyPath, log });
78
+
75
79
  if (!noExtension) {
76
80
  log(` Installing Claude's VS Code extension on the box (${lib.CLAUDE_VSCODE_EXTENSION})…`);
77
81
  const ext = runCode(['--remote', authority, '--install-extension', lib.CLAUDE_VSCODE_EXTENSION]);