@bongos/core 1.18.3 → 1.18.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.bongos-core.json +1576 -286
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +4 -2
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +6 -1
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/migrate.sh +99 -14
  347. package/scripts/status-mirror-sync.js +114 -84
  348. package/src/bongos/api-errors.js +1 -0
  349. package/src/bongos/auth.js +447 -102
  350. package/src/bongos/db-kernel.js +27 -0
  351. package/src/bongos/db.js +39 -0
  352. package/src/bongos/module-scope-map.js +27 -1
  353. package/src/bongos/permission-path-check.js +136 -102
  354. package/src/bongos/repo-info.js +47 -11
  355. package/src/bongos/route-rank-check.js +429 -46
  356. package/src/bongos/routes/_helpers.js +12 -1
  357. package/src/bongos/routes/auth.js +219 -2
  358. package/src/bongos/routes/backup.js +58 -2
  359. package/src/bongos/routes/me.js +35 -1
  360. package/src/bongos/serve-internal.js +84 -15
  361. package/src/branding.js +32 -0
  362. package/src/instance-config.js +30 -0
  363. package/src/module-api.js +124 -1
  364. package/src/platform-server.js +86 -9
  365. package/tests/abandon_task.mjs +59 -3
  366. package/tests/api_cli_flags.mjs +53 -0
  367. package/tests/auth_page_gate.mjs +13 -0
  368. package/tests/backfill_sessions.mjs +127 -1
  369. package/tests/backup_cli.mjs +54 -0
  370. package/tests/bongos_box_connect.mjs +77 -0
  371. package/tests/box_access.mjs +130 -1
  372. package/tests/box_credential.mjs +125 -0
  373. package/tests/box_host_keys.mjs +24 -0
  374. package/tests/box_infra_resolve.mjs +149 -0
  375. package/tests/box_scope_session.mjs +6 -2
  376. package/tests/box_sync.mjs +55 -0
  377. package/tests/box_task_scope.mjs +99 -0
  378. package/tests/branding.mjs +12 -0
  379. package/tests/cf_ruleset_preflight.mjs +142 -0
  380. package/tests/cf_tunnel.mjs +37 -0
  381. package/tests/claim_gate_rebase.mjs +98 -0
  382. package/tests/clawback_confirmed_backlog.mjs +103 -0
  383. package/tests/cli_lib_formatters.mjs +20 -0
  384. package/tests/confirm_attribution.mjs +30 -46
  385. package/tests/credit_grant.mjs +198 -10
  386. package/tests/criterion_authoring_gate.mjs +272 -0
  387. package/tests/criterion_progress.mjs +33 -1
  388. package/tests/dev_lib.mjs +253 -0
  389. package/tests/devbox_artifact_gate.mjs +75 -0
  390. package/tests/devbox_dmg_backdrop.mjs +137 -0
  391. package/tests/devbox_icons.mjs +304 -0
  392. package/tests/devbox_installer_brand.mjs +232 -0
  393. package/tests/devbox_key_register.mjs +234 -0
  394. package/tests/docs_openapi_route.mjs +110 -0
  395. package/tests/doctor_hooks.mjs +141 -0
  396. package/tests/extract_json.mjs +140 -0
  397. package/tests/fitness.mjs +89 -0
  398. package/tests/gen_freshness.mjs +96 -0
  399. package/tests/goal_advisory.mjs +251 -0
  400. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  401. package/tests/goal_routes.mjs +59 -4
  402. package/tests/goal_tier.mjs +12 -3
  403. package/tests/governance_catalog.mjs +129 -0
  404. package/tests/governance_claim_eligibility.mjs +262 -0
  405. package/tests/governance_config.mjs +187 -0
  406. package/tests/governance_ownership_scope.mjs +410 -0
  407. package/tests/governance_page_gates.mjs +324 -0
  408. package/tests/governance_parity.mjs +336 -0
  409. package/tests/governance_principals.mjs +232 -0
  410. package/tests/governance_protected_surfaces.mjs +208 -0
  411. package/tests/governance_require_permission.mjs +223 -0
  412. package/tests/governance_resolver.mjs +84 -0
  413. package/tests/governance_role_sync.mjs +231 -0
  414. package/tests/governance_seed.mjs +127 -0
  415. package/tests/governance_session_scope.mjs +434 -0
  416. package/tests/governance_vanilla_boot.mjs +203 -0
  417. package/tests/grade_attribution.mjs +7 -7
  418. package/tests/hall_audit.mjs +223 -0
  419. package/tests/hall_nav.mjs +191 -0
  420. package/tests/hall_palette.mjs +206 -0
  421. package/tests/hall_signout.mjs +124 -0
  422. package/tests/hook_user_agents.mjs +10 -0
  423. package/tests/idea_triage.mjs +162 -0
  424. package/tests/idp_device_exchange.mjs +93 -0
  425. package/tests/init.mjs +51 -0
  426. package/tests/instance_config.mjs +65 -0
  427. package/tests/instance_mismatch_guard.mjs +23 -6
  428. package/tests/instance_release_config.mjs +104 -0
  429. package/tests/linkify_refs.mjs +15 -2
  430. package/tests/migrate_root_resolution.mjs +124 -1
  431. package/tests/module_contributions.mjs +55 -0
  432. package/tests/module_loader.mjs +10 -5
  433. package/tests/module_scope_active_claims.mjs +94 -0
  434. package/tests/needs_rebase.mjs +92 -0
  435. package/tests/one_active_claim_per_session.mjs +76 -0
  436. package/tests/override_attribution.mjs +10 -8
  437. package/tests/override_request_ownership.mjs +8 -4
  438. package/tests/parse_cookie_decode.mjs +42 -0
  439. package/tests/pay_on_land.mjs +129 -0
  440. package/tests/platform_boot.mjs +100 -1
  441. package/tests/post_login_landing.mjs +42 -0
  442. package/tests/preflight_checks.mjs +72 -0
  443. package/tests/provision.mjs +132 -7
  444. package/tests/publish_branch_route.mjs +53 -0
  445. package/tests/publish_reconciler.mjs +187 -2
  446. package/tests/repo_info_binding.mjs +248 -0
  447. package/tests/reward_gate.mjs +11 -2
  448. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  449. package/tests/reward_mode_surfacing.mjs +168 -0
  450. package/tests/route_permission_check.mjs +301 -0
  451. package/tests/route_rank_check.mjs +254 -12
  452. package/tests/route_validation.mjs +30 -0
  453. package/tests/sequence.mjs +267 -0
  454. package/tests/session_digest.mjs +44 -0
  455. package/tests/session_index.mjs +18 -6
  456. package/tests/session_records.mjs +39 -1
  457. package/tests/session_reward_on_land.mjs +381 -0
  458. package/tests/session_reward_reconcile.mjs +229 -0
  459. package/tests/ship_api_client_pathspec.mjs +100 -0
  460. package/tests/ship_base_flag.mjs +93 -0
  461. package/tests/ship_ci_deploy.mjs +80 -3
  462. package/tests/ship_upstream_deletion.mjs +57 -0
  463. package/tests/smoke_target.mjs +233 -0
  464. package/tests/start_env_guard.mjs +87 -0
  465. package/tests/start_rebase_warning.mjs +103 -0
  466. package/tests/status_mirror_sync.mjs +122 -0
  467. package/tests/stop_refcheck.mjs +19 -1
  468. package/tests/task_goal_link.mjs +15 -6
  469. package/tests/upgrade.mjs +139 -0
  470. package/tests/web_admission_status.mjs +162 -0
  471. package/tests/write_validation_gaps.mjs +110 -0
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ //
4
+ // scripts/gds/dev.js — `bongos dev`: stand up THIS instance locally, in one command.
5
+ //
6
+ // The gap it closes (ADR 0149): a project the creation flow scaffolds ships a repo
7
+ // configured for http://localhost:<port> but no way to START it locally. `bongos
8
+ // onboard` provisions a CLOUD server (domain + DO/Cloudflare); Docker serves a
9
+ // vanilla brand on :3000. Running THIS repo's brand on its OWN port meant a manual
10
+ // six-part Postgres+migrate+env+server assembly with three silent mismatches. This
11
+ // command does the whole thing, idempotently, and hands off to sign-in.
12
+ //
13
+ // bongos dev up: Postgres -> createdb -> migrate -> server -> login
14
+ // bongos dev stop stop the background server this launched
15
+ // bongos dev status is it up? what URL? is sign-in configured?
16
+ //
17
+ // Flags: --foreground (run attached; Ctrl-C stops), --no-login (don't auto sign in),
18
+ // --skip-migrate, --port <n>, --db <name>.
19
+ //
20
+ // Design notes:
21
+ // * The server runs DETACHED but MANAGED (pid/log keyed per instance) — the fix for
22
+ // the orphaned-background-server confusion (a stale server on :3005 answering a
23
+ // new project's login). `bongos dev status`/`stop` make it visible + killable, and
24
+ // a re-run is a no-op if it's already up. --foreground is there for those who want
25
+ // Ctrl-C lifecycle.
26
+ // * PORT is forced to branding's port (the server ignores branding — platform-server.js:44).
27
+ // * PGDATABASE is set for BOTH migrate.sh and the server (fixes the pool.js/migrate.sh
28
+ // default split); PGHOST defaults to TCP localhost (not the Linux peer socket).
29
+
30
+ const cp = require('node:child_process');
31
+ const fs = require('node:fs');
32
+ const path = require('node:path');
33
+ const dev = require('./dev-lib');
34
+ const { arg, hasFlag, loadSessionSync } = require('./cli-lib');
35
+
36
+ // ---- small terminal helpers (ascii-first; a builder's private surface) ---------
37
+ const ok = (m) => console.log(` ✓ ${m}`);
38
+ const info = (m) => console.log(`▸ ${m}`);
39
+ const warn = (m) => console.log(` ⚠ ${m}`);
40
+ const fail = (m) => console.error(`✖ ${m}`);
41
+ const blank = () => console.log('');
42
+
43
+ function run(cmd, args, opts = {}) {
44
+ const r = cp.spawnSync(cmd, args, { encoding: 'utf8', ...opts });
45
+ return { status: r.status, stdout: r.stdout || '', stderr: r.stderr || '', error: r.error };
46
+ }
47
+ function has(bin) {
48
+ // `command -v` via the binary's own --version is unreliable across tools; probe PATH directly.
49
+ const which = process.platform === 'win32' ? 'where' : 'which';
50
+ return run(which, [bin]).status === 0;
51
+ }
52
+ function tail(s, n = 6) {
53
+ return String(s || '').trimEnd().split('\n').slice(-n).join('\n');
54
+ }
55
+ // Portable synchronous pause (no subprocess, no foreground-sleep dependency).
56
+ function sleepSync(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
57
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
58
+
59
+ function usage() {
60
+ console.log(`bongos dev — run THIS instance locally, in one command.
61
+
62
+ Usage:
63
+ bongos dev [--foreground] [--no-login] [--skip-migrate] [--port <n>] [--db <name>]
64
+ bongos dev stop Stop the local server this launched
65
+ bongos dev status Show whether it's running, its URL, and sign-in state
66
+
67
+ What it does (idempotent — safe to re-run):
68
+ 1. Make sure Postgres is running (starts it via Homebrew if it isn't)
69
+ 2. Create this instance's database if it doesn't exist
70
+ 3. Build/refresh its tables (migrations)
71
+ 4. Start the server on the port in config/branding.json
72
+ 5. Sign you in to this instance (unless --no-login)
73
+
74
+ Needs Postgres + Node >= 22 installed. macOS/Linux (uses bash for migrations;
75
+ on Windows use WSL or the Docker path in docs/recipes/self-host.md).`);
76
+ }
77
+
78
+ // psql/createdb take -h/-U only when we have them; libpq fills the rest.
79
+ function pgArgs(params) {
80
+ const a = ['-h', params.pgHost];
81
+ if (params.pgUser) a.push('-U', params.pgUser);
82
+ return a;
83
+ }
84
+
85
+ function pgReady(params) {
86
+ return run('pg_isready', [...pgArgs(params), '-q']).status === 0;
87
+ }
88
+
89
+ // Best-effort: start a Homebrew-managed Postgres, then poll until it accepts
90
+ // connections. Returns true if ready, false if we couldn't bring it up.
91
+ function ensurePostgres(params) {
92
+ if (pgReady(params)) { ok('Postgres is running'); return true; }
93
+ info('Postgres is not running — trying to start it…');
94
+ if (has('brew')) {
95
+ const formulae = run('brew', ['list', '--formula']).stdout
96
+ .split(/\s+/).filter((x) => /^postgresql(@\d+)?$/.test(x))
97
+ .sort().reverse();
98
+ const target = formulae[0] || 'postgresql@16';
99
+ run('brew', ['services', 'start', target], { stdio: 'ignore' });
100
+ } else if (has('pg_ctl') && process.env.PGDATA) {
101
+ run('pg_ctl', ['start', '-w', '-D', process.env.PGDATA], { stdio: 'ignore' });
102
+ }
103
+ // Poll for readiness (~15s). pg_isready is cheap; pause between probes.
104
+ for (let i = 0; i < 15; i++) {
105
+ if (pgReady(params)) { ok('Postgres started'); return true; }
106
+ sleepSync(1000);
107
+ }
108
+ return pgReady(params);
109
+ }
110
+
111
+ function dbExists(params) {
112
+ const r = run('psql', [...pgArgs(params), '-d', 'postgres', '-tAc',
113
+ `SELECT 1 FROM pg_database WHERE datname='${params.dbName}'`]);
114
+ return r.status === 0 && r.stdout.trim() === '1';
115
+ }
116
+
117
+ function createDb(params) {
118
+ if (dbExists(params)) { ok(`database "${params.dbName}" exists`); return true; }
119
+ const r = run('createdb', [...pgArgs(params), params.dbName]);
120
+ if (r.status === 0) { ok(`created database "${params.dbName}"`); return true; }
121
+ // A race or prior partial run may have created it; treat "already exists" as fine.
122
+ if (/already exists/i.test(r.stderr)) { ok(`database "${params.dbName}" exists`); return true; }
123
+ fail(`could not create database "${params.dbName}": ${tail(r.stderr, 2) || r.error}`);
124
+ return false;
125
+ }
126
+
127
+ function migrate(params) {
128
+ const migrateSh = path.join(params.coreRoot, 'scripts', 'migrate.sh');
129
+ const env = dev.buildEnv(params, { role: 'migrate' });
130
+ const r = run('bash', [migrateSh], { cwd: params.instanceRoot, env });
131
+ if (r.status !== 0) {
132
+ fail('migrations failed:');
133
+ console.error(tail(r.stderr || r.stdout, 12));
134
+ return false;
135
+ }
136
+ const applied = (r.stdout.match(/^apply /gm) || []).length;
137
+ ok(applied ? `schema migrated (${applied} migration${applied === 1 ? '' : 's'} applied)` : 'schema up to date');
138
+ return true;
139
+ }
140
+
141
+ // Guidance for the ONE step that can require the user: a GitHub OAuth App. Only
142
+ // printed when no creds are resolvable — most machines that already ran another
143
+ // instance carry usable creds and skip this entirely.
144
+ function printOAuthGuidance(params) {
145
+ const cb = `${params.publicOrigin}/api/gds/auth/web/callback`;
146
+ const credsPath = path.join(require('node:os').homedir(), '.config', params.configDir, 'gds-github.json');
147
+ blank();
148
+ warn('Sign-in is not configured yet (no GitHub OAuth credentials found).');
149
+ console.log(' The server will run, but `bongos login` needs a GitHub OAuth App first:');
150
+ console.log(' 1. github.com → Settings → Developer settings → OAuth Apps → New OAuth App');
151
+ console.log(` Homepage URL: ${params.publicOrigin}`);
152
+ console.log(` Authorization callback URL: ${cb}`);
153
+ console.log(' Then tick "Enable Device Flow" and Save (a separate save).');
154
+ console.log(' 2. Copy the Client ID; generate a client secret.');
155
+ console.log(` 3. Save both into ${credsPath} as:`);
156
+ console.log(' { "client_id": "…", "client_secret": "…" }');
157
+ console.log(' 4. Re-run `bongos dev`.');
158
+ }
159
+
160
+ function signedInToThisInstance(params) {
161
+ try {
162
+ const s = loadSessionSync();
163
+ if (!s || !s.token || !s.api_base) return false;
164
+ return new URL(s.api_base).host === new URL(params.publicOrigin).host;
165
+ } catch { return false; }
166
+ }
167
+
168
+ async function waitHealthy(params, ms = 20000) {
169
+ const start = Date.now();
170
+ while (Date.now() - start < ms) {
171
+ const h = await dev.health(params);
172
+ if (h.up) return h;
173
+ await sleep(1000);
174
+ }
175
+ return dev.health(params);
176
+ }
177
+
178
+ function loginNow(params) {
179
+ const loginJs = path.join(params.coreRoot, 'scripts', 'gds', 'login.js');
180
+ info(`Signing in to ${params.publicOrigin}${params.firstAdmin ? ` (sign in as ${params.firstAdmin})` : ''}…`);
181
+ const r = cp.spawnSync(process.execPath, [loginJs, params.publicOrigin], { stdio: 'inherit' });
182
+ return r.status === 0;
183
+ }
184
+
185
+ // ---- subcommands ---------------------------------------------------------------
186
+
187
+ function cmdStop(params) {
188
+ const res = dev.stop(params);
189
+ if (res.wasRunning) ok(`stopped the local ${params.dbName} server (pid ${res.pid})`);
190
+ else console.log(` (no running ${params.dbName} server to stop)`);
191
+ return 0;
192
+ }
193
+
194
+ async function cmdStatus(params) {
195
+ const running = dev.isRunning(params);
196
+ console.log(`instance: ${path.basename(params.instanceRoot)} (${params.publicOrigin})`);
197
+ console.log(`database: ${params.dbName} on ${params.pgHost}`);
198
+ if (!running) { console.log('server: not running — start it with `bongos dev`'); return 0; }
199
+ const h = await dev.health(params);
200
+ console.log(`server: running (pid ${dev.readPid(params)}), health ${h.up ? 'OK' : 'starting/unreachable'}`);
201
+ console.log(`sign-in: ${h.authConfigured ? 'configured' : 'NOT configured (see `bongos dev`)'}`);
202
+ console.log(`logs: ${dev.logPath(params)}`);
203
+ return 0;
204
+ }
205
+
206
+ async function cmdUp(params, { foreground, noLogin, skipMigrate }) {
207
+ info(`Standing up ${path.basename(params.instanceRoot)} on ${params.publicOrigin}`);
208
+
209
+ // Step 0: dependencies. A freshly-cloned instance has no node_modules, so
210
+ // migrate.sh (pg) and the server (pg/express) would crash. Install FIRST, so
211
+ // `bongos dev` is a true one-command bring-up even from a bare checkout (task
212
+ // 1002405 — pairs with the root START-HERE that names `npm install` for the
213
+ // truly-bare case where not even the `bongos` bin exists yet).
214
+ const depsPlan = dev.depsInstallPlan({ instanceRoot: params.instanceRoot });
215
+ if (depsPlan.needed) {
216
+ if (!has('npm')) {
217
+ fail('node_modules is missing and `npm` is not on PATH — install Node (it bundles npm), then re-run `bongos dev`.');
218
+ return 1;
219
+ }
220
+ info(`installing dependencies (npm ${depsPlan.cmd}) — first run in this checkout, this can take a minute…`);
221
+ const r = run('npm', depsPlan.args, { cwd: params.instanceRoot, stdio: 'inherit' });
222
+ if (r.status !== 0) {
223
+ fail(`\`npm ${depsPlan.cmd}\` failed (exit ${r.status ?? '?'}). Fix the error above, then re-run \`bongos dev\`.`);
224
+ return 1;
225
+ }
226
+ ok('dependencies installed');
227
+ }
228
+
229
+ // Preflight: DB tooling + bash for migrations.
230
+ const missing = ['psql', 'createdb', 'pg_isready'].filter((b) => !has(b));
231
+ if (missing.length) {
232
+ fail(`Postgres client tools missing: ${missing.join(', ')}`);
233
+ console.error(' Install Postgres, then re-run `bongos dev`:');
234
+ console.error(process.platform === 'darwin'
235
+ ? ' brew install postgresql@16 && brew services start postgresql@16'
236
+ : ' (Debian/Ubuntu) sudo apt-get install -y postgresql (or your distro’s package)');
237
+ return 1;
238
+ }
239
+ if (!skipMigrate && !has('bash')) {
240
+ fail('`bash` is required to run migrations (used by scripts/migrate.sh).');
241
+ console.error(' On Windows, use WSL, or the Docker path in docs/recipes/self-host.md.');
242
+ return 1;
243
+ }
244
+
245
+ if (!ensurePostgres(params)) {
246
+ fail('Postgres is installed but not reachable, and I could not start it.');
247
+ console.error(` Start it yourself, then re-run \`bongos dev\`:`);
248
+ console.error(process.platform === 'darwin' ? ' brew services start postgresql@16' : ' sudo service postgresql start');
249
+ return 1;
250
+ }
251
+ if (!createDb(params)) return 1;
252
+ if (skipMigrate) warn('skipping migrations (--skip-migrate)');
253
+ else if (!migrate(params)) return 1;
254
+
255
+ const authReady = dev.authWillWork();
256
+ if (authReady) ok('sign-in configured');
257
+
258
+ // Foreground mode: run the server attached; Ctrl-C stops it. Print the login hint
259
+ // up front (the server owns the terminal once it starts).
260
+ if (foreground) {
261
+ blank();
262
+ info(`Starting ${params.dbName} on ${params.publicOrigin} (Ctrl-C to stop)`);
263
+ if (authReady && !signedInToThisInstance(params)) {
264
+ console.log(' Once it says "listening", open a SECOND terminal and run:');
265
+ console.log(` bongos login ${params.publicOrigin}${params.firstAdmin ? ` (sign in as ${params.firstAdmin})` : ''}`);
266
+ } else if (!authReady) printOAuthGuidance(params);
267
+ blank();
268
+ const env = dev.buildEnv(params, { role: 'server' });
269
+ const r = cp.spawnSync(process.execPath, [dev.serverEntry(params)], { cwd: params.instanceRoot, env, stdio: 'inherit' });
270
+ return r.status == null ? 1 : r.status;
271
+ }
272
+
273
+ // Default: managed background server.
274
+ const started = dev.startDetached(params);
275
+ if (started.already) ok(`server already running (pid ${started.pid})`);
276
+ const h = await waitHealthy(params);
277
+ if (!h.up) {
278
+ fail('the server did not become healthy in time. Recent log:');
279
+ try { console.error(tail(fs.readFileSync(started.log, 'utf8'), 12)); } catch { /* no log */ }
280
+ dev.stop(params); // don't leave a broken detached server behind
281
+ return 1;
282
+ }
283
+ if (!started.already) ok(`server running on ${params.publicOrigin} (pid ${started.pid})`);
284
+ blank();
285
+ console.log(` → ${params.publicOrigin}`);
286
+ console.log(` status: bongos dev status logs: ${started.log}`);
287
+ console.log(` stop: bongos dev stop`);
288
+ blank();
289
+
290
+ if (!authReady) { printOAuthGuidance(params); return 0; }
291
+ if (noLogin) { console.log(' Sign in when ready: bongos login ' + params.publicOrigin); return 0; }
292
+ if (signedInToThisInstance(params)) { ok('already signed in to this instance'); return 0; }
293
+ const signedIn = loginNow(params);
294
+ blank();
295
+ if (signedIn) ok('all set — you’re signed in. Try: bongos start');
296
+ else console.log(' Sign-in didn’t finish — re-run: bongos login ' + params.publicOrigin);
297
+ return 0;
298
+ }
299
+
300
+ async function main() {
301
+ const argv = process.argv.slice(2);
302
+ if (hasFlag('--help') || hasFlag('-h') || argv[0] === 'help') { usage(); return 0; }
303
+
304
+ const params = dev.resolveRunParams({ instanceRoot: process.cwd(), argv });
305
+ const sub = argv[0] && !argv[0].startsWith('-') ? argv[0] : 'up';
306
+
307
+ if (sub === 'stop') return cmdStop(params);
308
+ if (sub === 'status') return cmdStatus(params);
309
+ if (sub !== 'up') { fail(`unknown subcommand "${sub}"`); usage(); return 2; }
310
+
311
+ return cmdUp(params, {
312
+ foreground: hasFlag('--foreground'),
313
+ noLogin: hasFlag('--no-login'),
314
+ skipMigrate: hasFlag('--skip-migrate'),
315
+ });
316
+ }
317
+
318
+ if (require.main === module) {
319
+ main().then((code) => process.exit(code || 0)).catch((e) => { fail(e && e.message ? e.message : String(e)); process.exit(1); });
320
+ }
321
+
322
+ module.exports = { usage, pgArgs };
@@ -28,6 +28,7 @@ const path = require('node:path');
28
28
  const os = require('node:os');
29
29
  const { execSync, spawnSync } = require('node:child_process');
30
30
  const { cliClient, SESSION_PATH, loadSession } = require('./cli-lib');
31
+ const { preflightChecks } = require('./preflight');
31
32
 
32
33
  // ANSI helpers (only if stdout is a TTY; otherwise emit plain text)
33
34
  const TTY = process.stdout.isTTY;
@@ -130,77 +131,101 @@ async function checkSession() {
130
131
  function checkToolchain() {
131
132
  section('Toolchain');
132
133
 
133
- // Node — setup-builder.sh requires >= 22; the scripts assume modern Node.
134
- const nodeMajor = Number((process.versions.node || '0').split('.')[0]);
135
- if (nodeMajor >= 22) {
136
- ok('toolchain', `Node ${process.version} (>= 22 required).`);
137
- } else {
138
- fail('toolchain', `Node ${process.version} is below the required v22. Upgrade Node (e.g. nvm install 22, or https://nodejs.org).`);
134
+ // Node + deps + `claude` come from the shared onboarding preflight (preflight.js)
135
+ // — the single source of truth also run up front at /builder-setup (task 1002401),
136
+ // so doctor and setup can never drift. (This also gives doctor the node_modules
137
+ // check it previously lacked.)
138
+ const results = preflightChecks();
139
+ for (const r of results) {
140
+ if (r.level === 'ok') {
141
+ ok('toolchain', `${r.title}: ${r.detail}`);
142
+ } else {
143
+ const emit = r.level === 'warn' ? warn : fail;
144
+ emit('toolchain', `${r.title}: ${r.detail}`);
145
+ if (r.fix) emit('toolchain', `Fix: ${r.fix}`);
146
+ }
139
147
  }
140
148
 
141
- // claude CLI — same resolution order the grader uses.
142
- const fromEnv = process.env.OTB_CLAUDE_BIN
143
- ? ' (via OTB_CLAUDE_BIN)'
144
- : process.env.CLAUDE_BIN
145
- ? ' (via CLAUDE_BIN)'
146
- : '';
147
- const bin = process.env.OTB_CLAUDE_BIN || process.env.CLAUDE_BIN || 'claude';
148
- let r;
149
- try {
150
- r = spawnSync(bin, ['--version'], {
151
- encoding: 'utf8',
152
- shell: process.platform === 'win32',
153
- windowsHide: true,
154
- timeout: 15000,
155
- });
156
- } catch (err) {
157
- r = { error: err };
158
- }
159
- if (r && !r.error && r.status === 0) {
160
- const ver = (r.stdout || '').trim().split('\n')[0] || 'present';
161
- ok('toolchain', `claude CLI launches${fromEnv}: ${ver} — the grader can spawn its workers.`);
162
- // Windows-only: warn if the resolved binary is the desktop "App execution
163
- // alias" stub. It can pass --version yet misbehave under the grader's
164
- // stdio redirection (SUBAGENT_NONZERO_EXIT). Learning #42.
165
- if (process.platform === 'win32' && !process.env.OTB_CLAUDE_BIN && !process.env.CLAUDE_BIN) {
166
- try {
167
- const where = spawnSync('where', ['claude'], { encoding: 'utf8', shell: true, timeout: 8000 });
168
- if (where.status === 0 && /WindowsApps/i.test(where.stdout || '')) {
169
- warn('toolchain', `claude resolves to the WindowsApps desktop alias, which can fail headless grading (SUBAGENT_NONZERO_EXIT). Prefer a real CLI: npm i -g @anthropic-ai/claude-code (learning #42).`);
170
- }
171
- } catch (_) { /* best-effort */ }
172
- }
173
- } else {
174
- const why = r && r.error ? (r.error.code || r.error.message) : `exited ${r ? r.status : '?'}`;
175
- fail('toolchain', `claude CLI not launchable (${bin}: ${why}). The grader spawns it per worker — without it, ships fail grading with SUBAGENT_SPAWN_FAILED.`);
176
- fail('toolchain', `Fix: npm i -g @anthropic-ai/claude-code (puts a real claude on PATH). If it lives elsewhere, set OTB_CLAUDE_BIN to its full path and re-run.`);
149
+ // Windows-only extra (beyond the shared preflight): even when `claude` launches,
150
+ // warn if it resolves to the WindowsApps desktop "App execution alias" stub — it
151
+ // can pass --version yet misbehave under the grader's stdio redirection
152
+ // (SUBAGENT_NONZERO_EXIT). Learning #42.
153
+ const claudeOk = results.some((r) => r.title === 'claude CLI' && r.level === 'ok');
154
+ if (claudeOk && process.platform === 'win32' && !process.env.OTB_CLAUDE_BIN && !process.env.CLAUDE_BIN) {
155
+ try {
156
+ const where = spawnSync('where', ['claude'], { encoding: 'utf8', shell: true, timeout: 8000 });
157
+ if (where.status === 0 && /WindowsApps/i.test(where.stdout || '')) {
158
+ warn('toolchain', `claude resolves to the WindowsApps desktop alias, which can fail headless grading (SUBAGENT_NONZERO_EXIT). Prefer a real CLI: npm i -g @anthropic-ai/claude-code (learning #42).`);
159
+ }
160
+ } catch (_) { /* best-effort */ }
177
161
  }
178
162
  }
179
163
 
180
164
  // --------------------------------------------------------------------------
181
165
  // Check 3 — Git plumbing
182
166
  // --------------------------------------------------------------------------
183
- function checkGitPlumbing() {
184
- section('Git plumbing');
185
- const hooksPath = gitOut('git config --get core.hooksPath');
167
+ // Pure verdict for the hooks gate, extracted so the truth table is testable
168
+ // (tests/doctor_hooks.mjs) — the box-auth-check `decideBoxAuth` precedent.
169
+ //
170
+ // `.husky/` is INSTANCE topology, not shipped in the portable core (README
171
+ // "Secrets scanning"). So its absence is not a builder error: it means ADR 0043
172
+ // layer 1 has nothing to activate here, and CI + the post-push main audit are
173
+ // the enforcing layers. Recommending `.husky` regardless is what produced the
174
+ // unbreakable red loop this replaces — doctor demanded a dir that wasn't there,
175
+ // then failed on the dangling path it had just told you to set.
176
+ function classifyHooksState({ hooksPath, worktreeRoot, exists = fs.existsSync }) {
177
+ const huskyDir = worktreeRoot ? path.join(worktreeRoot, '.husky') : '.husky';
178
+ const huskyPresent = exists(huskyDir);
179
+ const installer = 'node scripts/gds/install-git-hooks.js';
180
+
186
181
  if (!hooksPath) {
187
- fail('git', `core.hooksPath is unset. Activate the pre-commit secrets guard with:\n git config core.hooksPath .husky`);
188
- return;
189
- }
190
- // Resolve relative to current worktree root (git's behavior for relative
191
- // hooksPath values). Absolute paths resolve as-is.
192
- let resolved = hooksPath;
193
- if (!path.isAbsolute(hooksPath)) {
194
- const top = gitOut('git rev-parse --show-toplevel');
195
- if (top) resolved = path.join(top, hooksPath);
182
+ if (!huskyPresent) {
183
+ return {
184
+ status: 'warn',
185
+ code: 'no-hooks-dir',
186
+ resolved: null,
187
+ message:
188
+ 'No hooks dir in this checkout (.husky/ is instance topology, not shipped in the portable core), '
189
+ + 'so the local pre-commit/pre-push gates are inactive. CI secrets-scan and the post-push main audit still enforce.',
190
+ };
191
+ }
192
+ return {
193
+ status: 'fail',
194
+ code: 'inactive',
195
+ resolved: null,
196
+ message: `.husky/ is present but core.hooksPath is unset — the secrets + permission gates are not running. Activate with: ${installer}`,
197
+ };
196
198
  }
197
- if (!fs.existsSync(resolved)) {
198
- fail('git', `core.hooksPath="${hooksPath}" resolves to a non-existent path: ${resolved}`);
199
- fail('git', `Reset with: git config core.hooksPath .husky`);
200
- return;
199
+
200
+ // Relative hooksPath resolves against the worktree root (git's behavior);
201
+ // absolute resolves as-is.
202
+ const resolved = path.isAbsolute(hooksPath) || !worktreeRoot
203
+ ? hooksPath
204
+ : path.join(worktreeRoot, hooksPath);
205
+
206
+ if (!exists(resolved)) {
207
+ return {
208
+ status: 'fail',
209
+ code: 'dangling',
210
+ resolved,
211
+ message: huskyPresent
212
+ ? `core.hooksPath="${hooksPath}" resolves to a non-existent path: ${resolved}. Repoint it with: ${installer}`
213
+ : `core.hooksPath="${hooksPath}" resolves to a non-existent path: ${resolved}, and this checkout has no .husky/ to point at. Clear it with: git config --unset core.hooksPath`,
214
+ };
201
215
  }
202
- ok('git', `core.hooksPath="${hooksPath}" → ${resolved} (exists)`);
203
- const preCommit = path.join(resolved, 'pre-commit');
216
+ return { status: 'ok', code: 'active', resolved, message: `core.hooksPath="${hooksPath}" → ${resolved} (exists)` };
217
+ }
218
+
219
+ function checkGitPlumbing() {
220
+ section('Git plumbing');
221
+ const verdict = classifyHooksState({
222
+ hooksPath: gitOut('git config --get core.hooksPath'),
223
+ worktreeRoot: gitOut('git rev-parse --show-toplevel'),
224
+ });
225
+ const report = { ok, warn, fail }[verdict.status];
226
+ report('git', verdict.message);
227
+ if (verdict.status !== 'ok') return;
228
+ const preCommit = path.join(verdict.resolved, 'pre-commit');
204
229
  if (!fs.existsSync(preCommit)) {
205
230
  warn('git', `No pre-commit file at ${preCommit} — secrets-guard hook will not fire.`);
206
231
  } else {
@@ -353,7 +378,11 @@ function checkDeployTooling() {
353
378
  // --------------------------------------------------------------------------
354
379
  // main
355
380
  // --------------------------------------------------------------------------
356
- (async () => {
381
+ // Guarded so requiring this file for its pure helpers (tests/doctor_hooks.mjs)
382
+ // does not run the checks, which shell out to git and hit the network.
383
+ module.exports = { classifyHooksState };
384
+
385
+ if (require.main === module) (async () => {
357
386
  console.log(`${C.bold}GDS Doctor${C.reset} — local-machine health check`);
358
387
  await checkSession();
359
388
  checkToolchain();
@@ -46,7 +46,7 @@ async function main() {
46
46
  const res = await fetch(`${base}/api/gds/me/art-key`, {
47
47
  headers: {
48
48
  Authorization: `Bearer ${session.token}`,
49
- 'User-Agent': 'amazonprimea-fetch-art-key/1.0',
49
+ 'User-Agent': require('../../src/branding').userAgent(),
50
50
  },
51
51
  signal: ctrl.signal,
52
52
  });
@@ -49,11 +49,11 @@ async function main() {
49
49
  // existing cached state (fail-open).
50
50
  const [soundRes, eventRes] = await Promise.all([
51
51
  fetch(`${sess.apiBase}/api/gds/me/sound-prefs`, {
52
- headers: { Authorization: `Bearer ${sess.token}`, 'User-Agent': 'amazonprimea-fetch-sound-prefs/1.0' },
52
+ headers: { Authorization: `Bearer ${sess.token}`, 'User-Agent': require('../../src/branding').userAgent() },
53
53
  signal: ctrl.signal,
54
54
  }),
55
55
  fetch(`${sess.apiBase}/api/gds/me/event-sound-prefs`, {
56
- headers: { Authorization: `Bearer ${sess.token}`, 'User-Agent': 'amazonprimea-fetch-sound-prefs/1.0' },
56
+ headers: { Authorization: `Bearer ${sess.token}`, 'User-Agent': require('../../src/branding').userAgent() },
57
57
  signal: ctrl.signal,
58
58
  }),
59
59
  ]);