@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
@@ -95,15 +95,17 @@ await test('approve: tasks.shipped_by is attributed to the REQUESTER, not the Ar
95
95
  'must not re-SELECT the requester row already locked by the UPDATE');
96
96
  });
97
97
 
98
- await test('approve: the task.confirmed credit_log row is for the REQUESTER', async () => {
98
+ await test('approve: ADR 0120 pay-on-land — NO credit at override-approval; the reward waits for the land', async () => {
99
99
  const pool = fakePool({ requesterId: REQUESTER, decision: 'approve', task: TASK });
100
- await decideOverrideRequest({ requestId: 1, archonId: ARCHON, decision: 'approve' }, { pool });
101
-
102
- const credit = find(pool._client.queries, /INSERT INTO credit_log/);
103
- assert.ok(credit, 'a credit_log INSERT must run');
104
- assert.equal(credit.params[0], REQUESTER, 'credit_log.builder_id must be the requester');
105
- assert.equal(credit.params[1], TASK.id, 'credit_log.task_id must be the task');
106
- assert.ok(credit.params[2] > 0, 'credit delta must be positive');
100
+ const out = await decideOverrideRequest({ requestId: 1, archonId: ARCHON, decision: 'approve' }, { pool });
101
+
102
+ // The override-approval flips completed→confirmed and attributes shipped_by to
103
+ // the requester (asserted above), but the per-task credit now lands at the
104
+ // confirmed→shipped flip (shipTask → awardLandingCredit), NOT here — so a
105
+ // stranded override-confirmed task pays nothing until its code is on `main`.
106
+ assert.equal(out.confirmResult.creditsAwarded, 0, 'no per-task credit lands at override-approval');
107
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0,
108
+ 'override-approval must run ZERO credit_log INSERTs — the reward waits for the land');
107
109
  });
108
110
 
109
111
  await test('approve: transaction commits (not rolls back)', async () => {
@@ -183,10 +183,14 @@ await test('approve: credit + tasks.shipped_by go to the CLAIM HOLDER, not the r
183
183
  assert.deepEqual(taskUpdate.params, [TASK.id, WORKER],
184
184
  `shipped_by must bind to the claim holder ${WORKER}, got ${JSON.stringify(taskUpdate.params)}`);
185
185
 
186
- const credit = find(pool._client.queries, /INSERT INTO credit_log/);
187
- assert.ok(credit, 'a credit_log INSERT must run');
188
- assert.equal(credit.params[0], WORKER, 'credit_log.builder_id must be the claim holder');
189
- assert.ok(credit.params[2] > 0, 'credit delta must be positive');
186
+ // ADR 0120 pay-on-land: the per-task credit no longer lands at
187
+ // override-approval — it lands at the confirmed→shipped flip (shipTask). The
188
+ // credit-theft guard this test exists for is still intact: creditedBuilderId +
189
+ // shipped_by are the CLAIM HOLDER (asserted above), so when the credit DOES
190
+ // land at ship it lands on the worker, never the requester/Archon.
191
+ assert.equal(out.confirmResult.creditsAwarded, 0, 'no per-task credit lands at override-approval');
192
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0,
193
+ 'override-approval must run ZERO credit_log INSERTs — the reward waits for the land');
190
194
  });
191
195
 
192
196
  await test('approve: the claim-holder lookup happens BEFORE the tasks UPDATE binds shipped_by', async () => {
@@ -0,0 +1,42 @@
1
+ // tests/parse_cookie_decode.mjs — regression for the post-login redirect landing
2
+ // on /gate/ instead of the requested page (task 1002241).
3
+ //
4
+ // The onboarding wizard's "Connect GitHub" button hits /auth/web/start?return=/create,
5
+ // which stores the return target via `res.cookie(RETURN_COOKIE, '/create', …)`. Express
6
+ // `res.cookie` percent-ENCODES the value, so the wire cookie is `gds_return=%2Fcreate`.
7
+ // parseCookie() must DECODE symmetrically on read — else the callback runs
8
+ // safeReturnTo('%2Fcreate'), which (not starting with '/') is parsed as a URL, throws,
9
+ // returns null, and the post-login redirect silently falls through to the default
10
+ // landing (cloudbongos.com → /gate/). Decoding is a no-op for hex/simple values, so it
11
+ // can't regress the state/session/repos cookies.
12
+ import { createRequire } from 'node:module';
13
+ const require = createRequire(import.meta.url);
14
+ const { parseCookie } = require('../src/bongos/routes/_helpers.js');
15
+
16
+ let pass = 0, fail = 0;
17
+ const ok = (cond, msg) => { if (cond) { pass++; console.log(' ok ', msg); } else { fail++; console.log(' FAIL', msg); } };
18
+ const req = (cookie) => ({ headers: { cookie } });
19
+
20
+ console.log('parseCookie — decodes percent-encoded values (task 1002241):');
21
+ ok(parseCookie(req('gds_return=%2Fcreate'), 'gds_return') === '/create',
22
+ '"%2Fcreate" → "/create" (the Connect-GitHub return bug)');
23
+ ok(parseCookie(req('gds_return=https%3A%2F%2Fbuilders.cloudbongos.com%2Fx'), 'gds_return')
24
+ === 'https://builders.cloudbongos.com/x',
25
+ 'encoded full https URL round-trips (the #726 subdomain return, same latent bug)');
26
+
27
+ console.log('\nparseCookie — no-op / safe for the other cookies:');
28
+ ok(parseCookie(req('gds_oauth_state=03e92dd24cad3219c637e50fc9f115ef'), 'gds_oauth_state')
29
+ === '03e92dd24cad3219c637e50fc9f115ef', 'hex state value unchanged');
30
+ ok(parseCookie(req('gds_session=' + 'a'.repeat(64)), 'gds_session') === 'a'.repeat(64),
31
+ 'hex session token unchanged');
32
+ ok(parseCookie(req('gds_oauth_repos=1'), 'gds_oauth_repos') === '1', 'repos "1" unchanged');
33
+
34
+ console.log('\nparseCookie — misc:');
35
+ ok(parseCookie(req(''), 'gds_return') === null, 'no cookies → null');
36
+ ok(parseCookie(req('other=x; gds_return=%2Fcreate'), 'gds_return') === '/create',
37
+ 'finds the named cookie among several');
38
+ ok(parseCookie(req('gds_return=%E0%A4%A'), 'gds_return') === '%E0%A4%A',
39
+ 'malformed percent-escape falls back to the raw value (no throw)');
40
+
41
+ console.log(`\nparseCookie decode tests: ${pass} passed, ${fail} failed`);
42
+ if (fail) process.exit(1);
@@ -0,0 +1,129 @@
1
+ // tests/pay_on_land.mjs
2
+ //
3
+ // ADR 0120 part 1 (pay-on-land) — the per-task credit + idea-promotion bonus
4
+ // are booked at the confirmed→SHIPPED flip (db.shipTask → awardLandingCredit),
5
+ // NOT at confirm. This is the mirror of confirm_attribution.mjs / override_
6
+ // attribution.mjs / grade_attribution.mjs, which now assert NO credit at their
7
+ // (confirm-time) transition; here we assert the credit lands at ship.
8
+ //
9
+ // The contract this pins:
10
+ // • ship awards a positive per-task credit, reason='task.shipped'
11
+ // • the credit is attributed to the CLAIM HOLDER (the builder who did the
12
+ // work), even when a DIFFERENT builder (the reconciler / a supervisor)
13
+ // triggers the ship
14
+ // • idempotent on (task_id, 'task.shipped'): a re-ship / reconciler re-sweep
15
+ // books nothing a second time
16
+ // • credits_reward = 0 → no credit_log INSERT
17
+ // • a task not in 'confirmed' state throws TASK_NOT_CONFIRMED, awards nothing
18
+ //
19
+ // shipTask honors the #538 `deps.pool` seam under NODE_ENV==='test', so we drive
20
+ // it with a SQL-aware fakeClient — no live DB.
21
+ //
22
+ // Run: node tests/pay_on_land.mjs
23
+
24
+ import { strict as assert } from 'node:assert';
25
+ import { createRequire } from 'node:module';
26
+ import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
27
+
28
+ process.env.NODE_ENV = 'test';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ // shipTask resolves the `reward` port for the credit grant + achievement
32
+ // reconcile. Register it; stub the gamification hooks to no-ops so the test
33
+ // isolates the credit path (the real credits.js SQL still runs against the fake).
34
+ const achievements = require('../modules/economy/achievements.js');
35
+ achievements.evaluateAfterShip = async () => [];
36
+ achievements.evaluateAfterCreditAward = async () => [];
37
+ achievements.reconcileBuilderAchievements = async () => [];
38
+ const seams = require('../src/module-seams.js');
39
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
40
+ const db = require('../modules/lifecycle/db.js');
41
+ const { shipTask } = db;
42
+
43
+ const { test, summary } = makeRunner();
44
+
45
+ // SQL-aware fake covering shipTask's transaction. Knobs:
46
+ // claimHolderId → who mostRecentClaimHolderId resolves (null = none)
47
+ // shippedBy → tasks.shipped_by on the returned shipped row
48
+ // creditsReward, kind → the task's reward inputs
49
+ // confirmed=false → the UPDATE tasks matches nothing (TASK_NOT_CONFIRMED)
50
+ // priorShipCredit → a 'task.shipped' credit_log row already exists (idempotent)
51
+ function fakePool({ claimHolderId, shippedBy, creditsReward = 15, kind = 'feature', confirmed = true, priorShipCredit = false }) {
52
+ return makeSqlAwareClient((s) => {
53
+ if (/^UPDATE tasks/.test(s)) {
54
+ return {
55
+ rows: confirmed
56
+ ? [{ id: TASK.id, title: TASK.title, status: 'shipped', shipped_at: new Date(0).toISOString(),
57
+ shipped_by: shippedBy, security_sensitive: false, value_summary: null,
58
+ credits_reward: creditsReward, kind }]
59
+ : [],
60
+ };
61
+ }
62
+ if (/^UPDATE claims/.test(s)) return { rows: [] }; // no open claims to heal
63
+ if (/UPDATE builders SET first_ship_at/.test(s)) return { rows: [] };
64
+ if (/SELECT rank FROM builders/.test(s)) return { rows: [{ rank: 'thetes' }] }; // not xenos → no graduation
65
+ if (/SELECT builder_id FROM claims/.test(s)) {
66
+ return { rows: claimHolderId == null ? [] : [{ builder_id: claimHolderId }] };
67
+ }
68
+ if (/SELECT 1 FROM credit_log .* 'task\.shipped'/.test(s)) {
69
+ return { rows: priorShipCredit ? [{ '?column?': 1 }] : [] };
70
+ }
71
+ if (/SELECT 1 FROM credit_log .* 'idea_promotion_bonus'/.test(s)) return { rows: [] };
72
+ if (/FROM idea_inbox/.test(s)) return { rows: [] }; // no idea lineage
73
+ if (/INSERT INTO credit_log/.test(s)) return { rows: [] };
74
+ return { rows: [] }; // BEGIN/COMMIT/SAVEPOINT/etc.
75
+ });
76
+ }
77
+
78
+ const find = (queries, re) => queries.find((q) => re.test(String(q.sql).replace(/\s+/g, ' ')));
79
+ const all = (queries, re) => queries.filter((q) => re.test(String(q.sql).replace(/\s+/g, ' ')));
80
+
81
+ const RECONCILER = 999; // the builder_id the reconciler passes to shipTask
82
+ const WORKER = 25; // the claim holder who actually did the work
83
+ const TASK = { id: 660, title: 'fixture task' };
84
+
85
+ // =========================================================================
86
+ await test('ship: the per-task credit lands at SHIP (reason=task.shipped), attributed to the claim holder', async () => {
87
+ // shipped_by was set to the claim holder at confirm; the reconciler triggers
88
+ // the ship with its own builderId — attribution must still be the worker.
89
+ const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, creditsReward: 15, kind: 'feature' });
90
+ const out = await shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool });
91
+
92
+ assert.equal(out.status, 'shipped', 'task flips to shipped');
93
+ assert.equal(out.creditedBuilderId, WORKER, 'credit attributed to the claim holder, not the reconciler');
94
+ assert.ok(out.creditsAwarded > 0, 'a positive per-task credit must be awarded at ship');
95
+
96
+ const insert = find(pool._client.queries, /INSERT INTO credit_log/);
97
+ assert.ok(insert, 'a credit_log INSERT must run at ship');
98
+ assert.equal(insert.params[0], WORKER, 'credit_log.builder_id = claim holder');
99
+ assert.notEqual(insert.params[0], RECONCILER, 'credit must NOT go to the ship trigger');
100
+ assert.equal(insert.params[1], TASK.id, 'credit_log.task_id = the task');
101
+ assert.equal(insert.params[3], 'task.shipped', "reason must be 'task.shipped' (ADR 0120), not 'task.confirmed'");
102
+ });
103
+
104
+ await test('ship: idempotent — a prior task.shipped credit means no second INSERT', async () => {
105
+ const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, priorShipCredit: true });
106
+ const out = await shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool });
107
+ assert.equal(out.creditsAwarded, 0, 'no fresh credit on a re-ship / reconciler re-sweep');
108
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'must not double-insert credit');
109
+ });
110
+
111
+ await test('ship: credits_reward = 0 → no credit_log INSERT', async () => {
112
+ const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, creditsReward: 0 });
113
+ const out = await shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool });
114
+ assert.equal(out.creditsAwarded, 0, 'a zero-reward task pays nothing');
115
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'no INSERT for a 0-reward task');
116
+ });
117
+
118
+ await test('ship: not in confirmed state → throws TASK_NOT_CONFIRMED, awards nothing', async () => {
119
+ const pool = fakePool({ claimHolderId: WORKER, shippedBy: WORKER, confirmed: false });
120
+ await assert.rejects(
121
+ () => shipTask({ taskId: TASK.id, builderId: RECONCILER }, { pool }),
122
+ (err) => err.code === 'TASK_NOT_CONFIRMED',
123
+ 'must throw TASK_NOT_CONFIRMED when the UPDATE matches no confirmed row'
124
+ );
125
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'must not award credit on a failed ship');
126
+ });
127
+
128
+ // -------------------------------------------------------------------------
129
+ summary();
@@ -10,9 +10,16 @@
10
10
  import assert from 'node:assert/strict';
11
11
  import { test, before, after } from 'node:test';
12
12
  import http from 'node:http';
13
+ import net from 'node:net';
14
+ import path from 'node:path';
15
+ import { spawn } from 'node:child_process';
16
+ import { fileURLToPath } from 'node:url';
13
17
  import platformMod from '../src/platform-server.js';
18
+ import loader from '../src/module-loader/loader.js';
14
19
 
15
- const { buildPlatformApp } = platformMod;
20
+ const { buildPlatformApp, startBackgroundServices } = platformMod;
21
+
22
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
16
23
 
17
24
  let server;
18
25
  let port;
@@ -168,3 +175,95 @@ test('R60: the served hall injects window.__MODULES__ (feature-module flags) syn
168
175
  assert.equal(typeof parsed.enabled['dev-box'], 'boolean');
169
176
  assert.equal(typeof parsed.enabled.discord, 'boolean');
170
177
  });
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // task 1002419 — the platform entrypoint must actually START the module pollers.
181
+ //
182
+ // Regression fixture for a silent, high-blast-radius outage: startModulePollers
183
+ // had ZERO callers after the game entrypoint (server.js) was retired, so on a
184
+ // standalone instance EVERY module poller was dead — most visibly the lifecycle
185
+ // publish-reconciler, which is what drives 'confirmed' tasks to 'shipped'. Eleven
186
+ // tasks stranded at 'confirmed' (oldest 6.2 days) with nothing in any log to say
187
+ // why: a poller that is never started is indistinguishable from one with nothing
188
+ // to do. Both tests below are DB-free.
189
+ // ---------------------------------------------------------------------------
190
+
191
+ test('task 1002419: startBackgroundServices starts the enabled modules’ pollers', async () => {
192
+ // The wiring resolves a real, ENABLED module poller. lifecycle is default:true
193
+ // and declares contributes.pollers:["publish-reconciler"], so it must appear.
194
+ loader._reset();
195
+ let keys = [];
196
+ try {
197
+ const started = startBackgroundServices();
198
+ keys = started.map((p) => `${p.key}/${p.poller}`);
199
+ assert.ok(
200
+ keys.includes('lifecycle/publish-reconciler'),
201
+ `the lifecycle publish-reconciler poller must start (got: ${keys.join(', ') || 'none'})`
202
+ );
203
+ } finally {
204
+ // Clear the real (unref'd) timers this just started so the suite leaves none.
205
+ // The stopped count is also the loader's own record of what it started, so it
206
+ // must match what startBackgroundServices reported.
207
+ const stopped = await loader.stopModulePollers();
208
+ assert.equal(stopped, keys.length, 'stopModulePollers stops exactly what was started');
209
+ loader._reset();
210
+ }
211
+ });
212
+
213
+ function freePort() {
214
+ return new Promise((resolve, reject) => {
215
+ const s = net.createServer();
216
+ s.on('error', reject);
217
+ s.listen(0, '127.0.0.1', () => {
218
+ const { port } = s.address();
219
+ s.close(() => resolve(port));
220
+ });
221
+ });
222
+ }
223
+
224
+ test('task 1002419: a real `node src/platform-server.js` boot announces the reconciler', async () => {
225
+ // The mutation-proof half. The test above would still pass if the entry block
226
+ // stopped calling startBackgroundServices(); this one spawns the entrypoint the
227
+ // way docker-entrypoint.sh does and asserts the poller announces itself in the
228
+ // boot log — so deleting the call site turns it red.
229
+ const port = await freePort();
230
+ const child = spawn(process.execPath, [path.join(REPO_ROOT, 'src', 'platform-server.js')], {
231
+ cwd: REPO_ROOT,
232
+ env: {
233
+ ...process.env,
234
+ PORT: String(port),
235
+ HOST: '127.0.0.1',
236
+ // Push every first-fire far past the life of this test: we assert the
237
+ // START announcement, never a sweep, so no poller ever touches Postgres.
238
+ PUBLISH_RECONCILE_FIRST_MS: '3600000',
239
+ ROUTINE_PEER_VOTE_FIRST_MS: '3600000',
240
+ },
241
+ });
242
+
243
+ try {
244
+ const log = await new Promise((resolve, reject) => {
245
+ let out = '';
246
+ const timer = setTimeout(
247
+ () => reject(new Error(`platform-server did not announce the reconciler within 20s. Log:\n${out}`)),
248
+ 20_000
249
+ );
250
+ const onData = (d) => {
251
+ out += d;
252
+ if (/\[publish-reconciler\] started/.test(out)) {
253
+ clearTimeout(timer);
254
+ resolve(out);
255
+ }
256
+ };
257
+ child.stdout.on('data', onData);
258
+ child.stderr.on('data', onData);
259
+ child.on('error', (e) => { clearTimeout(timer); reject(e); });
260
+ child.on('exit', (code) => {
261
+ clearTimeout(timer);
262
+ reject(new Error(`platform-server exited early (code ${code}). Log:\n${out}`));
263
+ });
264
+ });
265
+ assert.match(log, /\[publish-reconciler\] started — sweeping confirmed→shipped every \d+s/);
266
+ } finally {
267
+ child.kill('SIGKILL');
268
+ }
269
+ });
@@ -0,0 +1,42 @@
1
+ // tests/post_login_landing.mjs — the configurable post-sign-in landing (task 1002240).
2
+ //
3
+ // postLoginLanding(brand) resolves the redirect target used after a successful
4
+ // web sign-in when NO explicit ?return= / same-apex Referer was carried. It is
5
+ // the ONE seam that lets a platform-home instance (cloudbongos.com) land a
6
+ // visitor back on its orbs (/gate/) instead of the builders' hall, while every
7
+ // other instance keeps the portable '/builders' default (the hall always
8
+ // exists; a /gate/ orbs site does not). Pure over an injected brand pack, and
9
+ // hardened through the SAME open-redirect allowlist as ?return= (safeReturnTo),
10
+ // so a misconfigured landing can never become an open redirect.
11
+ //
12
+ // Pure function, no DB — requiring the route module does not open a pool.
13
+
14
+ import { createRequire } from 'node:module';
15
+ const require = createRequire(import.meta.url);
16
+ const { postLoginLanding } = require('../src/bongos/routes/auth.js');
17
+
18
+ let pass = 0, fail = 0;
19
+ function ok(cond, msg) {
20
+ if (cond) { pass++; console.log(` ok ${msg}`); }
21
+ else { fail++; console.log(` FAIL ${msg}`); }
22
+ }
23
+
24
+ console.log('\npostLoginLanding — portable default is the hall:');
25
+ ok(postLoginLanding({}) === '/builders', 'no landing config → /builders');
26
+ ok(postLoginLanding({ landing: {} }) === '/builders', 'empty landing block → /builders');
27
+ // No-arg path reads the real committed neutral pack, which ships postLogin:'/builders'.
28
+ ok(postLoginLanding() === '/builders', 'committed neutral pack → /builders');
29
+
30
+ console.log('\npostLoginLanding — an instance lands visitors on its own front door:');
31
+ ok(postLoginLanding({ landing: { postLogin: '/gate/' } }) === '/gate/', 'cloudbongos orbs → /gate/');
32
+ ok(postLoginLanding({ landing: { postLogin: '/gate/?entered=1' } }) === '/gate/?entered=1', 'relative path with query preserved');
33
+
34
+ console.log('\npostLoginLanding — a misconfigured landing can never become an open redirect:');
35
+ ok(postLoginLanding({ landing: { postLogin: 'https://evil.com/' } }) === '/builders', 'foreign https origin → fallback');
36
+ ok(postLoginLanding({ landing: { postLogin: '//evil.com' } }) === '/builders', 'protocol-relative → fallback');
37
+ ok(postLoginLanding({ landing: { postLogin: 'javascript:alert(1)' } }) === '/builders', 'javascript: scheme → fallback');
38
+ ok(postLoginLanding({ landing: { postLogin: '/a\\b' } }) === '/builders', 'backslash path → fallback');
39
+ ok(postLoginLanding({ landing: { postLogin: 42 } }) === '/builders', 'non-string value → fallback');
40
+
41
+ console.log(`\nPost-login-landing tests: ${pass} passed, ${fail} failed`);
42
+ if (fail) process.exit(1);
@@ -0,0 +1,72 @@
1
+ // tests/preflight_checks.mjs
2
+ //
3
+ // Unit test for scripts/gds/preflight.js `preflightChecks()` (task 1002401,
4
+ // onboarding-audit area 4) — the pure onboarding environment preflight shared by
5
+ // /builder-setup and `bongos doctor`. DB-free and does zero real I/O: node
6
+ // version, fs, and child_process are all injected, so we assert the real
7
+ // decision logic (levels + fixes) without touching the machine.
8
+
9
+ import { test } from 'node:test';
10
+ import assert from 'node:assert/strict';
11
+ import { createRequire } from 'node:module';
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const { preflightChecks, formatPreflight } = require('../scripts/gds/preflight.js');
15
+
16
+ const okSpawn = () => ({ status: 0, stdout: '1.2.3 (Claude Code)\n' });
17
+ const badSpawn = () => ({ error: Object.assign(new Error('spawn claude ENOENT'), { code: 'ENOENT' }) });
18
+ const fsWith = (present) => ({ existsSync: () => present });
19
+ const find = (results, title) => results.find((r) => r.title === title);
20
+
21
+ test('all green: node>=22, deps present, claude launches', () => {
22
+ const r = preflightChecks({ nodeVersion: 'v22.5.0', fs: fsWith(true), spawn: okSpawn, env: {} });
23
+ assert.equal(find(r, 'Node').level, 'ok');
24
+ assert.equal(find(r, 'Dependencies').level, 'ok');
25
+ assert.equal(find(r, 'claude CLI').level, 'ok');
26
+ });
27
+
28
+ test('node below v22 → fail with an upgrade fix', () => {
29
+ const r = preflightChecks({ nodeVersion: 'v18.19.0', fs: fsWith(true), spawn: okSpawn, env: {} });
30
+ const node = find(r, 'Node');
31
+ assert.equal(node.level, 'fail');
32
+ assert.match(node.fix, /22|nvm|nodejs/);
33
+ });
34
+
35
+ test('missing node_modules → warn (non-fatal) with `npm ci` fix', () => {
36
+ const r = preflightChecks({ nodeVersion: 'v22.0.0', fs: fsWith(false), spawn: okSpawn, env: {} });
37
+ const deps = find(r, 'Dependencies');
38
+ assert.equal(deps.level, 'warn');
39
+ assert.match(deps.fix, /npm ci/);
40
+ });
41
+
42
+ test('claude not launchable → fail naming SUBAGENT_SPAWN_FAILED + install fix', () => {
43
+ const r = preflightChecks({ nodeVersion: 'v22.0.0', fs: fsWith(true), spawn: badSpawn, env: {} });
44
+ const claude = find(r, 'claude CLI');
45
+ assert.equal(claude.level, 'fail');
46
+ assert.match(claude.detail, /SUBAGENT_SPAWN_FAILED/);
47
+ assert.match(claude.fix, /@anthropic-ai\/claude-code|OTB_CLAUDE_BIN/);
48
+ });
49
+
50
+ test('OTB_CLAUDE_BIN is used as the probe binary and labelled', () => {
51
+ let seenBin = null;
52
+ const spy = (bin) => { seenBin = bin; return { status: 0, stdout: 'x\n' }; };
53
+ const r = preflightChecks({ nodeVersion: 'v22.0.0', fs: fsWith(true), spawn: spy, env: { OTB_CLAUDE_BIN: '/opt/claude' } });
54
+ assert.equal(seenBin, '/opt/claude');
55
+ assert.match(find(r, 'claude CLI').detail, /via OTB_CLAUDE_BIN/);
56
+ });
57
+
58
+ test('a thrown existsSync is treated as deps-missing, never crashes', () => {
59
+ const fsThrows = { existsSync: () => { throw new Error('boom'); } };
60
+ const r = preflightChecks({ nodeVersion: 'v22.0.0', fs: fsThrows, spawn: okSpawn, env: {} });
61
+ assert.equal(find(r, 'Dependencies').level, 'warn');
62
+ });
63
+
64
+ test('formatPreflight renders glyphs + fixes and stays plain (no ANSI) by default', () => {
65
+ const r = preflightChecks({ nodeVersion: 'v18.0.0', fs: fsWith(false), spawn: badSpawn, env: {} });
66
+ const s = formatPreflight(r, { heading: true });
67
+ assert.match(s, /Checking your environment/);
68
+ assert.match(s, /✖ Node/);
69
+ assert.match(s, /! Dependencies/);
70
+ assert.match(s, /→ .*npm ci/);
71
+ assert.doesNotMatch(s, /\x1b\[/); // color:false ⇒ no ANSI escapes
72
+ });
@@ -282,6 +282,10 @@ function recorder({ apply = false } = {}) {
282
282
  // Stub the DNS token-scope preflight (task 1980) so the composition stays hermetic
283
283
  // (no real CF call). Default: reachable; a test overrides it to exercise the block.
284
284
  checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
285
+ // Owner OAuth token lookup for the scaffold push (task 1002317). Default: none →
286
+ // the scaffold takes the SSH `origin` fallback (so existing push assertions hold
287
+ // without touching the real db module). A test overrides it to exercise the token path.
288
+ getGithubToken: async () => null,
285
289
  };
286
290
  return { deps, cmds, writes, statuses };
287
291
  }
@@ -337,8 +341,9 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
337
341
  const r = await P.provisionInstance({ ...coTenant, hosting_shape: 'standalone', target_ref: 'lars589/mercury' }, deps);
338
342
  assert.equal(r.ok, true);
339
343
  const joined = cmds.join('\n');
340
- // scaffold leg (task 2055): clone → package-core → init --vendor-core → push
341
- assert.match(joined, /git clone git@github-deploy:lars589\/mercury\.git/, 'clones the instance repo');
344
+ // scaffold leg (task 2055): git init → package-core → init --vendor-core → push
345
+ assert.match(joined, /git -C \S+ init -q/, 'inits the instance working tree (empty repo — nothing to clone)');
346
+ assert.match(joined, /git -C \S+ remote add origin git@github-deploy:lars589\/mercury\.git/, 'sets the instance repo as origin');
342
347
  assert.match(joined, /node scripts\/gds\/package-core\.js/, 'builds the core tarball');
343
348
  assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --vendor-core /, 'materializes + file:-pins the core');
344
349
  assert.match(joined, /git -C .* push -u origin main/, 'pushes the scaffold');
@@ -349,9 +354,9 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
349
354
  assert.match(joined, /node_modules\/@bongos\/core\/scripts\/migrate\.sh/, 'migrates from the INSTANCE root\'s core');
350
355
  assert.ok(!cmds.some((c) => /\.\/scripts\/migrate\.sh/.test(c)), 'standalone does NOT use the control-plane checkout migrate');
351
356
  assert.match(joined, /INSERT INTO versions/, 'standalone seeds its first version too (task 2119)');
352
- // ordering: clone → package-core → init → push → pull → npm ci → migrate → service
357
+ // ordering: git init → package-core → init → push → pull → npm ci → migrate → service
353
358
  const idx = (re) => cmds.findIndex((c) => re.test(c));
354
- assert.ok(idx(/git clone/) < idx(/package-core/), 'clone before tarball build');
359
+ assert.ok(idx(/git -C \S+ init -q/) < idx(/package-core/), 'git init before tarball build');
355
360
  assert.ok(idx(/package-core/) < idx(/init\.js/), 'tarball before init');
356
361
  assert.ok(idx(/init\.js/) < idx(/push -u origin main/), 'init before push');
357
362
  assert.ok(idx(/push -u origin main/) < idx(/git pull/), 'scaffold push before the deploy pull');
@@ -415,7 +420,7 @@ await ta('scaffoldStandaloneRepo: idempotent — a repo that already carries pac
415
420
  const { deps, cmds } = recorder({ apply: true });
416
421
  const res = await P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' }, deps);
417
422
  assert.equal(res.already, true);
418
- assert.equal(cmds.length, 0, 'no clone/build/push when already scaffolded');
423
+ assert.equal(cmds.length, 0, 'no init/build/push when already scaffolded');
419
424
  } finally {
420
425
  P.CONFIG.standaloneBase = origBase;
421
426
  rmSync(tmp, { recursive: true, force: true });
@@ -431,9 +436,13 @@ async function scaffoldWithEnv(env) {
431
436
  P.CONFIG.standaloneBase = tmp;
432
437
  try {
433
438
  const { deps, cmds } = recorder({ apply: true });
439
+ // instanceRoot = the fresh base (no package.json → no `file:` core pin), so these
440
+ // vendored-path assertions stay deterministic: installedCoreTarball returns null and
441
+ // the scaffold rebuilds via package-core.js (task 1002275). A consumer-instance root
442
+ // (a real pin) takes the reuse path — covered by its own test below.
434
443
  await P.scaffoldStandaloneRepo(
435
444
  { slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' },
436
- { ...deps, env },
445
+ { ...deps, env, instanceRoot: tmp },
437
446
  );
438
447
  return cmds.join('\n');
439
448
  } finally {
@@ -463,6 +472,122 @@ await ta('scaffoldStandaloneRepo: default (no registry env) stays on the vendore
463
472
  assert.ok(!/--registry-core/.test(joined));
464
473
  });
465
474
 
475
+ // ── consumer control-plane core reuse (the empty-repo fix, task 1002275) ──────────
476
+ // When the runner is ITSELF a consumer instance (its @bongos/core is a vendored npm dep,
477
+ // not a source checkout), package-core.js can't rebuild the core from git (REPO_ROOT is
478
+ // inside gitignored node_modules → an EMPTY core set → abort → no push → EMPTY repo, seen
479
+ // live on cloudbongos.com for trial/test). Reuse the pinned tarball the instance runs.
480
+ t('installedCoreTarball: reuses a consumer instance file: core pin; null on a source checkout', () => {
481
+ // consumer instance: package.json file:-pins the core AND the tarball is on disk → reuse it
482
+ const inst = mkdtempSync(join(tmpdir(), 'cptgz-consumer-'));
483
+ mkdirSync(join(inst, 'vendor'));
484
+ writeFileSync(join(inst, 'vendor', 'bongos-core-1.17.4.tgz'), 'x');
485
+ writeFileSync(join(inst, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': 'file:vendor/bongos-core-1.17.4.tgz' } }));
486
+ assert.equal(P.installedCoreTarball({ instanceRoot: inst }), join(inst, 'vendor', 'bongos-core-1.17.4.tgz'), 'reuses the pinned tarball');
487
+
488
+ // dangling pin (tarball pruned) → null, so the caller never points init at a missing file
489
+ rmSync(join(inst, 'vendor', 'bongos-core-1.17.4.tgz'));
490
+ assert.equal(P.installedCoreTarball({ instanceRoot: inst }), null, 'a dangling file: pin resolves to null');
491
+
492
+ // source checkout: a caret range (no file: pin) → null → the caller rebuilds via package-core.js
493
+ const src = mkdtempSync(join(tmpdir(), 'cptgz-source-'));
494
+ writeFileSync(join(src, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '^1.17.4' } }));
495
+ assert.equal(P.installedCoreTarball({ instanceRoot: src }), null, 'a source checkout has no file: pin');
496
+
497
+ // no package.json at all → null
498
+ const bare = mkdtempSync(join(tmpdir(), 'cptgz-bare-'));
499
+ assert.equal(P.installedCoreTarball({ instanceRoot: bare }), null, 'no package.json → null');
500
+
501
+ // the @cloudbongos/core scope alias is honored too (infra/core-node.sh accepts both)
502
+ const alt = mkdtempSync(join(tmpdir(), 'cptgz-alt-'));
503
+ mkdirSync(join(alt, 'vendor'));
504
+ writeFileSync(join(alt, 'vendor', 'bongos-core-2.0.0.tgz'), 'x');
505
+ writeFileSync(join(alt, 'package.json'), JSON.stringify({ dependencies: { '@cloudbongos/core': 'file:vendor/bongos-core-2.0.0.tgz' } }));
506
+ assert.equal(P.installedCoreTarball({ instanceRoot: alt }), join(alt, 'vendor', 'bongos-core-2.0.0.tgz'), 'honors the @cloudbongos/core scope alias');
507
+
508
+ [inst, src, bare, alt].forEach((d) => rmSync(d, { recursive: true, force: true }));
509
+ });
510
+
511
+ await ta('scaffoldStandaloneRepo: on a consumer control plane it REUSES the pinned tarball — no package-core rebuild (task 1002275)', async () => {
512
+ // the control plane is itself a consumer instance: its package.json file:-pins the core
513
+ const cp = mkdtempSync(join(tmpdir(), 'cptgz-cp-'));
514
+ mkdirSync(join(cp, 'vendor'));
515
+ const pinned = join(cp, 'vendor', 'bongos-core-1.17.4.tgz');
516
+ writeFileSync(pinned, 'x');
517
+ writeFileSync(join(cp, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': 'file:vendor/bongos-core-1.17.4.tgz' } }));
518
+ const base = mkdtempSync(join(tmpdir(), 'cptgz-standalone-')); // FRESH base → the scaffold proceeds
519
+ const origBase = P.CONFIG.standaloneBase;
520
+ P.CONFIG.standaloneBase = base;
521
+ try {
522
+ const { deps, cmds } = recorder({ apply: true });
523
+ await P.scaffoldStandaloneRepo(
524
+ { slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' },
525
+ { ...deps, instanceRoot: cp },
526
+ );
527
+ const joined = cmds.join('\n');
528
+ assert.ok(!/package-core\.js/.test(joined), 'does NOT rebuild the core from source (the empty-set abort path)');
529
+ assert.match(joined, new RegExp('init\\.js --from .* --no-seed --force --vendor-core ' + reEsc(pinned)), 'feeds the PINNED tarball to init --vendor-core');
530
+ assert.match(joined, /git -C \S+ remote add origin git@github-deploy:lars589\/demo\.git/, 'still targets the repo as origin');
531
+ assert.match(joined, /push /, 'still pushes the scaffold');
532
+ } finally {
533
+ P.CONFIG.standaloneBase = origBase;
534
+ [cp, base].forEach((d) => rmSync(d, { recursive: true, force: true }));
535
+ }
536
+ });
537
+
538
+ // ── scaffold push via the owner OAuth token (task 1002317) ────────────────────────
539
+ // Deploy keys are per-repo; a shared key can't write to a NEW project repo. Push with the
540
+ // owner's token (which created the repo) instead — WITHOUT leaking it into a logged command.
541
+ await ta('scaffoldStandaloneRepo: pushes via the owner OAuth token (not SSH), token never in a logged command (task 1002317)', async () => {
542
+ const base = mkdtempSync(join(tmpdir(), 'push-tok-'));
543
+ const origBase = P.CONFIG.standaloneBase;
544
+ P.CONFIG.standaloneBase = base;
545
+ try {
546
+ const { deps, cmds } = recorder({ apply: true });
547
+ const writes = [];
548
+ deps.writeFile = (p, body, opts) => { writes.push({ p, body, opts }); return { ok: true }; };
549
+ deps.getGithubToken = async () => ({ token: 'gho_SECRET_TOKEN_XYZ', scope: 'public_repo' });
550
+ await P.scaffoldStandaloneRepo(
551
+ { slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo', owner_builder_id: 7 },
552
+ { ...deps, instanceRoot: base },
553
+ );
554
+ const joined = cmds.join('\n');
555
+ // SECURITY: the token must NEVER appear in a command string (makeExec logs commands)
556
+ assert.ok(!joined.includes('gho_SECRET_TOKEN_XYZ'), 'token never appears in any command string');
557
+ // pushes over HTTPS via the credential-store helper to HEAD:main, NOT the SSH origin
558
+ assert.match(joined, /credential\.helper='store --file=[^']*bongos-gitcred-demo[^']*' push https:\/\/x-access-token@github\.com\/lars589\/demo\.git HEAD:main/, 'pushes via the token credential-store helper');
559
+ assert.ok(!/push -u origin main/.test(joined), 'does NOT use the SSH origin push when a token is available');
560
+ // the credential file is written chmod 600, carries the token (content, NOT logged), then removed
561
+ const cred = writes.find((w) => /bongos-gitcred-demo/.test(w.p));
562
+ assert.ok(cred, 'wrote a credential-store file');
563
+ assert.equal(cred.opts && cred.opts.mode, 0o600, 'credential file is chmod 600');
564
+ assert.ok(String(cred.body).includes('gho_SECRET_TOKEN_XYZ'), 'credential file carries the token in its CONTENT (makeWriteFile logs only the path + byte count)');
565
+ assert.match(joined, /rm -f [^\n]*bongos-gitcred-demo/, 'removes the credential file after the push');
566
+ } finally {
567
+ P.CONFIG.standaloneBase = origBase;
568
+ rmSync(base, { recursive: true, force: true });
569
+ }
570
+ });
571
+
572
+ await ta('scaffoldStandaloneRepo: no owner token → falls back to the SSH origin push (unchanged; task 1002317)', async () => {
573
+ const base = mkdtempSync(join(tmpdir(), 'push-ssh-'));
574
+ const origBase = P.CONFIG.standaloneBase;
575
+ P.CONFIG.standaloneBase = base;
576
+ try {
577
+ const { deps, cmds } = recorder({ apply: true }); // default getGithubToken → null
578
+ await P.scaffoldStandaloneRepo(
579
+ { slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo', owner_builder_id: 7 },
580
+ { ...deps, instanceRoot: base },
581
+ );
582
+ const joined = cmds.join('\n');
583
+ assert.match(joined, /git -C [^\n]* push -u origin main/, 'falls back to the SSH origin push');
584
+ assert.ok(!/credential\.helper=/.test(joined), 'no token credential helper when there is no token');
585
+ } finally {
586
+ P.CONFIG.standaloneBase = origBase;
587
+ rmSync(base, { recursive: true, force: true });
588
+ }
589
+ });
590
+
466
591
  await ta('provisionInstance is idempotent: an already-active instance is a no-op', async () => {
467
592
  const { deps, cmds } = recorder();
468
593
  const r = await P.provisionInstance({ ...coTenant, status: 'active' }, deps);
@@ -914,7 +1039,7 @@ await ta('provisionInstance(standalone, remote): EVERY box step runs control-pla
914
1039
  assert.ok(!box.some((c) => re.test(c)), `${re} does NOT cross to the co-hosting box`);
915
1040
  }
916
1041
  // The scaffold leg was already control-local; confirm it stayed there.
917
- assert.ok(control.some((c) => /git clone /.test(c)) && control.some((c) => /push -u origin main/.test(c)), 'scaffold clone+push stay control-local');
1042
+ assert.ok(control.some((c) => /git -C \S+ init -q/.test(c)) && control.some((c) => /push -u origin main/.test(c)), 'scaffold init+push stay control-local');
918
1043
  // NOTHING crosses to the remote box for a standalone — the whole point of the fix.
919
1044
  assert.equal(box.length, 0, 'zero remote (co-hosting box) commands for a standalone');
920
1045
  assert.equal(boxWrites.length, 0, 'zero remote writes for a standalone');