@bongos/core 1.21.175 → 1.21.177

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 (1719) hide show
  1. package/.bongos-core.json +3371 -1636
  2. package/.claude/skills/ask-for-help/SKILL.md +4 -4
  3. package/.claude/skills/backlog-review/SKILL.md +1 -1
  4. package/.claude/skills/blocker-review/SKILL.md +2 -2
  5. package/.claude/skills/blocker-solve/SKILL.md +4 -4
  6. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  7. package/.claude/skills/bug-triage/SKILL.md +8 -8
  8. package/.claude/skills/builder-backup/SKILL.md +3 -3
  9. package/.claude/skills/builder-claim/SKILL.md +5 -5
  10. package/.claude/skills/builder-cost/SKILL.md +5 -5
  11. package/.claude/skills/builder-end/SKILL.md +6 -6
  12. package/.claude/skills/builder-exit/SKILL.md +2 -2
  13. package/.claude/skills/builder-key/SKILL.md +6 -6
  14. package/.claude/skills/builder-reauth/SKILL.md +4 -4
  15. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  16. package/.claude/skills/builder-release/SKILL.md +3 -3
  17. package/.claude/skills/builder-sequence/SKILL.md +8 -8
  18. package/.claude/skills/builder-setup/SKILL.md +4 -4
  19. package/.claude/skills/builder-ship/SKILL.md +7 -7
  20. package/.claude/skills/builder-stage/SKILL.md +8 -8
  21. package/.claude/skills/builder-start/SKILL.md +4 -4
  22. package/.claude/skills/collab-review/SKILL.md +5 -5
  23. package/.claude/skills/design/SKILL.md +6 -4
  24. package/.claude/skills/design-sync/SKILL.md +3 -3
  25. package/.claude/skills/figma-design-sync/SKILL.md +4 -4
  26. package/.claude/skills/fix-task/SKILL.md +8 -8
  27. package/.claude/skills/goal-close/SKILL.md +9 -9
  28. package/.claude/skills/goal-create/SKILL.md +18 -18
  29. package/.claude/skills/grade-recover/SKILL.md +8 -8
  30. package/.claude/skills/grade-sweep/SKILL.md +15 -15
  31. package/.claude/skills/idea-triage/SKILL.md +15 -15
  32. package/.claude/skills/merge-mode/SKILL.md +14 -14
  33. package/.claude/skills/new-project/SKILL.md +5 -5
  34. package/.claude/skills/owner-review/SKILL.md +1 -1
  35. package/.claude/skills/planning-session/SKILL.md +15 -15
  36. package/.claude/skills/priority-session/SKILL.md +6 -6
  37. package/.claude/skills/read-session-export/SKILL.md +3 -3
  38. package/.claude/skills/recall/SKILL.md +7 -7
  39. package/.claude/skills/scan-before-install/SKILL.md +3 -3
  40. package/.claude/skills/session-handoff/SKILL.md +1 -1
  41. package/.claude/skills/ship-check/SKILL.md +3 -3
  42. package/.claude/skills/status/SKILL.md +3 -3
  43. package/.claude/skills/strand-fix/SKILL.md +6 -6
  44. package/.claude/skills/worktree-clean/SKILL.md +4 -4
  45. package/.gitattributes +12 -32
  46. package/GOVERNMENT.md +1 -1
  47. package/README.md +8 -8
  48. package/bin/bongos.js +23 -23
  49. package/clients/bongos-client/README.md +2 -2
  50. package/clients/bongos-client/bongos-client.global.js +22 -4
  51. package/clients/bongos-client/index.cjs +22 -4
  52. package/clients/bongos-client/index.d.ts +33 -4
  53. package/clients/bongos-client/index.mjs +21 -3
  54. package/config/branding.neutral.json +1 -1
  55. package/config/design-tokens.schema.json +1 -1
  56. package/config/modules.neutral.json +1 -3
  57. package/config/scheduled-routines.json +3 -8
  58. package/docs/adr/0029-status-page-outage-resilience.md +6 -6
  59. package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +1 -1
  60. package/docs/adr/0093-tranche-2-core-carve-sequence.md +1 -1
  61. package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +1 -1
  62. package/docs/adr/0294-cli-sessions-do-not-expire.md +1 -1
  63. package/docs/adr/0383-sessions-end-after-ten-idle-days.md +35 -0
  64. package/docs/adr/0384-the-raise-page-is-a-hub-module-builders-edit-in-place.md +31 -0
  65. package/docs/adr/README.md +2 -0
  66. package/docs/api/openapi.json +606 -44
  67. package/docs/api-reference.md +25 -12
  68. package/docs/architecture.md +8 -7
  69. package/docs/canonical-permissions.md +4 -4
  70. package/docs/copy-inventory.md +1370 -1411
  71. package/docs/copy-registry.json +2136 -2536
  72. package/docs/design/demonolith-plan.md +6 -6
  73. package/docs/design/modular-architecture/00-research-report.md +1 -1
  74. package/docs/design/project-startup-direction.md +2 -2
  75. package/docs/design/projects-hub-direction-v2.md +1 -1
  76. package/docs/design/reviews/hall-v2/README.md +2 -2
  77. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  78. package/docs/file-map.md +92 -96
  79. package/docs/handoff-template.md +5 -5
  80. package/docs/module-api-changelog.md +7 -3
  81. package/docs/modules-contract.md +8 -7
  82. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +1 -1
  83. package/docs/onboarding/diagrams/02-rank-ladder.mmd +1 -1
  84. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
  85. package/docs/onboarding/diagrams/04-architecture.mmd +1 -1
  86. package/docs/onboarding/diagrams/05-versioning.mmd +1 -1
  87. package/docs/onboarding/diagrams/README.md +5 -9
  88. package/docs/onboarding/diagrams/assertions.json +3 -3
  89. package/docs/onboarding/slash-commands.md +2 -2
  90. package/docs/packs/artist.md +4 -4
  91. package/docs/packs/engineer.md +11 -10
  92. package/docs/packs/ideator.md +7 -7
  93. package/docs/page-inventory.json +20 -1
  94. package/docs/page-readings.json +4387 -4603
  95. package/docs/recipes/README.md +1 -1
  96. package/docs/recipes/audit-replay.md +4 -4
  97. package/docs/recipes/autobongos-windows-host.md +6 -6
  98. package/docs/recipes/bongos-cli-release.md +2 -2
  99. package/docs/recipes/claude-design-loop.md +7 -7
  100. package/docs/recipes/core-release-pipeline.md +19 -19
  101. package/docs/recipes/failed-ship-recovery-proof.md +2 -2
  102. package/docs/recipes/figma-design-loop.md +6 -6
  103. package/docs/recipes/gds-db-backup.md +9 -9
  104. package/docs/recipes/instance-deploy.md +3 -3
  105. package/docs/recipes/instance-first-run.md +1 -1
  106. package/docs/recipes/multi-builder-merge.md +2 -2
  107. package/docs/recipes/ops-gotchas.md +13 -10
  108. package/docs/recipes/packaging-the-core.md +6 -6
  109. package/docs/recipes/parallel-goal-waves.md +4 -4
  110. package/docs/recipes/private-npm-distribution.md +9 -9
  111. package/docs/recipes/redteam-patrol.md +5 -5
  112. package/docs/recipes/render-pilot.md +2 -2
  113. package/docs/recipes/reranking-a-goals-board.md +2 -2
  114. package/docs/recipes/search-index-ingestion.md +5 -5
  115. package/docs/recipes/self-host.md +1 -1
  116. package/docs/recipes/ship-and-regrade.md +4 -4
  117. package/docs/recipes/staging-environment.md +1 -1
  118. package/docs/recipes/standalone-live-docs.md +1 -1
  119. package/docs/recipes/ui-look-before-you-ship.md +2 -2
  120. package/docs/recipes/upgrading-the-core.md +11 -11
  121. package/docs/recipes/windows-builders.md +18 -18
  122. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +6 -6
  123. package/docs/research/task-network-map-readability.md +3 -3
  124. package/migrations/core_278_builder_tester_tag.sql +18 -0
  125. package/migrations/core_279_bongos_shipper_new_scripts_path.sql +13 -0
  126. package/modules/agents/lib/answer-hold.js +1 -1
  127. package/modules/agents/lib/gate.js +1 -1
  128. package/modules/agents/lib/validate.js +1 -1
  129. package/modules/autonomy/db.js +9 -0
  130. package/modules/autonomy/gauge.js +1 -1
  131. package/modules/autonomy/governor-docket.js +2 -2
  132. package/modules/autonomy/module.json +2 -2
  133. package/modules/autonomy/pollers/daily-checks.js +262 -0
  134. package/modules/autonomy/pollers/routine-timers.js +9 -7
  135. package/modules/autonomy/rank-roles.js +64 -0
  136. package/modules/autonomy/routes/autonomy.js +3 -3
  137. package/modules/autonomy/server-schedule.js +37 -0
  138. package/modules/builder-settings/interaction-prefs.js +1 -1
  139. package/modules/builder-settings/render-prefs.js +1 -1
  140. package/modules/builder-settings/routes/settings.js +1 -1
  141. package/modules/builder-settings/skill-prefs.js +2 -2
  142. package/modules/builder-settings/wandering-prefs.js +2 -2
  143. package/modules/copy-desk/module.json +2 -2
  144. package/modules/copy-desk/page-data.js +5 -5
  145. package/modules/copy-desk/page-decisions.js +13 -1
  146. package/modules/copy-desk/proposals.js +4 -4
  147. package/modules/copy-desk/registry.js +2 -2
  148. package/modules/copy-desk/routes/copy-desk-release.js +119 -0
  149. package/modules/copy-desk/routes/copy-desk.js +3 -12
  150. package/modules/copy-desk/skills/tweak/SKILL.md +12 -12
  151. package/modules/copy-desk/tests/copy_flags.mjs +1 -1
  152. package/modules/copy-desk/tests/copy_no_cms.mjs +7 -3
  153. package/modules/copy-desk/tests/copy_proposals.mjs +2 -2
  154. package/modules/copy-desk/tests/copy_queue.mjs +1 -1
  155. package/modules/design-styles/skills/README.md +4 -4
  156. package/modules/design-styles/skills/imagegen-frontend-mobile/SKILL.md +1 -1
  157. package/modules/design-styles/skills/imagegen-frontend-web/SKILL.md +1 -1
  158. package/modules/discord/bug-attachments.js +1 -1
  159. package/modules/discord/ship-broadcast.js +1 -1
  160. package/modules/economy/achievements.js +49 -7
  161. package/modules/economy/cost-classify.js +6 -6
  162. package/modules/economy/cost-report.js +1 -1
  163. package/modules/economy/cost.js +1 -1
  164. package/modules/economy/credits.js +2 -1
  165. package/modules/economy/idea-size.js +2 -2
  166. package/modules/economy/reward.js +2 -0
  167. package/modules/economy/routes/cost.js +1 -1
  168. package/modules/economy/streak.js +43 -4
  169. package/modules/government/asks.js +1 -1
  170. package/modules/government/board.js +19 -2
  171. package/modules/government/catalog.js +3 -0
  172. package/modules/government/migrations/government_033_idea_vote.sql +46 -0
  173. package/modules/government/protected-surfaces.json +48 -47
  174. package/modules/government/recommendations.js +6 -2
  175. package/modules/government/routes/government.js +1 -1
  176. package/modules/grading/generated-provenance.js +13 -5
  177. package/modules/grading/grade-engine.js +1 -1
  178. package/modules/grading/grader-rubric.json +1 -1
  179. package/modules/grading/grader-score.js +1 -1
  180. package/modules/grading/grader-workers/index.js +1 -1
  181. package/modules/grading/grader.js +1 -1
  182. package/modules/grading/routes/grade.js +1 -1
  183. package/modules/hall-ui/public/atlas.html +5 -7
  184. package/modules/hall-ui/public/atlas.js +1 -1
  185. package/modules/hall-ui/public/atlas.states.json +1 -1
  186. package/modules/hall-ui/public/blockers.css +9 -0
  187. package/modules/hall-ui/public/blockers.html +4 -4
  188. package/modules/hall-ui/public/blockers.js +29 -30
  189. package/modules/hall-ui/public/board-room.css +5 -17
  190. package/modules/hall-ui/public/board-room.html +5 -6
  191. package/modules/hall-ui/public/board-room.js +41 -23
  192. package/modules/hall-ui/public/board-room.states.json +1 -1
  193. package/modules/hall-ui/public/brand-apply.js +1 -1
  194. package/modules/hall-ui/public/brand-holes.js +1 -1
  195. package/modules/hall-ui/public/builders.js +40 -86
  196. package/modules/hall-ui/public/city-decide.js +18 -4
  197. package/modules/hall-ui/public/city.html +1 -1
  198. package/modules/hall-ui/public/collab.html +1 -1
  199. package/modules/hall-ui/public/commands.html +6 -9
  200. package/modules/hall-ui/public/copy-desk.html +1 -1
  201. package/modules/hall-ui/public/copy-desk.js +2 -2
  202. package/modules/hall-ui/public/craft-mode.js +4 -0
  203. package/modules/hall-ui/public/deploy.html +6 -2
  204. package/modules/hall-ui/public/deploy.js +21 -13
  205. package/modules/hall-ui/public/diagrams.css +1 -1
  206. package/modules/hall-ui/public/diagrams.html +5 -8
  207. package/modules/hall-ui/public/diagrams.states.json +1 -1
  208. package/modules/hall-ui/public/drachmae.html +3 -7
  209. package/modules/hall-ui/public/fleet.html +3 -2
  210. package/modules/hall-ui/public/fleet.js +0 -4
  211. package/modules/hall-ui/public/gate.html +7 -7
  212. package/modules/hall-ui/public/gate.js +58 -77
  213. package/modules/hall-ui/public/goal-map.html +1 -1
  214. package/modules/hall-ui/public/goals-page.js +5 -5
  215. package/modules/hall-ui/public/goals-render.js +10 -11
  216. package/modules/hall-ui/public/goals.css +10 -0
  217. package/modules/hall-ui/public/goals.html +14 -7
  218. package/modules/hall-ui/public/government.html +3 -5
  219. package/modules/hall-ui/public/government.js +1 -5
  220. package/modules/hall-ui/public/hall-render.js +117 -57
  221. package/modules/hall-ui/public/hall-widgets.js +11 -11
  222. package/modules/hall-ui/public/idea.html +1 -1
  223. package/modules/hall-ui/public/ideas.html +3 -1
  224. package/modules/hall-ui/public/ideas.js +1 -0
  225. package/modules/hall-ui/public/index.html +12 -6
  226. package/modules/hall-ui/public/index.states.json +1 -1
  227. package/modules/hall-ui/public/mine-lens.js +1 -1
  228. package/modules/hall-ui/public/modules.html +7 -10
  229. package/modules/hall-ui/public/modules.js +0 -2
  230. package/modules/hall-ui/public/my-agents.html +1 -1
  231. package/modules/hall-ui/public/palette.js +5 -5
  232. package/modules/hall-ui/public/primer.html +4 -9
  233. package/modules/hall-ui/public/profile-connections.js +10 -2
  234. package/modules/hall-ui/public/profile-roles.js +3 -10
  235. package/modules/hall-ui/public/profile.html +12 -10
  236. package/modules/hall-ui/public/profile.js +13 -17
  237. package/modules/hall-ui/public/project-settings.html +3 -4
  238. package/modules/hall-ui/public/project-settings.js +0 -3
  239. package/modules/hall-ui/public/ranks.html +5 -11
  240. package/modules/hall-ui/public/ranks.js +6 -8
  241. package/modules/hall-ui/public/roadmap.css +0 -2
  242. package/modules/hall-ui/public/roadmap.html +1 -1
  243. package/modules/hall-ui/public/roadmap.js +16 -51
  244. package/modules/hall-ui/public/roster.html +1 -1
  245. package/modules/hall-ui/public/sessions.html +6 -8
  246. package/modules/hall-ui/public/sessions.js +7 -7
  247. package/modules/hall-ui/public/settings.html +1 -1
  248. package/modules/hall-ui/public/shell-trail.js +1 -1
  249. package/modules/hall-ui/public/shell.js +17 -17
  250. package/modules/hall-ui/public/street-deck.css +2 -1
  251. package/modules/hall-ui/public/street-deck.js +10 -2
  252. package/modules/hall-ui/public/street-desk.js +16 -5
  253. package/modules/hall-ui/public/street.html +1 -1
  254. package/modules/hall-ui/public/studio-lib.js +4 -0
  255. package/modules/hall-ui/public/studio.css +2 -0
  256. package/modules/hall-ui/public/studio.html +1 -1
  257. package/modules/hall-ui/public/studio.js +27 -1
  258. package/modules/hall-ui/public/style.css +43 -26
  259. package/modules/hall-ui/public/task-detail.js +10 -7
  260. package/modules/hall-ui/public/task.css +2 -0
  261. package/modules/hall-ui/public/task.html +3 -1
  262. package/modules/hall-ui/public/task.js +23 -7
  263. package/modules/hall-ui/public/thinking.html +1 -1
  264. package/modules/hall-ui/public/tweak-editor.html +1 -1
  265. package/modules/hall-ui/public/vote-control.js +208 -0
  266. package/modules/hall-ui/public/watch.html +3 -5
  267. package/modules/hall-ui/public/watch.js +5 -4
  268. package/modules/hall-ui/public/welcome-boot.css +130 -0
  269. package/modules/hall-ui/public/welcome-boot.js +334 -0
  270. package/modules/hall-ui/public/welcome-day.webp +0 -0
  271. package/modules/hall-ui/public/welcome-night.webp +0 -0
  272. package/modules/hall-ui/public/work-summary.js +1 -1
  273. package/modules/hall-ui/public/work.css +1 -0
  274. package/modules/hall-ui/public/work.html +3 -1
  275. package/modules/hall-ui/public/work.js +2 -1
  276. package/modules/hall-ui/records/accomplishments.md +16 -0
  277. package/modules/hall-ui/records/hall-ask-card.md +9 -0
  278. package/modules/hall-ui/records/karma-vote-control.md +13 -0
  279. package/modules/hall-ui/records/oversight-family.md +1 -1
  280. package/modules/hall-ui/records/profile-connections.md +2 -0
  281. package/modules/hall-ui/records/profile-roles.md +1 -1
  282. package/modules/hall-ui/records/reading-rooms.md +1 -1
  283. package/modules/hall-ui/records/welcome-tower.md +50 -0
  284. package/modules/ideas/migrations/ideas_011_idea_votes.sql +31 -0
  285. package/modules/ideas/projection.js +1 -1
  286. package/modules/ideas/routes/blockers.js +3 -3
  287. package/modules/ideas/routes/inbox.js +29 -1
  288. package/modules/ideas/routes/ratify-goal.js +1 -1
  289. package/modules/ideas/routing.js +1 -1
  290. package/modules/ideas/templates.js +1 -1
  291. package/modules/ideas/trail-descent.js +1 -1
  292. package/modules/ideas/trail-size.js +1 -1
  293. package/modules/ideas/votes.js +93 -0
  294. package/modules/lifecycle/abandon-leftovers.js +1 -1
  295. package/modules/lifecycle/cascade.js +1 -1
  296. package/modules/lifecycle/conflict-resolve.js +13 -21
  297. package/modules/lifecycle/criterion-ref.js +1 -1
  298. package/modules/lifecycle/db-analytics.js +57 -4
  299. package/modules/lifecycle/db-deps-criteria.js +1 -1
  300. package/modules/lifecycle/db-ship.js +2 -2
  301. package/modules/lifecycle/db-tasks.js +2 -2
  302. package/modules/lifecycle/db-versions.js +1 -1
  303. package/modules/lifecycle/db.js +2 -0
  304. package/modules/lifecycle/dead-deps.js +3 -3
  305. package/modules/lifecycle/dependency-advisory.js +3 -3
  306. package/modules/lifecycle/goal-category.js +1 -1
  307. package/modules/lifecycle/goal-forest.js +1 -1
  308. package/modules/lifecycle/goal-task-relevance-advisory.js +1 -1
  309. package/modules/lifecycle/kickoff-checklist.js +1 -1
  310. package/modules/lifecycle/kickoff-seed.js +1 -1
  311. package/modules/lifecycle/lifecycle.js +2 -0
  312. package/modules/lifecycle/merge-reach.js +29 -15
  313. package/modules/lifecycle/missing-dep-classify.js +3 -3
  314. package/modules/lifecycle/module.json +2 -1
  315. package/modules/lifecycle/page-tweak-approve.js +85 -1
  316. package/modules/lifecycle/page-tweak-hold.js +1 -1
  317. package/modules/lifecycle/publish-reconciler.js +1 -2
  318. package/modules/lifecycle/rot.js +1 -1
  319. package/modules/lifecycle/routes/artist-gate.js +1 -1
  320. package/modules/lifecycle/routes/gate-approvals.js +22 -1
  321. package/modules/lifecycle/routes/goals.js +1 -1
  322. package/modules/lifecycle/routes/task-route-helpers.js +1 -1
  323. package/modules/lifecycle/routes/task-votes.js +54 -0
  324. package/modules/lifecycle/routes/task-write-routes.js +1 -1
  325. package/modules/lifecycle/routes/tasks.js +2 -2
  326. package/modules/lifecycle/routes/versions.js +2 -2
  327. package/modules/lifecycle/ship-card.js +4 -4
  328. package/modules/lifecycle/ship-preflight.js +1 -1
  329. package/modules/lifecycle/task-classifier.js +1 -1
  330. package/modules/lifecycle/task-status.js +1 -1
  331. package/modules/lifecycle/task-visual-db.js +1 -1
  332. package/modules/memory/learnings.js +1 -1
  333. package/modules/memory/pollers/search-index.js +2 -2
  334. package/modules/memory/routes/learnings.js +1 -1
  335. package/modules/memory/routes/memory.js +1 -1
  336. package/modules/memory/search.js +2 -2
  337. package/modules/npm-release/release.js +1 -1
  338. package/modules/onboarding/howto-moments.js +1 -1
  339. package/modules/onboarding/newcomer-restock.js +3 -3
  340. package/modules/onboarding/onboarding.js +1 -1
  341. package/modules/pixel-art/skills/otb-tile-generate/SKILL.md +1 -1
  342. package/modules/platform-identity/origin-containment.js +1 -1
  343. package/modules/platform-identity/publish-gate.js +1 -1
  344. package/modules/platform-identity/tests/platform-identity.mjs +1 -1
  345. package/modules/provisioning/capacity.js +1 -1
  346. package/modules/provisioning/core-upgrade.js +15 -9
  347. package/modules/provisioning/credential-preflight.js +3 -3
  348. package/modules/provisioning/disconnect.js +1 -1
  349. package/modules/provisioning/dns-resolves.js +1 -1
  350. package/modules/provisioning/module.json +1 -1
  351. package/modules/provisioning/onboard-plan.js +1 -1
  352. package/modules/provisioning/paid-shape-gate.js +2 -2
  353. package/modules/provisioning/planet-physics.js +11 -2
  354. package/modules/provisioning/pollers/liveness-sweep.js +1 -1
  355. package/modules/provisioning/pollers/runner-drift-notify.js +5 -5
  356. package/modules/provisioning/provisioning.js +2 -2
  357. package/modules/provisioning/render-lookup.js +1 -1
  358. package/modules/provisioning/render-standup.js +3 -3
  359. package/modules/provisioning/repo-private.js +1 -1
  360. package/modules/provisioning/routes/callback-page.js +1 -1
  361. package/modules/provisioning/routes/core-upgrade.js +23 -3
  362. package/modules/provisioning/routes/provisioning.js +2 -2
  363. package/modules/provisioning/routes/render-standup.js +1 -1
  364. package/modules/provisioning/routes/repo-private.js +1 -1
  365. package/modules/provisioning/soft-limits.js +1 -1
  366. package/modules/provisioning/starter-bundles.js +4 -2
  367. package/modules/provisioning/tests/provisioning.mjs +1 -1
  368. package/modules/public-landing/public/account.html +4 -6
  369. package/modules/public-landing/public/assets/cosmos.css +13 -6
  370. package/modules/public-landing/public/connections.html +4 -6
  371. package/modules/public-landing/public/contact.html +4 -6
  372. package/modules/public-landing/public/guild.html +4 -6
  373. package/modules/public-landing/public/index.html +11 -9
  374. package/modules/public-landing/public/invitations.html +4 -6
  375. package/modules/public-landing/public/privacy.html +4 -6
  376. package/modules/public-landing/public/projects.html +168 -46
  377. package/modules/public-landing/public/terms.html +4 -6
  378. package/modules/raise/content/copy.json +40 -0
  379. package/modules/raise/content/nda.html +39 -0
  380. package/modules/raise/content/settings.json +49 -0
  381. package/modules/raise/db.js +95 -0
  382. package/modules/raise/migrations/raise_001_tables.sql +71 -0
  383. package/modules/raise/module.json +28 -0
  384. package/modules/raise/page.js +194 -0
  385. package/modules/raise/public/edit.js +152 -0
  386. package/modules/raise/public/favicon.svg +1 -0
  387. package/modules/raise/public/logo-ink.svg +1 -0
  388. package/modules/raise/public/logo-night.svg +1 -0
  389. package/modules/raise/public/mode.js +8 -0
  390. package/modules/raise/public/raise.css +234 -0
  391. package/modules/raise/public/raise.js +91 -0
  392. package/modules/raise/raise.js +174 -0
  393. package/modules/raise/routes/raise.js +152 -0
  394. package/modules/raise/views/gate.html +39 -0
  395. package/modules/raise/views/nda.html +21 -0
  396. package/modules/raise/views/page.html +133 -0
  397. package/modules/raise/web.js +72 -0
  398. package/modules/render-deploy/deploys.js +1 -1
  399. package/modules/render-deploy/render.js +2 -2
  400. package/modules/security/secret-scrub.js +3 -3
  401. package/modules/security/security.js +1 -1
  402. package/modules/security/tests/artifact_scan.mjs +1 -1
  403. package/modules/security/tests/mirror.mjs +1 -1
  404. package/modules/security/tests/scan_store.mjs +1 -1
  405. package/modules/sessions/db.js +2 -2
  406. package/modules/sessions/pulse.js +2 -2
  407. package/modules/sessions/routes/sessions.js +1 -1
  408. package/modules/specialities/specialities.js +1 -1
  409. package/modules/status-ui/public/README.md +3 -4
  410. package/modules/status-ui/public/index.html +5 -31
  411. package/modules/status-ui/public/status.js +6 -174
  412. package/modules/status-ui/public/style.css +4 -133
  413. package/modules/ui-design/docs/design-contract.md +3 -3
  414. package/modules/ui-design/kit/README.md +1 -1
  415. package/modules/ui-design/kit/serve.js +13 -4
  416. package/modules/ui-design/scripts/design-sync.js +1 -1
  417. package/modules/ui-design/scripts/figma-design-sync.js +1 -1
  418. package/modules/ui-design/scripts/validate-design.js +1 -1
  419. package/package-lock.json +27 -11
  420. package/package.json +2 -3
  421. package/release-notes.json +186 -0
  422. package/scripts/bongos/CLAUDE.md +15 -0
  423. package/scripts/bongos/abandon.js +137 -0
  424. package/scripts/bongos/accessibility-replay.js +195 -0
  425. package/scripts/bongos/address-park-sweep.sh +91 -0
  426. package/scripts/bongos/adopt-detect.js +123 -0
  427. package/scripts/bongos/adopt-import.js +111 -0
  428. package/scripts/bongos/adopt-preflight.js +187 -0
  429. package/scripts/bongos/adr-namespace.js +273 -0
  430. package/scripts/bongos/agent-invoke.js +689 -0
  431. package/scripts/bongos/agents-sync.js +502 -0
  432. package/scripts/bongos/api-alias-caller-scan.js +256 -0
  433. package/scripts/bongos/api-artifacts.js +91 -0
  434. package/scripts/bongos/api.js +86 -0
  435. package/scripts/bongos/api.ps1 +26 -0
  436. package/scripts/bongos/architect-audit.js +294 -0
  437. package/scripts/bongos/artifact-audit.js +326 -0
  438. package/scripts/bongos/artifact-format.js +203 -0
  439. package/scripts/bongos/ask.js +231 -0
  440. package/scripts/bongos/audit-authorship.sh +324 -0
  441. package/scripts/bongos/audit-deps.js +189 -0
  442. package/scripts/bongos/audit-gate.js +121 -0
  443. package/scripts/bongos/audit-rank-roles.js +131 -0
  444. package/scripts/bongos/audit-replay.sh +205 -0
  445. package/scripts/bongos/autobongos-digest.js +123 -0
  446. package/scripts/bongos/autobongos-grade-cap.js +187 -0
  447. package/scripts/bongos/autobongos-loop.js +210 -0
  448. package/scripts/bongos/autobongos-run.js +1361 -0
  449. package/scripts/bongos/autobongos-service.cmd +50 -0
  450. package/scripts/bongos/autobongos-service.ps1 +270 -0
  451. package/scripts/bongos/autobongos-tweak-lane.js +262 -0
  452. package/scripts/bongos/autobongos-verify.js +618 -0
  453. package/scripts/bongos/autonomy-gate.js +135 -0
  454. package/scripts/bongos/autonomy-gauge-statusline.js +124 -0
  455. package/scripts/bongos/autonomy-gauge.js +84 -0
  456. package/scripts/bongos/backfill-cost.js +176 -0
  457. package/scripts/bongos/backfill-disciplines.js +144 -0
  458. package/scripts/bongos/backfill-session-earnings.js +123 -0
  459. package/scripts/bongos/backfill-session-types.js +87 -0
  460. package/scripts/bongos/backfill-sessions.js +411 -0
  461. package/scripts/bongos/backfill-subagent-rewards.js +485 -0
  462. package/scripts/bongos/backfill-task-rewards.js +413 -0
  463. package/scripts/bongos/backlog-review.js +399 -0
  464. package/scripts/bongos/backup-dropin-sweep.sh +95 -0
  465. package/scripts/bongos/backup.js +116 -0
  466. package/scripts/bongos/baseline-staleness.js +217 -0
  467. package/scripts/bongos/bfg-principal-token.js +65 -0
  468. package/scripts/bongos/bug-attachment-audit.js +149 -0
  469. package/scripts/bongos/bug-attachment-recover.js +198 -0
  470. package/scripts/bongos/bug-triage.js +292 -0
  471. package/scripts/bongos/build-bongos.js +94 -0
  472. package/scripts/bongos/bump-version.js +221 -0
  473. package/scripts/bongos/capture.js +481 -0
  474. package/scripts/bongos/category-advisory-guard.js +206 -0
  475. package/scripts/bongos/ci-grade.js +82 -0
  476. package/scripts/bongos/claim-recall.js +112 -0
  477. package/scripts/bongos/claim.js +789 -0
  478. package/scripts/bongos/claude-materialize.js +1200 -0
  479. package/scripts/bongos/claude-md-template.js +142 -0
  480. package/scripts/bongos/cleanup-orphans.js +74 -0
  481. package/scripts/bongos/cli-lib.js +1226 -0
  482. package/scripts/bongos/client-baseurl-guard.js +116 -0
  483. package/scripts/bongos/codemod-rename-scripts-gds.js +380 -0
  484. package/scripts/bongos/conductor-dispatch.js +535 -0
  485. package/scripts/bongos/context-pack.js +326 -0
  486. package/scripts/bongos/control-manifest.js +199 -0
  487. package/scripts/bongos/copy-apply.js +758 -0
  488. package/scripts/bongos/copy-inventory.js +571 -0
  489. package/scripts/bongos/core-origin-follow.js +185 -0
  490. package/scripts/bongos/core-pin-sync.js +260 -0
  491. package/scripts/bongos/cost-report.js +63 -0
  492. package/scripts/bongos/cost.js +47 -0
  493. package/scripts/bongos/credential-presence.js +296 -0
  494. package/scripts/bongos/cutover-codemod.js +240 -0
  495. package/scripts/bongos/db-backup-nightly.sh +232 -0
  496. package/scripts/bongos/db-backup-offsite.sh +267 -0
  497. package/scripts/bongos/demote.js +45 -0
  498. package/scripts/bongos/deploy-divergence.js +454 -0
  499. package/scripts/bongos/deploy-main-audit-gate.sh +97 -0
  500. package/scripts/bongos/design-sync.js +12 -0
  501. package/scripts/bongos/dev-box-guard.js +166 -0
  502. package/scripts/bongos/dev-lib.js +722 -0
  503. package/scripts/bongos/dev.js +645 -0
  504. package/scripts/bongos/device-flow-help.js +72 -0
  505. package/scripts/bongos/diagnose-achievements.js +118 -0
  506. package/scripts/bongos/diagram-facts.js +355 -0
  507. package/scripts/bongos/discord-channels.js +77 -0
  508. package/scripts/bongos/do-api.js +109 -0
  509. package/scripts/bongos/doc-cli-guard.js +274 -0
  510. package/scripts/bongos/doc-comment-coverage.js +139 -0
  511. package/scripts/bongos/doc-control-claims.js +170 -0
  512. package/scripts/bongos/docs-entropy.js +453 -0
  513. package/scripts/bongos/doctor.js +493 -0
  514. package/scripts/bongos/enroll-control-plane.js +101 -0
  515. package/scripts/bongos/exec-path-guard.js +564 -0
  516. package/scripts/bongos/feedback-send.js +249 -0
  517. package/scripts/bongos/fetch-art-key.js +106 -0
  518. package/scripts/bongos/fetch-sound-prefs.js +126 -0
  519. package/scripts/bongos/figma-design-sync.js +11 -0
  520. package/scripts/bongos/find-untested.js +371 -0
  521. package/scripts/bongos/fitness-checks-error-envelope.js +134 -0
  522. package/scripts/bongos/fitness-checks-identity.js +332 -0
  523. package/scripts/bongos/fitness-checks-packaging.js +132 -0
  524. package/scripts/bongos/fitness-checks-route-pins.js +76 -0
  525. package/scripts/bongos/fitness-checks-vocabulary.js +392 -0
  526. package/scripts/bongos/fitness-checks-write-validation.js +157 -0
  527. package/scripts/bongos/fitness-lib.js +127 -0
  528. package/scripts/bongos/fitness-migration-contraction.js +50 -0
  529. package/scripts/bongos/fitness-ratchets.js +594 -0
  530. package/scripts/bongos/fitness.js +1498 -0
  531. package/scripts/bongos/forwarder.js +80 -0
  532. package/scripts/bongos/gate-review.js +330 -0
  533. package/scripts/bongos/gating.js +348 -0
  534. package/scripts/{gds → bongos}/gds-db-backup.service +6 -6
  535. package/scripts/{gds → bongos}/gds-db-backup.timer +4 -4
  536. package/scripts/{gds → bongos}/gds-db-offsite.service +4 -4
  537. package/scripts/{gds → bongos}/gds-db-offsite.timer +4 -4
  538. package/scripts/bongos/gds-literal-scan.js +196 -0
  539. package/scripts/bongos/gen-api-client.js +576 -0
  540. package/scripts/bongos/gen-api-docs.js +1111 -0
  541. package/scripts/bongos/gen-atlas.js +366 -0
  542. package/scripts/bongos/gen-diagrams.js +608 -0
  543. package/scripts/bongos/gen-file-map.js +337 -0
  544. package/scripts/bongos/gen-freshness.js +32 -0
  545. package/scripts/bongos/gen-interaction-posture.js +122 -0
  546. package/scripts/bongos/gen-repo-map.js +540 -0
  547. package/scripts/bongos/gen-role-responsibilities.js +134 -0
  548. package/scripts/bongos/gen-session-index.js +243 -0
  549. package/scripts/bongos/gen-sounds.js +176 -0
  550. package/scripts/bongos/git-merge-regen.js +254 -0
  551. package/scripts/bongos/go-live.js +1071 -0
  552. package/scripts/bongos/goal.js +272 -0
  553. package/scripts/bongos/government-walk.js +1477 -0
  554. package/scripts/bongos/grade-correlation-audit.js +310 -0
  555. package/scripts/bongos/grade-replay.js +237 -0
  556. package/scripts/bongos/grade-smoke.js +205 -0
  557. package/scripts/bongos/home-isolation-guard.js +126 -0
  558. package/scripts/bongos/hooks/pre-push.js +211 -0
  559. package/scripts/bongos/howto.js +145 -0
  560. package/scripts/bongos/http-api-client.js +81 -0
  561. package/scripts/bongos/identity-strip.js +75 -0
  562. package/scripts/bongos/init.js +1159 -0
  563. package/scripts/bongos/install-git-hooks.js +200 -0
  564. package/scripts/bongos/instance-apex.js +38 -0
  565. package/scripts/bongos/intent-retry.js +52 -0
  566. package/scripts/bongos/key-card.js +222 -0
  567. package/scripts/bongos/key-status.js +126 -0
  568. package/scripts/bongos/land-watch.js +201 -0
  569. package/scripts/bongos/leak-scan-allowlist.js +293 -0
  570. package/scripts/bongos/learning-capture.js +145 -0
  571. package/scripts/bongos/lib.sh +74 -0
  572. package/scripts/bongos/linkify-refs.js +655 -0
  573. package/scripts/bongos/lint-infra-exec.js +70 -0
  574. package/scripts/bongos/lint-migrations.js +102 -0
  575. package/scripts/bongos/lint-workflow-exec.js +63 -0
  576. package/scripts/bongos/local-preview-lib.js +128 -0
  577. package/scripts/bongos/local-preview.js +82 -0
  578. package/scripts/bongos/login.js +360 -0
  579. package/scripts/bongos/main-audit.js +625 -0
  580. package/scripts/bongos/me.js +97 -0
  581. package/scripts/bongos/memory-map.js +176 -0
  582. package/scripts/bongos/merge-lock.js +71 -0
  583. package/scripts/bongos/migration-namespace.js +313 -0
  584. package/scripts/bongos/mirror-lineage-check.js +343 -0
  585. package/scripts/bongos/mirror-redact.js +350 -0
  586. package/scripts/bongos/module-artifact.js +289 -0
  587. package/scripts/bongos/module-assess-docs.js +238 -0
  588. package/scripts/bongos/module-assess-installs.js +228 -0
  589. package/scripts/bongos/module-assess-score.js +111 -0
  590. package/scripts/bongos/module-assess-security.js +252 -0
  591. package/scripts/bongos/module-assess-tests.js +278 -0
  592. package/scripts/bongos/module-assess-version.js +92 -0
  593. package/scripts/bongos/module-error-report.js +148 -0
  594. package/scripts/{gds → bongos}/module-test-runner/README.md +3 -3
  595. package/scripts/{gds → bongos}/module-test-runner/module-tests.yml +2 -2
  596. package/scripts/bongos/module-test-runner.js +414 -0
  597. package/scripts/bongos/module.js +1376 -0
  598. package/scripts/bongos/move-escalation.js +141 -0
  599. package/scripts/{gds → bongos}/newcomer-floor.config.json +1 -1
  600. package/scripts/bongos/newcomer-floor.js +421 -0
  601. package/scripts/{gds → bongos}/newcomer-floor.templates.json +13 -13
  602. package/scripts/bongos/newcomer-floor.test.js +235 -0
  603. package/scripts/bongos/next-number.js +163 -0
  604. package/scripts/bongos/oauth-secret.js +202 -0
  605. package/scripts/bongos/onboard.js +267 -0
  606. package/scripts/bongos/onboarding-analyze.js +226 -0
  607. package/scripts/bongos/onboarding-config.js +179 -0
  608. package/scripts/bongos/onboarding-ship.js +92 -0
  609. package/scripts/bongos/pack-env-prefix.js +73 -0
  610. package/scripts/bongos/package-core.js +1200 -0
  611. package/scripts/bongos/page-inventory.js +348 -0
  612. package/scripts/bongos/page-reader.js +722 -0
  613. package/scripts/bongos/paste-token.js +212 -0
  614. package/scripts/bongos/plain-cards.js +351 -0
  615. package/scripts/bongos/portability-smoke-art.sh +85 -0
  616. package/scripts/bongos/portability-smoke-memory.sh +176 -0
  617. package/scripts/bongos/preflight.js +214 -0
  618. package/scripts/bongos/probe-cache.js +246 -0
  619. package/scripts/bongos/prove-strict-versioning.js +284 -0
  620. package/scripts/bongos/provision-auto-upgrade.js +162 -0
  621. package/scripts/bongos/provision-byo-host.js +68 -0
  622. package/scripts/bongos/provision-capacity.js +79 -0
  623. package/scripts/bongos/provision-caps.js +63 -0
  624. package/scripts/bongos/provision-config.js +233 -0
  625. package/scripts/bongos/provision-core-upgrade.js +1166 -0
  626. package/scripts/bongos/provision-disconnect.js +88 -0
  627. package/scripts/bongos/provision-github-setup.js +168 -0
  628. package/scripts/bongos/provision-net.js +357 -0
  629. package/scripts/bongos/provision-pin-key.js +176 -0
  630. package/scripts/bongos/provision-pin-replay.js +126 -0
  631. package/scripts/bongos/provision-process.js +170 -0
  632. package/scripts/bongos/provision-render.js +201 -0
  633. package/scripts/bongos/provision-repo-auth.js +164 -0
  634. package/scripts/bongos/provision-repo-private.js +98 -0
  635. package/scripts/bongos/provision-repo.js +1337 -0
  636. package/scripts/bongos/provision-teardown.js +106 -0
  637. package/scripts/bongos/provision-units.js +469 -0
  638. package/scripts/bongos/provision.js +1479 -0
  639. package/scripts/bongos/publish-credential-check.js +66 -0
  640. package/scripts/bongos/publish-integration-gate.js +142 -0
  641. package/scripts/bongos/publish-manifest.js +440 -0
  642. package/scripts/bongos/publish-watch.js +268 -0
  643. package/scripts/bongos/pulse.js +338 -0
  644. package/scripts/bongos/push-main.js +56 -0
  645. package/scripts/bongos/push-path-brief.js +100 -0
  646. package/scripts/bongos/read-session-export.js +359 -0
  647. package/scripts/bongos/reauth.js +155 -0
  648. package/scripts/bongos/recall.js +183 -0
  649. package/scripts/bongos/redteam-patrol.js +437 -0
  650. package/scripts/bongos/regen-instance-docs.js +47 -0
  651. package/scripts/bongos/release-core.js +80 -0
  652. package/scripts/bongos/release-notes.js +271 -0
  653. package/scripts/bongos/release.js +139 -0
  654. package/scripts/bongos/rename-history-check.js +369 -0
  655. package/scripts/bongos/rename-ledger-prose.js +482 -0
  656. package/scripts/bongos/render-api.js +197 -0
  657. package/scripts/bongos/render-check.js +206 -0
  658. package/scripts/bongos/render-ideas.js +226 -0
  659. package/scripts/bongos/render-payload.js +85 -0
  660. package/scripts/bongos/repo-metrics.js +363 -0
  661. package/scripts/bongos/review-sheet.js +108 -0
  662. package/scripts/bongos/role-pack-guard.js +142 -0
  663. package/scripts/bongos/route-shadow-guard.js +175 -0
  664. package/scripts/bongos/routine-schedule.js +318 -0
  665. package/scripts/bongos/run-routine.js +257 -0
  666. package/scripts/bongos/run-unit-tests.js +654 -0
  667. package/scripts/bongos/runner-drift.js +151 -0
  668. package/scripts/bongos/sandbox-stage.js +459 -0
  669. package/scripts/bongos/scope-map-guard.js +300 -0
  670. package/scripts/bongos/scope-roster-guard.js +118 -0
  671. package/scripts/bongos/seam-wiring-guard.js +275 -0
  672. package/scripts/bongos/search-ingest-db.js +288 -0
  673. package/scripts/bongos/search-ingest-md.js +269 -0
  674. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +152 -0
  675. package/scripts/bongos/seed-bv1-profiles-tasks.js +135 -0
  676. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +390 -0
  677. package/scripts/bongos/seed-staging.sh +117 -0
  678. package/scripts/bongos/seed-verify-criteria.js +124 -0
  679. package/scripts/bongos/sequence.js +597 -0
  680. package/scripts/bongos/session-cost.js +430 -0
  681. package/scripts/bongos/session-digest-build.js +293 -0
  682. package/scripts/bongos/session-digest.js +200 -0
  683. package/scripts/bongos/session-eval.js +631 -0
  684. package/scripts/bongos/session-transcript-build.js +155 -0
  685. package/scripts/bongos/session-transcript-upload.js +29 -0
  686. package/scripts/bongos/session.js +404 -0
  687. package/scripts/bongos/setup.js +646 -0
  688. package/scripts/bongos/ship-check.js +89 -0
  689. package/scripts/bongos/ship-deploy-target.js +393 -0
  690. package/scripts/bongos/ship-escalation.js +167 -0
  691. package/scripts/bongos/ship-finish.js +420 -0
  692. package/scripts/bongos/ship-flow.js +1217 -0
  693. package/scripts/bongos/ship-git.js +100 -0
  694. package/scripts/bongos/ship-grade-steps.js +547 -0
  695. package/scripts/bongos/ship-grade.js +426 -0
  696. package/scripts/bongos/ship-honesty.js +314 -0
  697. package/scripts/bongos/ship-io.js +139 -0
  698. package/scripts/bongos/ship-land.js +1130 -0
  699. package/scripts/bongos/ship-memory-sync.js +249 -0
  700. package/scripts/bongos/ship-merge-gate.js +225 -0
  701. package/scripts/bongos/ship-merge.js +466 -0
  702. package/scripts/bongos/ship-mode-ask.js +138 -0
  703. package/scripts/bongos/ship-mode-manual.js +54 -0
  704. package/scripts/bongos/ship-preflight-steps.js +399 -0
  705. package/scripts/bongos/ship-regen.js +291 -0
  706. package/scripts/bongos/ship-resume.js +201 -0
  707. package/scripts/bongos/ship-session-upload.js +313 -0
  708. package/scripts/bongos/ship-state.js +110 -0
  709. package/scripts/bongos/ship-surprise.js +84 -0
  710. package/scripts/bongos/ship-visual.js +75 -0
  711. package/scripts/bongos/ship.js +382 -0
  712. package/scripts/bongos/shipping-mode.js +71 -0
  713. package/scripts/bongos/skill-lint.js +250 -0
  714. package/scripts/bongos/skill-preflight.js +361 -0
  715. package/scripts/bongos/smoke-batch-claim.sh +238 -0
  716. package/scripts/bongos/smoke-batch-release.sh +316 -0
  717. package/scripts/bongos/smoke-claim-consistency.sh +222 -0
  718. package/scripts/bongos/smoke-dependencies.sh +143 -0
  719. package/scripts/bongos/smoke-gds.js +256 -0
  720. package/scripts/bongos/smoke-write.sh +275 -0
  721. package/scripts/bongos/source-text-test-scan.js +106 -0
  722. package/scripts/bongos/spark.js +294 -0
  723. package/scripts/bongos/stale-worktrees.js +212 -0
  724. package/scripts/bongos/start-goals.js +102 -0
  725. package/scripts/bongos/start.js +1090 -0
  726. package/scripts/bongos/status.js +545 -0
  727. package/scripts/bongos/strand-watch.js +315 -0
  728. package/scripts/bongos/stranded-rows.js +200 -0
  729. package/scripts/bongos/stray-unit-sweep.sh +71 -0
  730. package/scripts/bongos/surface.js +101 -0
  731. package/scripts/bongos/task.js +273 -0
  732. package/scripts/bongos/test-path-guard.js +99 -0
  733. package/scripts/bongos/triage.js +742 -0
  734. package/scripts/bongos/turn-end-sound.js +74 -0
  735. package/scripts/bongos/tweak-halt.js +70 -0
  736. package/scripts/bongos/tweak-renders.js +200 -0
  737. package/scripts/bongos/uat.js +120 -0
  738. package/scripts/bongos/ui-kit-transform.js +43 -0
  739. package/scripts/bongos/update-channel.js +512 -0
  740. package/scripts/bongos/update-sweep.js +829 -0
  741. package/scripts/bongos/upgrade-artist-gate.js +46 -0
  742. package/scripts/bongos/upgrade-commit-identity.js +32 -0
  743. package/scripts/bongos/upgrade-migrate.js +224 -0
  744. package/scripts/bongos/upgrade-operator.js +73 -0
  745. package/scripts/bongos/upgrade-outcome.js +174 -0
  746. package/scripts/bongos/upgrade.js +1499 -0
  747. package/scripts/bongos/validate-design.js +12 -0
  748. package/scripts/bongos/version-close.js +621 -0
  749. package/scripts/bongos/version-literal-guard.js +140 -0
  750. package/scripts/bongos/version-select.js +79 -0
  751. package/scripts/bongos/visual-review-nudge.js +39 -0
  752. package/scripts/bongos/wedge-remedy.js +174 -0
  753. package/scripts/bongos/worktree-claim-guard.js +130 -0
  754. package/scripts/bongos/worktree.js +449 -0
  755. package/scripts/discord/post.js +1 -1
  756. package/scripts/gds/README.md +9 -0
  757. package/scripts/gds/abandon.js +6 -136
  758. package/scripts/gds/accessibility-replay.js +5 -194
  759. package/scripts/gds/address-park-sweep.sh +7 -90
  760. package/scripts/gds/adopt-detect.js +5 -121
  761. package/scripts/gds/adopt-import.js +5 -109
  762. package/scripts/gds/adopt-preflight.js +5 -185
  763. package/scripts/gds/adr-namespace.js +5 -271
  764. package/scripts/gds/agent-invoke.js +5 -687
  765. package/scripts/gds/agents-sync.js +5 -500
  766. package/scripts/gds/api-alias-caller-scan.js +5 -255
  767. package/scripts/gds/api-artifacts.js +6 -0
  768. package/scripts/gds/api.js +6 -85
  769. package/scripts/gds/api.ps1 +8 -26
  770. package/scripts/gds/architect-audit.js +6 -293
  771. package/scripts/gds/artifact-audit.js +5 -324
  772. package/scripts/gds/artifact-format.js +6 -203
  773. package/scripts/gds/ask.js +5 -229
  774. package/scripts/gds/audit-authorship.sh +7 -328
  775. package/scripts/gds/audit-deps.js +6 -188
  776. package/scripts/gds/audit-gate.js +5 -119
  777. package/scripts/gds/audit-rank-roles.js +6 -171
  778. package/scripts/gds/audit-replay.sh +7 -204
  779. package/scripts/gds/autobongos-digest.js +5 -121
  780. package/scripts/gds/autobongos-grade-cap.js +5 -186
  781. package/scripts/gds/autobongos-loop.js +5 -208
  782. package/scripts/gds/autobongos-run.js +5 -1359
  783. package/scripts/gds/autobongos-service.cmd +8 -49
  784. package/scripts/gds/autobongos-service.ps1 +8 -270
  785. package/scripts/gds/autobongos-tweak-lane.js +5 -261
  786. package/scripts/gds/autobongos-verify.js +5 -616
  787. package/scripts/gds/autonomy-gate.js +5 -133
  788. package/scripts/gds/autonomy-gauge-statusline.js +5 -122
  789. package/scripts/gds/autonomy-gauge.js +5 -82
  790. package/scripts/gds/backfill-cost.js +6 -175
  791. package/scripts/gds/backfill-disciplines.js +5 -142
  792. package/scripts/gds/backfill-session-earnings.js +5 -121
  793. package/scripts/gds/backfill-session-types.js +5 -85
  794. package/scripts/gds/backfill-sessions.js +5 -409
  795. package/scripts/gds/backfill-subagent-rewards.js +5 -483
  796. package/scripts/gds/backfill-task-rewards.js +5 -411
  797. package/scripts/gds/backlog-review.js +6 -398
  798. package/scripts/gds/backup-dropin-sweep.sh +7 -94
  799. package/scripts/gds/backup.js +6 -115
  800. package/scripts/gds/baseline-staleness.js +5 -215
  801. package/scripts/gds/bfg-principal-token.js +6 -64
  802. package/scripts/gds/bug-attachment-audit.js +5 -147
  803. package/scripts/gds/bug-attachment-recover.js +5 -196
  804. package/scripts/gds/bug-triage.js +6 -291
  805. package/scripts/gds/build-bongos.js +5 -92
  806. package/scripts/gds/bump-version.js +5 -219
  807. package/scripts/gds/capture.js +6 -480
  808. package/scripts/gds/category-advisory-guard.js +5 -205
  809. package/scripts/gds/ci-grade.js +6 -81
  810. package/scripts/gds/claim-recall.js +6 -111
  811. package/scripts/gds/claim.js +6 -788
  812. package/scripts/gds/claude-materialize.js +5 -1198
  813. package/scripts/gds/claude-md-template.js +5 -140
  814. package/scripts/gds/cleanup-orphans.js +6 -73
  815. package/scripts/gds/cli-lib.js +6 -1204
  816. package/scripts/gds/client-baseurl-guard.js +5 -114
  817. package/scripts/gds/conductor-dispatch.js +6 -534
  818. package/scripts/gds/context-pack.js +6 -325
  819. package/scripts/gds/control-manifest.js +5 -197
  820. package/scripts/gds/copy-apply.js +5 -756
  821. package/scripts/gds/copy-inventory.js +5 -569
  822. package/scripts/gds/core-origin-follow.js +5 -184
  823. package/scripts/gds/core-pin-sync.js +5 -258
  824. package/scripts/gds/cost-report.js +6 -62
  825. package/scripts/gds/cost.js +6 -46
  826. package/scripts/gds/credential-presence.js +5 -294
  827. package/scripts/gds/cutover-codemod.js +5 -239
  828. package/scripts/gds/db-backup-nightly.sh +7 -231
  829. package/scripts/gds/db-backup-offsite.sh +7 -266
  830. package/scripts/gds/demote.js +6 -44
  831. package/scripts/gds/deploy-divergence.js +6 -445
  832. package/scripts/gds/deploy-main-audit-gate.sh +7 -96
  833. package/scripts/gds/design-sync.js +5 -10
  834. package/scripts/gds/dev-box-guard.js +5 -164
  835. package/scripts/gds/dev-lib.js +5 -721
  836. package/scripts/gds/dev.js +5 -643
  837. package/scripts/gds/device-flow-help.js +5 -70
  838. package/scripts/gds/diagnose-achievements.js +5 -116
  839. package/scripts/gds/diagram-facts.js +6 -355
  840. package/scripts/gds/discord-channels.js +5 -75
  841. package/scripts/gds/do-api.js +6 -109
  842. package/scripts/gds/doc-cli-guard.js +5 -266
  843. package/scripts/gds/doc-comment-coverage.js +5 -137
  844. package/scripts/gds/doc-control-claims.js +5 -166
  845. package/scripts/gds/docs-entropy.js +5 -443
  846. package/scripts/gds/doctor.js +6 -492
  847. package/scripts/gds/enroll-control-plane.js +5 -99
  848. package/scripts/gds/exec-path-guard.js +5 -562
  849. package/scripts/gds/feedback-send.js +5 -247
  850. package/scripts/gds/fetch-art-key.js +5 -104
  851. package/scripts/gds/fetch-sound-prefs.js +5 -124
  852. package/scripts/gds/figma-design-sync.js +5 -9
  853. package/scripts/gds/find-untested.js +5 -369
  854. package/scripts/gds/fitness-checks-error-envelope.js +5 -133
  855. package/scripts/gds/fitness-checks-identity.js +5 -342
  856. package/scripts/gds/fitness-checks-packaging.js +5 -131
  857. package/scripts/gds/fitness-checks-route-pins.js +5 -75
  858. package/scripts/gds/fitness-checks-vocabulary.js +5 -391
  859. package/scripts/gds/fitness-checks-write-validation.js +5 -156
  860. package/scripts/gds/fitness-lib.js +5 -126
  861. package/scripts/gds/fitness-migration-contraction.js +5 -49
  862. package/scripts/gds/fitness-ratchets.js +5 -563
  863. package/scripts/gds/fitness.js +5 -1493
  864. package/scripts/gds/gate-review.js +5 -328
  865. package/scripts/gds/gating.js +5 -346
  866. package/scripts/gds/gds-literal-scan.js +5 -194
  867. package/scripts/gds/gen-api-client.js +5 -555
  868. package/scripts/gds/gen-api-docs.js +5 -1100
  869. package/scripts/gds/gen-atlas.js +5 -364
  870. package/scripts/gds/gen-diagrams.js +6 -607
  871. package/scripts/gds/gen-file-map.js +5 -335
  872. package/scripts/gds/gen-freshness.js +5 -31
  873. package/scripts/gds/gen-interaction-posture.js +5 -120
  874. package/scripts/gds/gen-repo-map.js +5 -538
  875. package/scripts/gds/gen-role-responsibilities.js +5 -133
  876. package/scripts/gds/gen-session-index.js +5 -241
  877. package/scripts/gds/gen-sounds.js +5 -174
  878. package/scripts/gds/git-merge-regen.js +6 -291
  879. package/scripts/gds/go-live.js +5 -1069
  880. package/scripts/gds/goal.js +6 -271
  881. package/scripts/gds/government-walk.js +5 -1475
  882. package/scripts/gds/grade-correlation-audit.js +5 -308
  883. package/scripts/gds/grade-replay.js +5 -235
  884. package/scripts/gds/grade-smoke.js +6 -204
  885. package/scripts/gds/home-isolation-guard.js +5 -125
  886. package/scripts/gds/hooks/pre-push.js +5 -209
  887. package/scripts/gds/howto.js +5 -143
  888. package/scripts/gds/http-api-client.js +6 -81
  889. package/scripts/gds/identity-strip.js +5 -74
  890. package/scripts/gds/init.js +5 -1157
  891. package/scripts/gds/install-git-hooks.js +5 -198
  892. package/scripts/gds/instance-apex.js +5 -37
  893. package/scripts/gds/intent-retry.js +5 -51
  894. package/scripts/gds/key-card.js +6 -221
  895. package/scripts/gds/key-status.js +5 -124
  896. package/scripts/gds/land-watch.js +5 -199
  897. package/scripts/gds/leak-scan-allowlist.js +5 -291
  898. package/scripts/gds/learning-capture.js +6 -144
  899. package/scripts/gds/lib.sh +7 -73
  900. package/scripts/gds/linkify-refs.js +6 -654
  901. package/scripts/gds/lint-infra-exec.js +5 -68
  902. package/scripts/gds/lint-migrations.js +5 -100
  903. package/scripts/gds/lint-workflow-exec.js +5 -61
  904. package/scripts/gds/local-preview-lib.js +6 -128
  905. package/scripts/gds/local-preview.js +6 -81
  906. package/scripts/gds/login.js +5 -358
  907. package/scripts/gds/main-audit.js +5 -623
  908. package/scripts/gds/me.js +6 -96
  909. package/scripts/gds/memory-map.js +5 -174
  910. package/scripts/gds/merge-lock.js +6 -70
  911. package/scripts/gds/migration-namespace.js +5 -311
  912. package/scripts/gds/mirror-lineage-check.js +5 -341
  913. package/scripts/gds/mirror-redact.js +5 -348
  914. package/scripts/gds/module-artifact.js +6 -289
  915. package/scripts/gds/module-assess-docs.js +6 -237
  916. package/scripts/gds/module-assess-installs.js +6 -227
  917. package/scripts/gds/module-assess-score.js +6 -110
  918. package/scripts/gds/module-assess-security.js +6 -251
  919. package/scripts/gds/module-assess-tests.js +6 -277
  920. package/scripts/gds/module-assess-version.js +6 -91
  921. package/scripts/gds/module-error-report.js +5 -146
  922. package/scripts/gds/module-test-runner.js +6 -413
  923. package/scripts/gds/module.js +5 -1374
  924. package/scripts/gds/move-escalation.js +5 -140
  925. package/scripts/gds/newcomer-floor.js +6 -420
  926. package/scripts/gds/newcomer-floor.test.js +6 -234
  927. package/scripts/gds/next-number.js +6 -162
  928. package/scripts/gds/oauth-secret.js +5 -200
  929. package/scripts/gds/onboard.js +5 -265
  930. package/scripts/gds/onboarding-analyze.js +6 -225
  931. package/scripts/gds/onboarding-config.js +6 -179
  932. package/scripts/gds/onboarding-ship.js +6 -91
  933. package/scripts/gds/pack-env-prefix.js +5 -72
  934. package/scripts/gds/package-core.js +5 -1102
  935. package/scripts/gds/page-inventory.js +5 -346
  936. package/scripts/gds/page-reader.js +5 -720
  937. package/scripts/gds/paste-token.js +6 -211
  938. package/scripts/gds/plain-cards.js +5 -350
  939. package/scripts/gds/portability-smoke-art.sh +7 -84
  940. package/scripts/gds/portability-smoke-memory.sh +7 -175
  941. package/scripts/gds/preflight.js +5 -212
  942. package/scripts/gds/probe-cache.js +5 -244
  943. package/scripts/gds/prove-strict-versioning.js +6 -283
  944. package/scripts/gds/provision-auto-upgrade.js +5 -161
  945. package/scripts/gds/provision-byo-host.js +5 -67
  946. package/scripts/gds/provision-capacity.js +5 -78
  947. package/scripts/gds/provision-caps.js +5 -62
  948. package/scripts/gds/provision-config.js +5 -232
  949. package/scripts/gds/provision-core-upgrade.js +5 -1165
  950. package/scripts/gds/provision-disconnect.js +5 -87
  951. package/scripts/gds/provision-github-setup.js +5 -167
  952. package/scripts/gds/provision-net.js +5 -356
  953. package/scripts/gds/provision-pin-key.js +5 -175
  954. package/scripts/gds/provision-pin-replay.js +5 -125
  955. package/scripts/gds/provision-process.js +5 -169
  956. package/scripts/gds/provision-render.js +5 -200
  957. package/scripts/gds/provision-repo-auth.js +5 -163
  958. package/scripts/gds/provision-repo-private.js +5 -97
  959. package/scripts/gds/provision-repo.js +5 -1336
  960. package/scripts/gds/provision-teardown.js +5 -105
  961. package/scripts/gds/provision-units.js +5 -468
  962. package/scripts/gds/provision.js +5 -1477
  963. package/scripts/gds/publish-credential-check.js +6 -65
  964. package/scripts/gds/publish-integration-gate.js +5 -140
  965. package/scripts/gds/publish-manifest.js +5 -437
  966. package/scripts/gds/publish-watch.js +5 -266
  967. package/scripts/gds/pulse.js +6 -335
  968. package/scripts/gds/push-main.js +5 -54
  969. package/scripts/gds/push-path-brief.js +5 -98
  970. package/scripts/gds/read-session-export.js +5 -357
  971. package/scripts/gds/reauth.js +6 -154
  972. package/scripts/gds/recall.js +6 -182
  973. package/scripts/gds/redteam-patrol.js +5 -435
  974. package/scripts/gds/regen-instance-docs.js +5 -45
  975. package/scripts/gds/release-core.js +5 -78
  976. package/scripts/gds/release-notes.js +5 -270
  977. package/scripts/gds/release.js +6 -138
  978. package/scripts/gds/rename-history-check.js +5 -367
  979. package/scripts/gds/rename-ledger-prose.js +5 -480
  980. package/scripts/gds/render-api.js +5 -196
  981. package/scripts/gds/render-check.js +5 -204
  982. package/scripts/gds/render-ideas.js +6 -225
  983. package/scripts/gds/render-payload.js +5 -84
  984. package/scripts/gds/repo-metrics.js +6 -362
  985. package/scripts/gds/review-sheet.js +5 -106
  986. package/scripts/gds/role-pack-guard.js +5 -141
  987. package/scripts/gds/route-shadow-guard.js +5 -173
  988. package/scripts/gds/routine-schedule.js +5 -281
  989. package/scripts/gds/run-routine.js +5 -255
  990. package/scripts/gds/run-unit-tests.js +6 -659
  991. package/scripts/gds/runner-drift.js +5 -149
  992. package/scripts/gds/sandbox-stage.js +6 -458
  993. package/scripts/gds/scope-map-guard.js +5 -299
  994. package/scripts/gds/scope-roster-guard.js +5 -117
  995. package/scripts/gds/seam-wiring-guard.js +5 -274
  996. package/scripts/gds/search-ingest-db.js +6 -287
  997. package/scripts/gds/search-ingest-md.js +6 -268
  998. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +6 -152
  999. package/scripts/gds/seed-bv1-profiles-tasks.js +6 -135
  1000. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +6 -390
  1001. package/scripts/gds/seed-staging.sh +7 -116
  1002. package/scripts/gds/seed-verify-criteria.js +6 -124
  1003. package/scripts/gds/sequence.js +6 -596
  1004. package/scripts/gds/session-cost.js +6 -429
  1005. package/scripts/gds/session-digest-build.js +6 -293
  1006. package/scripts/gds/session-digest.js +6 -199
  1007. package/scripts/gds/session-eval.js +6 -630
  1008. package/scripts/gds/session-transcript-build.js +6 -155
  1009. package/scripts/gds/session-transcript-upload.js +6 -29
  1010. package/scripts/gds/session.js +6 -403
  1011. package/scripts/gds/setup.js +6 -645
  1012. package/scripts/gds/ship-check.js +5 -85
  1013. package/scripts/gds/ship-deploy-target.js +5 -392
  1014. package/scripts/gds/ship-escalation.js +5 -166
  1015. package/scripts/gds/ship-finish.js +5 -419
  1016. package/scripts/gds/ship-flow.js +5 -1216
  1017. package/scripts/gds/ship-git.js +5 -99
  1018. package/scripts/gds/ship-grade-steps.js +5 -546
  1019. package/scripts/gds/ship-grade.js +5 -425
  1020. package/scripts/gds/ship-honesty.js +5 -312
  1021. package/scripts/gds/ship-io.js +5 -138
  1022. package/scripts/gds/ship-land.js +5 -1129
  1023. package/scripts/gds/ship-memory-sync.js +5 -248
  1024. package/scripts/gds/ship-merge-gate.js +5 -224
  1025. package/scripts/gds/ship-merge.js +5 -465
  1026. package/scripts/gds/ship-mode-ask.js +5 -137
  1027. package/scripts/gds/ship-mode-manual.js +5 -53
  1028. package/scripts/gds/ship-preflight-steps.js +5 -398
  1029. package/scripts/gds/ship-regen.js +5 -313
  1030. package/scripts/gds/ship-resume.js +5 -200
  1031. package/scripts/gds/ship-session-upload.js +5 -312
  1032. package/scripts/gds/ship-state.js +5 -109
  1033. package/scripts/gds/ship-surprise.js +5 -83
  1034. package/scripts/gds/ship-visual.js +5 -74
  1035. package/scripts/gds/ship.js +5 -380
  1036. package/scripts/gds/shipping-mode.js +5 -69
  1037. package/scripts/gds/skill-lint.js +5 -248
  1038. package/scripts/gds/skill-preflight.js +5 -359
  1039. package/scripts/gds/smoke-batch-claim.sh +7 -237
  1040. package/scripts/gds/smoke-batch-release.sh +7 -315
  1041. package/scripts/gds/smoke-claim-consistency.sh +7 -221
  1042. package/scripts/gds/smoke-dependencies.sh +7 -142
  1043. package/scripts/gds/smoke-gds.js +6 -255
  1044. package/scripts/gds/smoke-write.sh +7 -274
  1045. package/scripts/gds/source-text-test-scan.js +7 -0
  1046. package/scripts/gds/spark.js +6 -293
  1047. package/scripts/gds/stale-worktrees.js +5 -210
  1048. package/scripts/gds/start-goals.js +5 -101
  1049. package/scripts/gds/start.js +6 -1089
  1050. package/scripts/gds/status.js +6 -544
  1051. package/scripts/gds/strand-watch.js +5 -313
  1052. package/scripts/gds/stranded-rows.js +5 -198
  1053. package/scripts/gds/stray-unit-sweep.sh +7 -70
  1054. package/scripts/gds/surface.js +5 -100
  1055. package/scripts/gds/task.js +6 -272
  1056. package/scripts/gds/test-path-guard.js +5 -97
  1057. package/scripts/gds/triage.js +6 -741
  1058. package/scripts/gds/turn-end-sound.js +5 -72
  1059. package/scripts/gds/tweak-halt.js +5 -68
  1060. package/scripts/gds/tweak-renders.js +5 -198
  1061. package/scripts/gds/uat.js +6 -119
  1062. package/scripts/gds/ui-kit-transform.js +5 -41
  1063. package/scripts/gds/update-channel.js +5 -510
  1064. package/scripts/gds/update-sweep.js +5 -827
  1065. package/scripts/gds/upgrade-artist-gate.js +5 -45
  1066. package/scripts/gds/upgrade-commit-identity.js +5 -31
  1067. package/scripts/gds/upgrade-migrate.js +5 -223
  1068. package/scripts/gds/upgrade-operator.js +6 -73
  1069. package/scripts/gds/upgrade-outcome.js +5 -173
  1070. package/scripts/gds/upgrade.js +5 -1497
  1071. package/scripts/gds/validate-design.js +5 -10
  1072. package/scripts/gds/version-close.js +6 -620
  1073. package/scripts/gds/version-literal-guard.js +5 -138
  1074. package/scripts/gds/version-select.js +5 -77
  1075. package/scripts/gds/visual-review-nudge.js +5 -38
  1076. package/scripts/gds/wedge-remedy.js +5 -173
  1077. package/scripts/gds/worktree-claim-guard.js +5 -129
  1078. package/scripts/gds/worktree.js +6 -448
  1079. package/scripts/hall-preview/refresh-fixtures.js +1 -1
  1080. package/scripts/hall-preview/server.js +2 -2
  1081. package/scripts/migrate.sh +3 -3
  1082. package/scripts/public-mirror-export.js +3 -3
  1083. package/scripts/security/dep-audit.js +50 -124
  1084. package/scripts/setup-builder.js +7 -7
  1085. package/src/bongos/api-errors.js +1 -0
  1086. package/src/bongos/api-prefix.js +1 -1
  1087. package/src/bongos/auth-github.js +19 -3
  1088. package/src/bongos/auth-idp.js +7 -0
  1089. package/src/bongos/claim-attribution.js +2 -2
  1090. package/src/bongos/core-moves.js +1 -1
  1091. package/src/bongos/core-update.js +3 -3
  1092. package/src/bongos/craft-mode.js +2 -2
  1093. package/src/bongos/db-kernel.js +15 -3
  1094. package/src/bongos/db.js +30 -4
  1095. package/src/bongos/github-repos.js +1 -1
  1096. package/src/bongos/llm-cache.js +1 -1
  1097. package/src/bongos/llm-pricing.js +1 -1
  1098. package/src/bongos/metrics.js +1 -1
  1099. package/src/bongos/middleware/rate-limit.js +2 -2
  1100. package/src/bongos/module-listing.js +93 -1
  1101. package/src/bongos/module-scope-map.js +90 -85
  1102. package/src/bongos/module-store.js +1 -1
  1103. package/src/bongos/module-submissions.js +1 -1
  1104. package/src/bongos/optimizer.js +1 -1
  1105. package/src/bongos/package-registry.js +1 -1
  1106. package/src/bongos/path-match.js +1 -1
  1107. package/src/bongos/permission-path-check.js +3 -3
  1108. package/src/bongos/platform-visibility-gate.js +3 -5
  1109. package/src/bongos/prelaunch-gate.js +7 -6
  1110. package/src/bongos/route-rank-check.js +2 -2
  1111. package/src/bongos/routes/auth.js +15 -2
  1112. package/src/bongos/routes/backup.js +1 -1
  1113. package/src/bongos/routes/builders.js +29 -1
  1114. package/src/bongos/routes/me.js +49 -18
  1115. package/src/bongos/routes/modules.js +29 -9
  1116. package/src/bongos/routes/public.js +3 -72
  1117. package/src/bongos/schema-lag.js +1 -1
  1118. package/src/bongos/sensitive-surfaces.js +1 -1
  1119. package/src/bongos/serve-internal.js +65 -16
  1120. package/src/bongos/software-update.js +2 -2
  1121. package/src/bongos/upgrade-hold.js +3 -3
  1122. package/src/hall-look.js +6 -5
  1123. package/src/module-api.js +3 -3
  1124. package/src/module-loader/catalog.js +1 -1
  1125. package/src/module-loader/error-tally.js +3 -3
  1126. package/src/module-loader/manifest-schema.js +3 -3
  1127. package/src/platform-server.js +10 -0
  1128. package/src/role-responsibilities.js +1 -1
  1129. package/src/stale-timer.js +2 -2
  1130. package/tests/accessibility_replay.mjs +1 -1
  1131. package/tests/activity_rollup_order_db.mjs +1 -1
  1132. package/tests/activity_snapshots_db.mjs +1 -1
  1133. package/tests/adopt_detect.mjs +1 -1
  1134. package/tests/adopt_import.mjs +1 -1
  1135. package/tests/adopt_preflight.mjs +1 -1
  1136. package/tests/adr_0089_scope.mjs +10 -7
  1137. package/tests/adr_0162_citation.mjs +1 -1
  1138. package/tests/adr_0162_gate_citation.mjs +1 -1
  1139. package/tests/agent_invoke.mjs +1 -1
  1140. package/tests/agent_invoke_live.mjs +3 -3
  1141. package/tests/agents_cron_trigger.mjs +2 -2
  1142. package/tests/agents_roster.mjs +1 -1
  1143. package/tests/agents_sync.mjs +4 -4
  1144. package/tests/allow_rule_guard.mjs +20 -20
  1145. package/tests/api_alias_caller_ratchet.mjs +9 -10
  1146. package/tests/api_alias_exceptions.mjs +7 -6
  1147. package/tests/api_artifacts.mjs +214 -0
  1148. package/tests/api_cli_flags.mjs +3 -3
  1149. package/tests/api_client.mjs +56 -54
  1150. package/tests/api_docs.mjs +30 -9
  1151. package/tests/api_error_envelope_buildcheck.mjs +1 -1
  1152. package/tests/api_msys_path_repair.mjs +2 -2
  1153. package/tests/architect.mjs +1 -1
  1154. package/tests/art_stage.mjs +2 -2
  1155. package/tests/artifact_audit.mjs +2 -2
  1156. package/tests/atlas.mjs +1 -1
  1157. package/tests/audit_deps.mjs +1 -1
  1158. package/tests/audit_gate.mjs +1 -1
  1159. package/tests/auth_flow_slice1.mjs +3 -3
  1160. package/tests/auth_inactive_builder.mjs +43 -5
  1161. package/tests/auth_session_expiry_db.mjs +16 -4
  1162. package/tests/auth_session_ttl.mjs +23 -0
  1163. package/tests/authorship_audit.mjs +2 -2
  1164. package/tests/autobongos_cadence.mjs +1 -1
  1165. package/tests/autobongos_fence.mjs +1 -1
  1166. package/tests/autobongos_grade_cap.mjs +2 -2
  1167. package/tests/autobongos_heartbeat_head.mjs +1 -1
  1168. package/tests/autobongos_host_access.mjs +3 -3
  1169. package/tests/autobongos_loop.mjs +17 -17
  1170. package/tests/autobongos_service_ps1.mjs +3 -3
  1171. package/tests/autobongos_tweak_lane.mjs +2 -2
  1172. package/tests/autobongos_verify.mjs +2 -2
  1173. package/tests/autonomous_fit_protected_paths.mjs +3 -3
  1174. package/tests/autonomy_config_idle.mjs +1 -1
  1175. package/tests/autonomy_daily_checks.mjs +201 -0
  1176. package/tests/autonomy_digest.mjs +1 -1
  1177. package/tests/autonomy_gate.mjs +1 -1
  1178. package/tests/autonomy_gauge.mjs +2 -2
  1179. package/tests/autonomy_runner_identity.mjs +1 -1
  1180. package/tests/backfill_sessions.mjs +8 -8
  1181. package/tests/backfill_subagent_rewards.mjs +1 -1
  1182. package/tests/backfill_task_rewards.mjs +2 -2
  1183. package/tests/backlog_review.mjs +3 -3
  1184. package/tests/baseline_staleness.mjs +2 -2
  1185. package/tests/bfg.mjs +11 -5
  1186. package/tests/bfg_write.mjs +20 -11
  1187. package/tests/blockers_hall.mjs +14 -4
  1188. package/tests/blockers_page_ui.mjs +56 -12
  1189. package/tests/bongos_dispatch.mjs +5 -5
  1190. package/tests/bongos_login.mjs +2 -2
  1191. package/tests/bug_attachment_audit.mjs +1 -1
  1192. package/tests/bug_attachment_recover.mjs +2 -2
  1193. package/tests/bug_triage.mjs +3 -3
  1194. package/tests/build_info.mjs +1 -1
  1195. package/tests/builder_tester_tag.mjs +184 -0
  1196. package/tests/bump_version.mjs +3 -3
  1197. package/tests/canonical_profile_url.mjs +1 -1
  1198. package/tests/capture_error_format.mjs +1 -1
  1199. package/tests/capture_goal_routing.mjs +2 -2
  1200. package/tests/category_advisory_guard.mjs +3 -3
  1201. package/tests/claim_error_surface.mjs +2 -2
  1202. package/tests/claim_from_task_and_home.mjs +1 -1
  1203. package/tests/claim_gate_open_blocker.mjs +7 -4
  1204. package/tests/claim_gate_rebase.mjs +12 -8
  1205. package/tests/claim_recall.mjs +4 -4
  1206. package/tests/claim_resolve_worktree_lock.mjs +1 -1
  1207. package/tests/claude_materialize.mjs +68 -68
  1208. package/tests/clawback_confirmed_backlog.mjs +8 -5
  1209. package/tests/cli_allowlist_permissions.mjs +11 -11
  1210. package/tests/cli_dispatcher.mjs +2 -2
  1211. package/tests/cli_err_code.mjs +6 -6
  1212. package/tests/cli_exit_no_abort.mjs +7 -7
  1213. package/tests/cli_lib_formatters.mjs +2 -2
  1214. package/tests/cli_sessions.mjs +5 -5
  1215. package/tests/cli_surface.mjs +15 -15
  1216. package/tests/cli_token_reissue.mjs +2 -2
  1217. package/tests/client_baseurl_guard.mjs +1 -1
  1218. package/tests/client_bundle_route.mjs +35 -4
  1219. package/tests/cloud_bongos_pack.mjs +1 -1
  1220. package/tests/community_leaderboard_db.mjs +1 -1
  1221. package/tests/conductor.mjs +18 -18
  1222. package/tests/conflict_resolve.mjs +14 -16
  1223. package/tests/consumer_layout_boot.mjs +12 -12
  1224. package/tests/context_pack.mjs +6 -6
  1225. package/tests/control_manifest.mjs +4 -4
  1226. package/tests/copy_desk_page_reads.mjs +1 -1
  1227. package/tests/copy_desk_page_release.mjs +192 -0
  1228. package/tests/copy_inventory.mjs +2 -2
  1229. package/tests/core_auto_upgrade.mjs +1 -1
  1230. package/tests/core_move_notice.mjs +2 -2
  1231. package/tests/core_origin_follow.mjs +7 -7
  1232. package/tests/core_pin_sync.mjs +3 -3
  1233. package/tests/core_upgrade_door.mjs +2 -2
  1234. package/tests/core_upgrade_owner_door.mjs +34 -1
  1235. package/tests/core_upgrade_runner.mjs +19 -19
  1236. package/tests/credential_presence.mjs +2 -2
  1237. package/tests/credit_grant.mjs +3 -3
  1238. package/tests/credit_log_single_writer.mjs +5 -5
  1239. package/tests/criterion_ref_status.mjs +2 -2
  1240. package/tests/cross_discipline_priority_direction.mjs +3 -3
  1241. package/tests/cutover_codemod.mjs +2 -4
  1242. package/tests/db_backup_nightly.mjs +1 -1
  1243. package/tests/db_backup_offsite.mjs +1 -1
  1244. package/tests/db_ownership_handover.mjs +2 -2
  1245. package/tests/dead_deps.mjs +3 -3
  1246. package/tests/dependency_advisory.mjs +1 -1
  1247. package/tests/deploy_audit_gate.mjs +4 -4
  1248. package/tests/deploy_divergence_line.mjs +36 -16
  1249. package/tests/deploy_page_projects.mjs +35 -4
  1250. package/tests/dev_lib.mjs +2 -2
  1251. package/tests/dev_windows.mjs +2 -2
  1252. package/tests/diagram_facts_offset.mjs +1 -1
  1253. package/tests/diagram_repo_slug.mjs +1 -1
  1254. package/tests/diff_derived_signals.mjs +2 -2
  1255. package/tests/dns_preflight.mjs +2 -2
  1256. package/tests/do_api.mjs +3 -3
  1257. package/tests/doc_cli_guard.mjs +28 -28
  1258. package/tests/doc_comment_coverage.mjs +1 -1
  1259. package/tests/doc_control_claims.mjs +11 -11
  1260. package/tests/docs_entropy.mjs +2 -2
  1261. package/tests/docs_openapi_route.mjs +8 -6
  1262. package/tests/doctor_hooks.mjs +1 -1
  1263. package/tests/edit_hints.mjs +3 -2
  1264. package/tests/effective_visibility_predicate.mjs +1 -19
  1265. package/tests/env_knobs_neutral_prefix.mjs +13 -13
  1266. package/tests/exec_path_guard.mjs +4 -4
  1267. package/tests/executed_closure.mjs +18 -12
  1268. package/tests/file_map.mjs +1 -1
  1269. package/tests/find_untested.mjs +13 -13
  1270. package/tests/first_admin_bootstrap.mjs +1 -1
  1271. package/tests/fitness.mjs +16 -16
  1272. package/tests/fitness_gds_ratchet.mjs +2 -2
  1273. package/tests/fitness_installed_core.mjs +3 -3
  1274. package/tests/fitness_no_raw_nul.mjs +1 -1
  1275. package/tests/fitness_ratchets.mjs +3 -3
  1276. package/tests/fitness_scope_roster.mjs +2 -2
  1277. package/tests/fitness_two_root.mjs +8 -8
  1278. package/tests/fixtures/accessibility-replay.json +5 -5
  1279. package/tests/fixtures/grade-replay/recorded-panel-grades.json +1 -1
  1280. package/tests/fixtures/ideator-prefold/README.md +1 -1
  1281. package/tests/fixtures/page-ledger.mjs +22 -0
  1282. package/tests/fixtures/session-506-digest.json +5 -5
  1283. package/tests/gate_approvals.mjs +50 -0
  1284. package/tests/gate_review.mjs +8 -8
  1285. package/tests/gating_review_retired.mjs +2 -2
  1286. package/tests/gen_freshness.mjs +3 -3
  1287. package/tests/git_guard.mjs +3 -3
  1288. package/tests/git_merge_regen.mjs +41 -65
  1289. package/tests/github_one_trip.mjs +4 -4
  1290. package/tests/github_pick_up.mjs +2 -2
  1291. package/tests/github_push_merge_driver.mjs +1 -1
  1292. package/tests/go_live.mjs +3 -2
  1293. package/tests/goal_category.mjs +1 -1
  1294. package/tests/goal_cli.mjs +2 -2
  1295. package/tests/goal_create_spec_entry.mjs +1 -1
  1296. package/tests/goal_invite_consent.mjs +3 -1
  1297. package/tests/goal_move.mjs +1 -1
  1298. package/tests/goal_working_area.mjs +1 -1
  1299. package/tests/government_agent_asks.mjs +7 -7
  1300. package/tests/government_board_room_copy.mjs +6 -1
  1301. package/tests/government_board_view.mjs +39 -0
  1302. package/tests/government_board_vote_ui.mjs +39 -1
  1303. package/tests/government_config.mjs +2 -2
  1304. package/tests/government_live_proof_shape.mjs +1 -1
  1305. package/tests/government_protected_surfaces.mjs +54 -53
  1306. package/tests/government_require_permission.mjs +9 -1
  1307. package/tests/government_vanilla_boot.mjs +1 -1
  1308. package/tests/government_vocabulary.mjs +2 -2
  1309. package/tests/government_walk.mjs +2 -2
  1310. package/tests/grade_body_limit.mjs +6 -4
  1311. package/tests/grade_correlation_audit.mjs +3 -3
  1312. package/tests/grade_diff_hash.mjs +1 -1
  1313. package/tests/grade_engine_replay.mjs +2 -2
  1314. package/tests/grade_server_authoritative.mjs +1 -1
  1315. package/tests/grade_unavailable_display.mjs +1 -1
  1316. package/tests/grader.mjs +8 -24
  1317. package/tests/grader_generated_provenance.mjs +14 -14
  1318. package/tests/grader_no_artifact.mjs +1 -1
  1319. package/tests/grader_prompt_fencing.mjs +1 -1
  1320. package/tests/grader_quality_window_overflow.mjs +1 -1
  1321. package/tests/grader_reader_lens.mjs +3 -3
  1322. package/tests/grader_regrade_delta.mjs +1 -1
  1323. package/tests/grader_unbacked_score.mjs +2 -2
  1324. package/tests/hall_acclaim_vote_control.mjs +113 -0
  1325. package/tests/hall_asset_cache.mjs +127 -0
  1326. package/tests/hall_audit.mjs +63 -0
  1327. package/tests/hall_board_world.mjs +20 -0
  1328. package/tests/hall_city_card_words.mjs +61 -0
  1329. package/tests/hall_client_request_shape.mjs +5 -1
  1330. package/tests/hall_home_ships_bounded.mjs +160 -0
  1331. package/tests/hall_landing_boot.mjs +15 -2
  1332. package/tests/hall_nav.mjs +33 -0
  1333. package/tests/hall_onboarding_bongos_setup.mjs +1 -1
  1334. package/tests/hall_oversight_world.mjs +44 -4
  1335. package/tests/hall_palette.mjs +12 -9
  1336. package/tests/hall_preview_security.mjs +1 -1
  1337. package/tests/hall_profile_world.mjs +17 -0
  1338. package/tests/hall_reading_world.mjs +16 -1
  1339. package/tests/hall_roster_world.mjs +2 -1
  1340. package/tests/hall_studio_world.mjs +4 -1
  1341. package/tests/hall_work_history_id.mjs +39 -0
  1342. package/tests/held_tweak_reporting.mjs +4 -4
  1343. package/tests/helpers.mjs +3 -3
  1344. package/tests/home_bounded_reads.mjs +1 -1
  1345. package/tests/home_isolation_guard.mjs +1 -1
  1346. package/tests/hook_core_resolve.mjs +7 -7
  1347. package/tests/hook_user_agents.mjs +1 -1
  1348. package/tests/hooks_drain.mjs +2 -2
  1349. package/tests/host_topology_skips.mjs +1 -1
  1350. package/tests/howto_guide.mjs +1 -1
  1351. package/tests/howto_moments.mjs +3 -3
  1352. package/tests/hub_account_page.mjs +1 -1
  1353. package/tests/idea_capture_grades.mjs +2 -2
  1354. package/tests/idea_develop_spark.mjs +1 -1
  1355. package/tests/idea_filed_via.mjs +2 -2
  1356. package/tests/idea_ideate_full.mjs +1 -1
  1357. package/tests/idea_spark_mark.mjs +1 -1
  1358. package/tests/idea_spark_queue.mjs +1 -1
  1359. package/tests/idea_triage.mjs +1 -1
  1360. package/tests/idea_triage_grade.mjs +3 -3
  1361. package/tests/idea_vote_permission.mjs +113 -0
  1362. package/tests/idea_votes_fold_db.mjs +123 -0
  1363. package/tests/infra_exec_paths.mjs +12 -12
  1364. package/tests/init.mjs +2 -2
  1365. package/tests/instance_db_migrated_on_code_move.mjs +5 -5
  1366. package/tests/instance_feedback.mjs +9 -9
  1367. package/tests/instance_mismatch_guard.mjs +2 -2
  1368. package/tests/integration_parked_ratchet.mjs +24 -2
  1369. package/tests/intent_retry.mjs +1 -1
  1370. package/tests/interaction_posture.mjs +3 -3
  1371. package/tests/invite_expiry_db.mjs +1 -1
  1372. package/tests/joinability_invite_only.mjs +1 -1
  1373. package/tests/karma_vote_lists.mjs +270 -0
  1374. package/tests/key-card.mjs +2 -2
  1375. package/tests/key-status.mjs +2 -2
  1376. package/tests/knip_ratchet_ambient.mjs +2 -2
  1377. package/tests/known_failing_ratchet.mjs +1 -1
  1378. package/tests/land_watch.mjs +2 -2
  1379. package/tests/landing_legal_pages.mjs +1 -1
  1380. package/tests/landing_page.mjs +2 -2
  1381. package/tests/leaderboard_roles.mjs +4 -1
  1382. package/tests/leak_scan_allowlist.mjs +8 -8
  1383. package/tests/leak_scan_fail_closed.mjs +1 -1
  1384. package/tests/lib_sh_resolution.mjs +9 -9
  1385. package/tests/lifecycle-card.mjs +7 -7
  1386. package/tests/linkify_check.mjs +2 -2
  1387. package/tests/linkify_refs.mjs +1 -1
  1388. package/tests/llm-cache-route.mjs +1 -1
  1389. package/tests/local_preview.mjs +2 -2
  1390. package/tests/main_audit.mjs +3 -3
  1391. package/tests/main_audit_attestation.mjs +1 -1
  1392. package/tests/main_audit_db_roster.mjs +2 -2
  1393. package/tests/main_worktree_busy_reason.mjs +3 -3
  1394. package/tests/markdown_line_endings.mjs +2 -2
  1395. package/tests/me_streak_leaderboard_queries.mjs +193 -0
  1396. package/tests/membership_kind_lockstep.mjs +1 -1
  1397. package/tests/memory_isolation.mjs +16 -8
  1398. package/tests/memory_map.mjs +2 -2
  1399. package/tests/memory_pull.mjs +33 -25
  1400. package/tests/merge_driver_register.mjs +3 -3
  1401. package/tests/migration_allocation.mjs +2 -2
  1402. package/tests/migration_namespace.mjs +1 -1
  1403. package/tests/migrations_forward_safe.mjs +1 -1
  1404. package/tests/mingle.mjs +1 -1
  1405. package/tests/mirror_lineage_fulltree.mjs +3 -3
  1406. package/tests/mirror_lineage_proof.mjs +2 -2
  1407. package/tests/mirror_redact.mjs +4 -4
  1408. package/tests/missing_dep_classify.mjs +3 -3
  1409. package/tests/module-scope-map.mjs +22 -22
  1410. package/tests/module_api_lazy.mjs +1 -1
  1411. package/tests/module_assess_docs.mjs +2 -2
  1412. package/tests/module_assess_installs.mjs +3 -3
  1413. package/tests/module_assess_price_parity.mjs +5 -5
  1414. package/tests/module_assess_score.mjs +5 -5
  1415. package/tests/module_assess_security.mjs +4 -4
  1416. package/tests/module_assess_tests.mjs +3 -3
  1417. package/tests/module_assessments_migration.mjs +1 -1
  1418. package/tests/module_catalog.mjs +2 -2
  1419. package/tests/module_cli.mjs +3 -3
  1420. package/tests/module_entitlements.mjs +1 -1
  1421. package/tests/module_error_counts.mjs +1 -1
  1422. package/tests/module_install.mjs +4 -4
  1423. package/tests/module_loader.mjs +3 -1
  1424. package/tests/module_owned_tests.mjs +1 -1
  1425. package/tests/module_release_channels.mjs +1 -1
  1426. package/tests/module_store_howto.mjs +3 -3
  1427. package/tests/module_store_listing.mjs +139 -2
  1428. package/tests/module_store_publish.mjs +4 -4
  1429. package/tests/module_store_publish_route.mjs +1 -1
  1430. package/tests/module_store_reassess.mjs +1 -1
  1431. package/tests/module_store_registry_migration.mjs +1 -1
  1432. package/tests/module_test_runner.mjs +3 -3
  1433. package/tests/module_update.mjs +4 -4
  1434. package/tests/module_upgrade_updates.mjs +1 -1
  1435. package/tests/move_escalation.mjs +2 -2
  1436. package/tests/nav_permission_atoms.mjs +3 -2
  1437. package/tests/needs_rebase.mjs +7 -3
  1438. package/tests/newcomer_floor.mjs +1 -1
  1439. package/tests/next_number.mjs +1 -1
  1440. package/tests/no_artifact_escalation.mjs +3 -3
  1441. package/tests/npm_release_where.mjs +1 -1
  1442. package/tests/oauth_secret.mjs +2 -2
  1443. package/tests/onboard.mjs +6 -6
  1444. package/tests/onboarding_gate.mjs +2 -2
  1445. package/tests/opt_in_skill_modules.mjs +3 -3
  1446. package/tests/own_scope_guard.mjs +1 -1
  1447. package/tests/own_session_revoke.mjs +4 -1
  1448. package/tests/package_core.mjs +10 -11
  1449. package/tests/page_inventory.mjs +5 -5
  1450. package/tests/page_reader.mjs +7 -7
  1451. package/tests/page_tweak_hold.mjs +6 -6
  1452. package/tests/parallel-load.mjs +1 -1
  1453. package/tests/paste_token_api_base.mjs +1 -1
  1454. package/tests/paste_token_session_store.mjs +2 -2
  1455. package/tests/pending_invite_purge_db.mjs +1 -1
  1456. package/tests/permission_path.mjs +1 -1
  1457. package/tests/pin_replay.mjs +1 -1
  1458. package/tests/pin_write_key.mjs +7 -7
  1459. package/tests/plain_cards.mjs +15 -15
  1460. package/tests/pr_assign.mjs +1 -1
  1461. package/tests/pre_push_hook.mjs +3 -3
  1462. package/tests/preflight_checks.mjs +2 -2
  1463. package/tests/prelaunch_gate.mjs +1 -1
  1464. package/tests/privacy_matrix.mjs +1 -1
  1465. package/tests/probe_cache.mjs +6 -6
  1466. package/tests/profile_connections_ui.mjs +4 -3
  1467. package/tests/profile_fields.mjs +11 -1
  1468. package/tests/profile_privacy_owner_view.mjs +1 -1
  1469. package/tests/profile_role_headline.mjs +10 -9
  1470. package/tests/project_admin_console_model.mjs +1 -1
  1471. package/tests/projects_hub.mjs +1 -1
  1472. package/tests/projects_hub_cloud_hop.mjs +1 -1
  1473. package/tests/promote_goal_id.mjs +6 -6
  1474. package/tests/promotion_sweep.mjs +1 -1
  1475. package/tests/provision.mjs +24 -24
  1476. package/tests/provision_account_identity.mjs +2 -2
  1477. package/tests/provision_byo_host.mjs +1 -1
  1478. package/tests/provision_caddy_wiring.mjs +2 -2
  1479. package/tests/provision_disconnect.mjs +2 -2
  1480. package/tests/provision_dns_first.mjs +1 -1
  1481. package/tests/provision_render.mjs +9 -9
  1482. package/tests/provision_repo_private.mjs +3 -3
  1483. package/tests/provision_resource_caps.mjs +5 -5
  1484. package/tests/provision_restart.mjs +3 -3
  1485. package/tests/provision_settings_apply.mjs +7 -7
  1486. package/tests/provision_teardown_address.mjs +2 -2
  1487. package/tests/provisioning_capacity.mjs +5 -5
  1488. package/tests/provisioning_credential_preflight.mjs +1 -1
  1489. package/tests/provisioning_disconnect.mjs +1 -1
  1490. package/tests/provisioning_error_note_scope.mjs +4 -4
  1491. package/tests/provisioning_paid_shape_gate.mjs +2 -2
  1492. package/tests/provisioning_planet_physics.mjs +16 -0
  1493. package/tests/provisioning_runner_drift_notice.mjs +2 -2
  1494. package/tests/provisioning_settings_env.mjs +1 -1
  1495. package/tests/pruned_lockfile_integrity.mjs +1 -1
  1496. package/tests/public_mirror_export.mjs +1 -1
  1497. package/tests/publish_integration_gate.mjs +2 -2
  1498. package/tests/publish_manifest.mjs +6 -6
  1499. package/tests/publish_watch.mjs +1 -1
  1500. package/tests/push_path_brief.mjs +2 -2
  1501. package/tests/raise.mjs +248 -0
  1502. package/tests/rank_roles_audit.mjs +1 -1
  1503. package/tests/rank_tier_single_source.mjs +2 -2
  1504. package/tests/rate_limit_public.mjs +1 -1
  1505. package/tests/read_default_meter.mjs +2 -2
  1506. package/tests/read_session_export.mjs +1 -1
  1507. package/tests/rebase_gate_set_aside.mjs +1 -1
  1508. package/tests/recall_cli.mjs +1 -1
  1509. package/tests/recall_corpus_health.mjs +3 -2
  1510. package/tests/recall_hints.mjs +1 -1
  1511. package/tests/recruiter_sliver_boundary.mjs +1 -1
  1512. package/tests/redteam_patrol.mjs +2 -2
  1513. package/tests/regrade_actual_minutes.mjs +2 -2
  1514. package/tests/regrade_eligibility.mjs +2 -2
  1515. package/tests/release_core.mjs +2 -2
  1516. package/tests/release_notes.mjs +4 -4
  1517. package/tests/rename_history_restraint.mjs +3 -3
  1518. package/tests/rename_ledger_prose.mjs +6 -6
  1519. package/tests/render_api.mjs +1 -1
  1520. package/tests/render_check.mjs +4 -4
  1521. package/tests/render_deploy.mjs +1 -1
  1522. package/tests/repo_info_binding.mjs +1 -1
  1523. package/tests/repo_map.mjs +5 -5
  1524. package/tests/reward_mode_cost_plus_only.mjs +7 -3
  1525. package/tests/reward_mode_surfacing.mjs +1 -1
  1526. package/tests/role_responsibilities.mjs +1 -1
  1527. package/tests/route_permission_check.mjs +1 -1
  1528. package/tests/route_shadow_guard.mjs +1 -1
  1529. package/tests/route_validation.mjs +1 -1
  1530. package/tests/routine_capture_kind.mjs +24 -13
  1531. package/tests/routine_schedule.mjs +51 -9
  1532. package/tests/run_routine.mjs +5 -5
  1533. package/tests/run_routine_two_root.mjs +5 -5
  1534. package/tests/runner_drift.mjs +17 -17
  1535. package/tests/sandbox_stage.mjs +5 -5
  1536. package/tests/scope_map_guard.mjs +7 -7
  1537. package/tests/scouting_owner_access_and_reach.mjs +1 -1
  1538. package/tests/scripts_gds_forwarders.mjs +224 -0
  1539. package/tests/scrubber_corpus.mjs +2 -2
  1540. package/tests/seam_wiring_guard.mjs +1 -1
  1541. package/tests/search_ingest.mjs +2 -2
  1542. package/tests/search_ingest_db.mjs +1 -1
  1543. package/tests/search_ingest_nul.mjs +6 -6
  1544. package/tests/seed_verify_criteria.mjs +3 -3
  1545. package/tests/sequence.mjs +3 -3
  1546. package/tests/served_paths_shipped.mjs +3 -3
  1547. package/tests/session_506_regression.mjs +1 -1
  1548. package/tests/session_accessibility.mjs +1 -1
  1549. package/tests/session_caps.mjs +1 -1
  1550. package/tests/session_cost.mjs +1 -1
  1551. package/tests/session_digest.mjs +2 -2
  1552. package/tests/session_eval.mjs +6 -6
  1553. package/tests/session_index.mjs +1 -1
  1554. package/tests/session_records.mjs +4 -4
  1555. package/tests/session_rename_fallback.mjs +2 -2
  1556. package/tests/session_reward_on_land.mjs +7 -149
  1557. package/tests/session_reward_on_land_db.mjs +191 -0
  1558. package/tests/session_reward_reconcile.mjs +6 -88
  1559. package/tests/session_reward_reconcile_db.mjs +132 -0
  1560. package/tests/session_transcript.mjs +1 -1
  1561. package/tests/sessions_kit_pager.mjs +2 -1
  1562. package/tests/setup_device_flow_fallback.mjs +2 -2
  1563. package/tests/setup_smoke.mjs +12 -5
  1564. package/tests/setup_status_facts.mjs +54 -12
  1565. package/tests/ship_autolinkify.mjs +2 -2
  1566. package/tests/ship_automerge_injection.mjs +1 -1
  1567. package/tests/ship_base_flag.mjs +1 -1
  1568. package/tests/ship_broadcast_routing.mjs +1 -1
  1569. package/tests/ship_broadcast_server.mjs +2 -2
  1570. package/tests/ship_builder_model.mjs +1 -1
  1571. package/tests/ship_cannot_lie.mjs +9 -9
  1572. package/tests/ship_card.mjs +2 -2
  1573. package/tests/ship_ci_deploy.mjs +7 -7
  1574. package/tests/ship_error_shape.mjs +1 -1
  1575. package/tests/ship_land_ci_not_started.mjs +3 -3
  1576. package/tests/ship_land_conflict.mjs +3 -3
  1577. package/tests/ship_land_proof.mjs +3 -3
  1578. package/tests/ship_land_red_pr.mjs +5 -5
  1579. package/tests/ship_merge_gate.mjs +14 -14
  1580. package/tests/ship_mode.mjs +1 -1
  1581. package/tests/ship_mode_first_ship.mjs +4 -4
  1582. package/tests/ship_mode_hands_off.mjs +8 -8
  1583. package/tests/ship_post_land_verify.mjs +1 -1
  1584. package/tests/ship_preflight.mjs +19 -19
  1585. package/tests/ship_premerge.mjs +1 -1
  1586. package/tests/ship_publish_error.mjs +2 -2
  1587. package/tests/ship_push_diagnosis.mjs +1 -1
  1588. package/tests/ship_regen_copy_inventory.mjs +3 -3
  1589. package/tests/ship_resume.mjs +4 -4
  1590. package/tests/ship_smoke_gate.mjs +1 -1
  1591. package/tests/ship_upload_skip.mjs +1 -1
  1592. package/tests/ship_upstream_deletion.mjs +1 -1
  1593. package/tests/ship_vendored_instance.mjs +8 -8
  1594. package/tests/ship_visual_regrade.mjs +3 -3
  1595. package/tests/ship_zero_commit.mjs +1 -1
  1596. package/tests/skill_docs_unavailable_regrade.mjs +3 -3
  1597. package/tests/skill_friction_kind.mjs +1 -1
  1598. package/tests/skill_grade_recover.mjs +1 -1
  1599. package/tests/skill_grader_health.mjs +1 -1
  1600. package/tests/skill_lint.mjs +1 -1
  1601. package/tests/skill_menu_ruling.mjs +1 -1
  1602. package/tests/skill_preflight.mjs +7 -7
  1603. package/tests/skip_is_not_pass.mjs +22 -1
  1604. package/tests/smoke_dependencies_witness.mjs +2 -2
  1605. package/tests/smoke_preflight.mjs +1 -1
  1606. package/tests/smoke_target.mjs +2 -2
  1607. package/tests/smoke_write_contract.mjs +2 -2
  1608. package/tests/sound_player.mjs +8 -8
  1609. package/tests/source_text_test_ratchet.mjs +97 -0
  1610. package/tests/speciality_corpus_scope.mjs +1 -1
  1611. package/tests/speciality_session_skills.mjs +1 -1
  1612. package/tests/sso_checkin_legacy_field_gone.mjs +4 -2
  1613. package/tests/stale_timer.mjs +3 -3
  1614. package/tests/stale_worktrees.mjs +1 -1
  1615. package/tests/start_env_guard.mjs +1 -1
  1616. package/tests/start_goal_first.mjs +3 -3
  1617. package/tests/start_inbox_count.mjs +1 -1
  1618. package/tests/start_rebase_warning.mjs +1 -1
  1619. package/tests/status_default_version.mjs +3 -3
  1620. package/tests/status_mirror_sync.mjs +1 -1
  1621. package/tests/status_page_no_dead_uptime.mjs +80 -0
  1622. package/tests/stop_refcheck.mjs +1 -1
  1623. package/tests/strand_watch.mjs +1 -1
  1624. package/tests/stranded_rows.mjs +1 -1
  1625. package/tests/stuck_detector.mjs +14 -14
  1626. package/tests/sweep_app_account.mjs +3 -3
  1627. package/tests/task_cli.mjs +2 -2
  1628. package/tests/task_detail_ui.mjs +11 -1
  1629. package/tests/task_discipline_required.mjs +2 -2
  1630. package/tests/task_list_paging.mjs +1 -1
  1631. package/tests/task_visual_slots.mjs +2 -2
  1632. package/tests/terms_acceptance.mjs +1 -1
  1633. package/tests/terms_grandfather_db.mjs +1 -1
  1634. package/tests/test_path_guard.mjs +1 -1
  1635. package/tests/triage_development.mjs +1 -1
  1636. package/tests/tweak_batch_apply.mjs +2 -2
  1637. package/tests/tweak_land_heal.mjs +3 -3
  1638. package/tests/tweak_unattended.mjs +3 -3
  1639. package/tests/ui_design_skills.mjs +2 -2
  1640. package/tests/unit_report_status.mjs +2 -2
  1641. package/tests/unit_runner_serial.mjs +2 -2
  1642. package/tests/unit_runner_teardown_abort.mjs +1 -1
  1643. package/tests/update_channel.mjs +2 -2
  1644. package/tests/update_channel_db.mjs +4 -4
  1645. package/tests/update_subscription_engine.mjs +26 -26
  1646. package/tests/update_sweep_home.mjs +14 -14
  1647. package/tests/upgrade.mjs +11 -11
  1648. package/tests/upgrade_artist_gate.mjs +4 -4
  1649. package/tests/upgrade_commit_identity.mjs +2 -2
  1650. package/tests/upgrade_migrate.mjs +7 -7
  1651. package/tests/upgrade_operator.mjs +2 -2
  1652. package/tests/upgrade_outcome.mjs +2 -2
  1653. package/tests/upgrade_persist_pin.mjs +5 -5
  1654. package/tests/upkeep_home.mjs +1 -1
  1655. package/tests/version_close.mjs +2 -2
  1656. package/tests/version_close_cli.mjs +2 -2
  1657. package/tests/version_close_route.mjs +1 -1
  1658. package/tests/version_literals.mjs +23 -23
  1659. package/tests/visual_review.mjs +5 -5
  1660. package/tests/wedge_remedy.mjs +5 -5
  1661. package/tests/welcome_boot.mjs +116 -0
  1662. package/tests/welcome_boot_browser.mjs +271 -0
  1663. package/tests/wizard_born_first_board.mjs +3 -3
  1664. package/tests/wizard_demo.mjs +1 -1
  1665. package/tests/wizard_one_page.mjs +4 -3
  1666. package/tests/wizard_physics.mjs +3 -1
  1667. package/tests/wizard_physics_more.mjs +42 -3
  1668. package/tests/work_title_wrap.mjs +1 -1
  1669. package/tests/workflow_exec_paths.mjs +6 -6
  1670. package/tests/worktree_claim_guard.mjs +1 -1
  1671. package/tests/worktree_helper.mjs +2 -2
  1672. package/tests/write_validation_gaps.mjs +3 -3
  1673. package/scripts/gds/api.sh +0 -6
  1674. package/scripts/gds/artifact-staleness.js +0 -267
  1675. package/scripts/gds/audit-exports.js +0 -128
  1676. package/scripts/gds/classify-discipline.js +0 -153
  1677. package/scripts/gds/codemod-rename-src-gds.js +0 -145
  1678. package/scripts/gds/coreB-carve-proof.sh +0 -725
  1679. package/scripts/gds/goal-names.js +0 -102
  1680. package/scripts/gds/rename-goals.js +0 -109
  1681. package/scripts/gds/resolve-skill-model.js +0 -57
  1682. package/scripts/gds/security-baseline-check.sh +0 -180
  1683. package/scripts/gds/seed-adopt-tasks.js +0 -75
  1684. package/scripts/gds/seed-api-contract-goal-tasks.js +0 -141
  1685. package/scripts/gds/seed-board-room-tasks.js +0 -196
  1686. package/scripts/gds/seed-bongos-app-tasks.js +0 -294
  1687. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +0 -81
  1688. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +0 -100
  1689. package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +0 -307
  1690. package/scripts/gds/seed-bongos-module-system-tasks.js +0 -346
  1691. package/scripts/gds/seed-bongos-tasks.js +0 -214
  1692. package/scripts/gds/seed-bongos-v1-seo-tasks.js +0 -104
  1693. package/scripts/gds/seed-bv1-community-tasks.js +0 -131
  1694. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +0 -133
  1695. package/scripts/gds/seed-bv1-profile-page-tasks.js +0 -161
  1696. package/scripts/gds/seed-bv1-r01-delta-tasks.js +0 -154
  1697. package/scripts/gds/seed-bv2-tweak-mode-tasks.js +0 -277
  1698. package/scripts/gds/seed-gds-v4-tasks.js +0 -624
  1699. package/scripts/gds/seed-governance-tasks.js +0 -340
  1700. package/scripts/gds/seed-medusa-tasks.js +0 -440
  1701. package/scripts/gds/seed-module-upstreaming.js +0 -219
  1702. package/scripts/gds/seed-productization-tasks.js +0 -235
  1703. package/scripts/gds/seed-project-admin-console-tasks.js +0 -88
  1704. package/scripts/gds/seed-provisioning-tasks.js +0 -83
  1705. package/scripts/gds/seed-strict-versioning-tasks.js +0 -275
  1706. package/scripts/gds/seed-v2-world-tasks.js +0 -283
  1707. package/scripts/gds/spike-task-map.js +0 -899
  1708. package/scripts/migrate_cost_ledger.sh +0 -178
  1709. package/scripts/status-probe.mjs +0 -213
  1710. package/src/bongos/uptime-poller.js +0 -93
  1711. package/tests/artifact_staleness.mjs +0 -207
  1712. package/tests/goal_names.mjs +0 -120
  1713. package/tests/government_fold_bench.mjs +0 -50
  1714. package/tests/spike_task_map.mjs +0 -422
  1715. package/tests/status-probe.mjs +0 -89
  1716. /package/{scripts/gds → modules/raise}/CLAUDE.md +0 -0
  1717. /package/scripts/{gds → bongos}/cloudbongos-token-isolation.conf +0 -0
  1718. /package/scripts/{gds → bongos}/discipline-modes.json +0 -0
  1719. /package/scripts/{gds → bongos}/retired-core-skills.json +0 -0
@@ -0,0 +1,1479 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // scripts/bongos/provision.js — the INSTANCE-provisioning control-plane runner
4
+ // (ADR 0111 §2/§3, BONGOS-V1 goal 32, task #1944).
5
+ //
6
+ // WHAT IT IS. The trust-boundary-safe executor for the `provisioning` module.
7
+ // The internet-facing web tier can only ENQUEUE a provision/teardown intent into
8
+ // provisioning_intents (modules/provisioning/routes/provisioning.js) — it holds
9
+ // NO cloud tokens. THIS runner, driven by a systemd timer
10
+ // (provision-intent-runner.timer → `provision.js run-intents --apply`), is the
11
+ // SOLE holder of the DigitalOcean / Cloudflare tokens (from a chmod-600
12
+ // /etc/<inst>/box.env via systemd EnvironmentFile=). It drains the queue and
13
+ // composes the docs/recipes/cloudbongos-standup.md runbook into ONE idempotent,
14
+ // dry-run-default, drift-reconciling flow (ADR 0016 / ADR 0031 §9.4 trust boundary):
15
+ //
16
+ // DNS A record FIRST (task 1004505 — the name must exist before anyone is shown it)
17
+ // → server (a process on an allocated port — co-tenant or cloud-host)
18
+ // → createdb + GDS-only migrate (MEDUSA_GDS_ONLY=1)
19
+ // → write config + per-instance /etc/<inst>/web.env
20
+ // → install + enable a per-instance systemd unit (+ allocate a port)
21
+ // → Caddy site block + reload
22
+ // → /healthz
23
+ //
24
+ // DESIGN — shared with do-api.js:
25
+ // • DRY-RUN by DEFAULT. Without --apply, every step only PRINTS the command it
26
+ // WOULD run and mutates nothing (no DB writes, no shell, no cloud call). The
27
+ // LIVE leg is an explicit operator/timer `--apply`.
28
+ // • PRIVILEGE model (task 2066, ADR 0128). The intent runner runs as a NON-root
29
+ // user (the app user) with passwordless sudo, which cannot itself write under /etc,
30
+ // reload systemd/Caddy, or peer-auth to Postgres as a superuser. PROVISION_SUDO=1
31
+ // escalates exactly those steps (sudo install / sudo systemctl / sudo -u postgres);
32
+ // default OFF = bare commands (root / test / co-tenant-legacy contexts unchanged).
33
+ // • Injectable executor. `makeExec` returns a `run(cmd)` that is a pure logger
34
+ // in dry-run and child_process in --apply; tests pass a recording fake, so the
35
+ // whole runbook composition is unit-tested with no box / DB / network.
36
+ // • Idempotent. Re-running a half-done provision is safe (createdb tolerates
37
+ // "already exists"; unit/Caddy writes are overwrites; healthz is a poll).
38
+ // • The step-command generators are PURE functions (exported), tested directly.
39
+ //
40
+ // This is control-plane ops tooling (scripts/), so it may require the core + a
41
+ // module's data slice directly (a module may not, but a CLI may). It
42
+ // reuses modules/provisioning/provisioning.js (the intent queue + state).
43
+
44
+ const fs = require('node:fs');
45
+ const os = require('node:os');
46
+ const path = require('node:path');
47
+ const cp = require('node:child_process');
48
+ const crypto = require('node:crypto');
49
+
50
+ const retry = require('./intent-retry.js'); // when a failed intent is tried again (task 1004447)
51
+ const { CONFIG, MANIFEST_UA, softFailResult, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, MAX_INTENT_ATTEMPTS, REPO_ROOT, coreVersionSafe, hasFlag, loadDeps, oauthSecret, provisionerBotEmail } = require('./provision-config.js');
52
+ const { accountPreflight, appUser, alreadyScaffolded, buildCorePinRefreshCommit, coreCheckoutHasGit, dbCreateCmd, dbName, dbRoleCmd, dbRoleCmdShown, dbRoleGrantsCmd, dbOwnershipCmd, ensureInstanceUserCmd, generateDbPassword, grantInstanceRepoReadCmd, instanceDbRole, instanceStateDir, instanceUser, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, ownerLoginOf, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
53
+ const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, envPrefixMismatch, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, renamedEnvCarry, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
54
+ const { caddyBlock, caddySnippetPath, checkCaddyWiring, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, identityCmd, identityVerdict, ingressPlaneGap, ingressPreflight, shouldFederate, zoneForDomain } = require('./provision-net.js');
55
+ const { resolveProcessManager } = require('./provision-process.js');
56
+ const { capacityPreflight, statusCapacityLine } = require('./provision-capacity.js');
57
+ const { installedCoreVersion, runnerBanner } = require('./runner-drift.js');
58
+ const { teardownInstance } = require('./provision-teardown.js');
59
+ const { disconnectInstance } = require('./provision-disconnect.js');
60
+ const { coreUpgradeInstance, preflightCoreUpgrade, snapshotAllCoreVersions } = require('./provision-core-upgrade.js'); // the owner's deploy door, ADR 0293
61
+ const { isByoHost, recordByoHost } = require('./provision-byo-host.js'); // stand up NOTHING for a byo-host row, ADR 0323
62
+
63
+ // WHAT supervises an instance's service (ADR 0306). systemd on every box we run — the
64
+ // seam exists so a plane without systemd is not a dead end. Injectable for tests.
65
+ const pmFor = (deps) => (deps && deps.processManager) || resolveProcessManager();
66
+
67
+ function makeExec({ apply, log = console.log, remoteHost = null, remoteCwd = null, sshOpts = '' }) {
68
+ // `shown` (task 1003140): what to LOG in place of the command text — for a command
69
+ // that carries a secret (a remote file placement embeds the base64 body), so the
70
+ // journal records "write <path> (N bytes)" and never the payload.
71
+ return function run(cmd, { allowFail = false, cwd, env, shown: shownAs } = {}) {
72
+ // Extra env is passed to the child (not interpolated into the command), so
73
+ // values like a hostname/IP never need shell quoting. It's shown in the dry-run
74
+ // plan for transparency — DNS env (BOX_HOSTNAME/BOX_IP/CLOUDFLARE_ZONE) is not
75
+ // secret (the token stays in the hook's own EnvironmentFile, out of `env`).
76
+ const envPrefix = env ? `${Object.entries(env).map(([k, v]) => `${k}=${v}`).join(' ')} ` : '';
77
+ if (remoteHost) {
78
+ // Remote step (task 2065): run it on the co-hosting box over SSH. The cwd +
79
+ // env + command are composed into a small script fed to the remote shell via
80
+ // STDIN (`ssh … bash -s`), so nothing needs shell-quoting on the way over
81
+ // (the #1 remote-exec footgun). cwd defaults to the remote checkout so a
82
+ // relative `./scripts/migrate.sh` resolves there. env goes INTO the script
83
+ // (a local child env wouldn't cross the ssh hop).
84
+ const wd = cwd || remoteCwd;
85
+ const script = `${wd ? `cd ${wd} && ` : ''}${envPrefix}${cmd}`;
86
+ const sshCmd = `ssh ${sshOpts} ${remoteHost} bash -s`;
87
+ const shown = `[ssh ${remoteHost}] ${shownAs || script}`;
88
+ if (!apply) { log(` would run: ${shown}`); return { dryRun: true, ok: true, stdout: '' }; }
89
+ try {
90
+ const stdout = cp.execSync(sshCmd, { input: script, stdio: ['pipe', 'pipe', 'pipe'], env: process.env }).toString();
91
+ log(` ✓ ${shown}`);
92
+ return { ok: true, stdout };
93
+ } catch (e) {
94
+ // The script rides STDIN, so e.message names only the ssh hop — but a
95
+ // `shown` call site is secret-bearing by declaration: log the exit status
96
+ // alone, never stderr (a remote shell may echo the failing line back).
97
+ const why = shownAs ? `exit status ${e && e.status != null ? e.status : '?'}` : (e.stderr || e.message || '').toString().trim().slice(0, 120);
98
+ if (allowFail) { log(` ~ ${shown} (non-fatal: ${why})`); return softFailResult(e, !!shownAs); }
99
+ log(` ✖ ${shown}`);
100
+ throw e;
101
+ }
102
+ }
103
+ // Local step (control plane / today's behavior). cwd defaults to REPO_ROOT.
104
+ const shown = shownAs || `${envPrefix}${cmd}`;
105
+ const label = shownAs || cmd;
106
+ if (!apply) { log(` would run: ${shown}`); return { dryRun: true, ok: true, stdout: '' }; }
107
+ try {
108
+ const stdout = cp.execSync(cmd, { cwd: cwd || REPO_ROOT, stdio: ['ignore', 'pipe', 'pipe'], env: env ? { ...process.env, ...env } : process.env }).toString();
109
+ log(` ✓ ${label}`);
110
+ return { ok: true, stdout };
111
+ } catch (e) {
112
+ // With `shown` set the command text is considered secret-bearing, so the
113
+ // non-fatal line carries only the exit status — never e.message, whose
114
+ // "Command failed: <cmd>" would echo the command back into the journal.
115
+ const why = shownAs ? `exit status ${e && e.status != null ? e.status : '?'}` : (e.stderr || e.message || '').toString().trim().slice(0, 120);
116
+ if (allowFail) { log(` ~ ${label} (non-fatal: ${why})`); return softFailResult(e, !!shownAs); }
117
+ log(` ✖ ${label}`);
118
+ throw e;
119
+ }
120
+ };
121
+ }
122
+ // A file write that respects dry-run (log-only) and applies chmod when asked.
123
+ // When privileged AND the call opts in with { sudo: true } (the /etc destinations
124
+ // the runner's non-root user can't write directly), the body is staged in a temp
125
+ // file (lars-writable) then atomically placed with `sudo install -D -m <mode>`
126
+ // (`-D` creates the parent dir, e.g. /etc/<slug>/). Otherwise a direct fs write
127
+ // (unchanged). `exec` is injected so the install goes through the SAME dry-run/apply
128
+ // + logging path as every other command (task 2066).
129
+ function makeWriteFile({ apply, privileged = false, exec, log = console.log, remoteHost = null }) {
130
+ return function writeFile(p, body, { mode, sudo = false } = {}) {
131
+ if (remoteHost) {
132
+ // Remote file placement (task 2065): base64 the body, decode it ON the
133
+ // co-hosting box, and `tee` it into place — sudo-escalated when the
134
+ // destination (/etc/<slug>/…) needs root and we're privileged. base64 keeps
135
+ // arbitrary bytes/newlines out of the shell. It rides the box `exec` (which
136
+ // ssh's it), so it shares the same dry-run + logging + ssh path.
137
+ const dir = path.posix.dirname(p);
138
+ const sp = (sudo && privileged) ? 'sudo ' : '';
139
+ const b64 = Buffer.from(body, 'utf8').toString('base64');
140
+ const steps = [
141
+ `${sp}mkdir -p ${dir}`,
142
+ `printf %s '${b64}' | base64 -d | ${sp}tee ${p} >/dev/null`,
143
+ ];
144
+ if (mode) steps.push(`${sp}chmod ${mode.toString(8)} ${p}`);
145
+ const cmd = steps.join(' && ');
146
+ // The command text embeds the (base64) body — a web.env carries the OAuth
147
+ // secret and the App private key — so the exec logs THIS instead (task 1003140).
148
+ const shown = `write ${p} (${body.length} bytes${mode ? `, mode ${mode.toString(8)}` : ''})`;
149
+ if (!apply) {
150
+ log(` would write ${p} (${body.length} bytes${mode ? `, mode ${mode.toString(8)}` : ''}) on ${remoteHost}`);
151
+ if (exec) exec(cmd, { shown });
152
+ return { dryRun: true };
153
+ }
154
+ exec(cmd, { shown });
155
+ log(` ✓ wrote ${p} (on ${remoteHost})`);
156
+ return { ok: true };
157
+ }
158
+ if (sudo && privileged) {
159
+ // lars can't write under /etc; stage in a temp file then place it as root.
160
+ const tmp = path.join(os.tmpdir(), `prov-${path.basename(p)}-${process.pid}`);
161
+ const installCmd = `sudo install -D ${mode ? `-m ${mode.toString(8)} ` : ''}${tmp} ${p}`;
162
+ if (!apply) {
163
+ log(` would write ${p} (${body.length} bytes${mode ? `, mode ${mode.toString(8)}` : ''}) via temp + sudo install`);
164
+ if (exec) exec(installCmd);
165
+ return { dryRun: true };
166
+ }
167
+ // Owner-only while staged: the destination is 0600 and the body may carry secrets.
168
+ fs.writeFileSync(tmp, body, { mode: 0o600 });
169
+ try { exec(installCmd); } finally { fs.rmSync(tmp, { force: true }); }
170
+ log(` ✓ wrote ${p} (via sudo install)`);
171
+ return { ok: true };
172
+ }
173
+ if (!apply) { log(` would write ${p} (${body.length} bytes${mode ? `, mode ${mode.toString(8)}` : ''})`); return { dryRun: true }; }
174
+ fs.mkdirSync(path.dirname(p), { recursive: true });
175
+ fs.writeFileSync(p, body, mode ? { mode } : undefined);
176
+ if (mode) fs.chmodSync(p, mode);
177
+ log(` ✓ wrote ${p}`);
178
+ return { ok: true };
179
+ };
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Port allocation for a co-tenant instance: the lowest free port ≥ portBase not
184
+ // already taken by another instance row or a reserved port. PURE given the taken set.
185
+ // ---------------------------------------------------------------------------
186
+ function nextFreePort(takenPorts, { portBase = CONFIG.portBase, reserved = CONFIG.reservedPorts } = {}) {
187
+ const taken = new Set([...(takenPorts || []), ...reserved].map(Number));
188
+ let p = portBase;
189
+ while (taken.has(p)) p += 1;
190
+ return p;
191
+ }
192
+ // Ports held by a LISTENING socket on the box the instance will bind on. A
193
+ // co-tenant/standalone instance binds 127.0.0.1:<port>, so ANY process already
194
+ // listening there — not just another provisioned instance or a CONFIG reserved
195
+ // port — causes an EADDRINUSE crash-loop. Snapshot `ss` and return every listening
196
+ // port so allocatePort can exclude them all → robust to any co-located service
197
+ // (e.g. Grafana on the dedicated control plane, task 2070). `allowFail` degrades to
198
+ // the prior DB-only behavior if `ss` is unavailable rather than blocking provisioning.
199
+ function boundLocalPorts(exec) {
200
+ if (typeof exec !== 'function') return [];
201
+ let res;
202
+ try { res = exec('ss -ltnH 2>/dev/null || ss -ltn 2>/dev/null', { allowFail: true }); }
203
+ catch { return []; }
204
+ const ports = [];
205
+ for (const line of String((res && res.stdout) || '').split('\n')) {
206
+ const cols = line.trim().split(/\s+/);
207
+ if (cols.length < 4) continue; // skip header / blank lines
208
+ const m = /:(\d+)$/.exec(cols[3]); // Local Address:Port → 127.0.0.1:3003, *:80, [::1]:3003
209
+ if (m) ports.push(Number(m[1]));
210
+ }
211
+ return ports;
212
+ }
213
+ // Impure: reads the instance table AND (via the injected exec) the box's live
214
+ // listening ports, so a port squatted by a NON-instance co-tenant is never handed
215
+ // out. `exec` is the same executor provisionInstance uses, so for a remote co-tenant
216
+ // it snapshots the remote box (task 2065) — where the instance actually binds.
217
+ async function allocatePort(db, provisioning, { exec } = {}) {
218
+ const rows = await db.query(`SELECT port FROM provisioning_instances WHERE port IS NOT NULL`);
219
+ const dbPorts = rows.rows.map((r) => r.port);
220
+ return nextFreePort([...dbPorts, ...boundLocalPorts(exec)]);
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Compose + run the standup runbook for ONE instance (idempotent, dry-run-safe).
225
+ // deps: { db, provisioning, exec, writeFile, log, apply, privileged }
226
+ // ---------------------------------------------------------------------------
227
+ async function provisionInstance(inst, deps) {
228
+ const { db, provisioning, exec, writeFile, log, apply, privileged } = deps;
229
+ // The token step (Cloudflare DNS hook) runs on the control plane where the token
230
+ // lives — NEVER on the remote box (task 2065). Falls back to `exec` when a caller
231
+ // (e.g. a test) passes no controlExec — then both are the same local executor.
232
+ const controlExec = deps.controlExec || exec;
233
+ // Standalone instances run on the CONTROL PLANE (task 2074): scaffoldStandaloneRepo
234
+ // created their OWN repo dir here via controlExec, so the box-mutating steps below
235
+ // (code sync, migrate, /etc writes, systemd, Caddy, healthz) must run on the SAME
236
+ // box — NOT the remote co-hosting box that PROVISION_REMOTE_HOST routes co-tenant
237
+ // steps to (that box never had the dir; ADR 0130's remote-exec was co-tenant-only).
238
+ // So a standalone uses the control-plane executors; co-tenant is
239
+ // unchanged (boxExec === exec, boxWriteFile === writeFile). When no remoteHost is
240
+ // set, controlExec === exec anyway, so this is a no-op for the all-local default.
241
+ const standalone = inst.hosting_shape === 'cloud-host';
242
+ const boxExec = standalone ? controlExec : exec;
243
+ const boxWriteFile = standalone ? (deps.controlWriteFile || writeFile) : writeFile;
244
+ const sudoP = privileged ? 'sudo ' : ''; // systemctl escalation prefix (task 2066)
245
+ // One fresh per-instance DB password per run (task 1003369, see provision-repo.js): step
246
+ // 2b sets it on the role, step 3 writes it to web.env, so the two cannot disagree.
247
+ const dbPassword = generateDbPassword();
248
+ log(`provision ${inst.slug} (shape=${inst.hosting_shape}, status=${inst.status})`);
249
+
250
+ // Idempotency: an already-active instance is a no-op success (re-drained intent).
251
+ if (inst.status === 'active') { log(' already active — nothing to do'); return { ok: true, already: true }; }
252
+
253
+ if (isByoHost(inst.hosting_shape)) return recordByoHost(inst, deps); // stand up NOTHING, ABOVE every preflight — reasoning in provision-byo-host.js's header (ADR 0323)
254
+ // ── preflight: DNS token scope (task 1980) ─────────────────────────────────
255
+ // BEFORE committing to the address (the step-0 DNS UPSERT /
256
+ // allocating a port), verify the control-plane Cloudflare token can actually reach
257
+ // the domain's zone. Turns the Emersonian standup's confusing mid-flow step-5 DNS
258
+ // failure into an up-front, actionable stop with no half-done provision. Read-only.
259
+ if (inst.domain && CONFIG.dnsHook) {
260
+ if (apply) {
261
+ // Injectable so the composition stays hermetic in tests (deps.checkDnsTokenScope
262
+ // stubs the CF call); production passes the real checker (network read-only).
263
+ const scope = await (deps.checkDnsTokenScope || checkDnsTokenScope)(inst.domain);
264
+ if (!scope.reachable) {
265
+ throw new Error(`DNS preflight failed for ${inst.domain}: ${scope.reason}. ${scope.guidance}`);
266
+ }
267
+ log(` [preflight] DNS token can reach zone ${scope.zone} ✓`);
268
+ } else {
269
+ log(` [preflight] would verify the Cloudflare token can reach zone ${zoneForDomain(inst.domain)} before the address step`);
270
+ }
271
+ }
272
+ const room = await capacityPreflight(inst, deps); // box capacity (ADR 0285, task 1003927): an honest refusal BEFORE the status flip — never a silent oversubscribe
273
+ if (room.ok === false) return room;
274
+ // Ingress preflight (task 1004017, ADR 0315). Ordered capacity → ingress → wiring, all three
275
+ // before any mutation: ingress asks whether this plane has a proxy AT ALL, 0303 below whether
276
+ // that proxy is wired for THIS domain. Deliberately NOT domain-gated — a domainless child
277
+ // skipped 0303 entirely, reached step 6 and died on the snippet write.
278
+ const ingressGap = ingressPreflight({ exec: boxExec, apply, log, probe: deps.ingressPlaneGap });
279
+ if (ingressGap) return { ok: false, error: ingressGap };
280
+ // Account preflight (task 1004018, ADR 0317). The third and last PLANE question — can this
281
+ // box give the child its own unix account? — before 0303 below asks the per-DOMAIN one. A
282
+ // plane that cannot would run every child as one uid, the ADR 0281 vector, and step 1c's
283
+ // useradd would crash mid-provision instead of refusing.
284
+ const accountGap = accountPreflight({ exec: boxExec, apply, log, privileged, probe: deps.accountPlaneGap });
285
+ if (accountGap) return { ok: false, error: accountGap };
286
+ // App-user preflight (task 1002719): steps below name the app user, and appUser() refuses
287
+ // root or an unusable name. Refuse HERE, before the status flip, not midway through.
288
+ try { appUser(); } catch (e) { return { ok: false, error: e.message }; }
289
+ // Caddy preflight (task 1002860, ADR 0303): a box whose base Caddyfile has no tls-check ask
290
+ // can never certify this domain, so refuse before touching it. Injectable like the DNS check.
291
+ const caddyCheck = deps.checkCaddyWiring || checkCaddyWiring;
292
+ if (inst.domain && apply) {
293
+ const gap = caddyCheck(boxExec, { domain: inst.domain });
294
+ if (gap) return { ok: false, error: `Caddy preflight failed: ${gap}` };
295
+ }
296
+ // ── step 0: DNS FIRST — Cloudflare A-record UPSERT for the instance's domain (P4) ──
297
+ // The first mutation of a standup, ahead of the status flip and every step after it
298
+ // (task 1004505, BV2.PS17). The hub can show a project's address from the moment it
299
+ // is created, and a lookup made before the record exists is cached as "not found" by
300
+ // the visitor's resolver for the zone's negative TTL (30 min on cloudbongos.com) — a
301
+ // founder's first click used to lock their own home network out of the project for
302
+ // half an hour. Created here, the name answers before any surface can offer it; the
303
+ // hub also holds every link until a platform lookup has seen it answer (dns_resolves).
304
+ // The A-record target (recordIp):
305
+ // • co-tenant → PROVISION_PUBLIC_IP (the box it runs on — under remote-exec that
306
+ // is the co-hosting box, ADR 0130).
307
+ // • standalone → the CONTROL PLANE's own IP (task 2074): it runs control-plane-
308
+ // local, so its domain must point HERE, not at the co-hosting box.
309
+ // PROVISION_CONTROL_PLANE_IP when remote-exec splits the two boxes;
310
+ // else PROVISION_PUBLIC_IP (all-local: one and the same machine).
311
+ // Remote-exec set but no control-plane IP configured → null, which
312
+ // makes this step fail loud rather than point the domain at the wrong box.
313
+ const recordIp = standalone ? CONFIG.controlPlaneIp || (CONFIG.remoteHost ? null : CONFIG.publicIp) : CONFIG.publicIp;
314
+ // Formalized in P4 (#1945): the shared box-dns-cloudflare.sh hook UPSERTs an A
315
+ // record pointing the domain at recordIp, in the domain's own zone. It runs on the
316
+ // CONTROL PLANE for every shape: the hook needs the Cloudflare token, which never
317
+ // leaves the control plane. A DNS FAILURE SURFACES (the task-1180 lesson: never
318
+ // report provision success while the A record silently didn't land) — and, being
319
+ // first, it now fails before anything else was touched.
320
+ let dnsUpserted = false;
321
+ if (inst.domain && CONFIG.dnsHook) {
322
+ if (!recordIp) {
323
+ const ipEnvHint = standalone ? 'PROVISION_CONTROL_PLANE_IP (standalone)' : 'PROVISION_PUBLIC_IP (co-tenant)';
324
+ if (apply) throw new Error(`cannot UPSERT DNS for ${inst.domain}: no target IP — set ${ipEnvHint} in the control-plane box.env`);
325
+ log(` [dns] would upsert A ${inst.domain} once the target IP is known (set ${ipEnvHint})`);
326
+ } else {
327
+ // Standalone runs control-plane-local under the *.cloudbongos.com wildcard origin
328
+ // cert (task 2074/2077) → proxied=true (CF edge fronts it, IP hidden); co-tenant
329
+ // stays proxied=false so their on-demand ACME challenge (step 6) reaches the origin.
330
+ const dnsEnv = dnsUpsertEnv(inst, { publicIp: recordIp, proxied: standalone });
331
+ log(` [dns] upsert A ${inst.domain} -> ${recordIp} (zone ${dnsEnv.CLOUDFLARE_ZONE}, proxied=${dnsEnv.BOX_PROXIED === '1'}) via ${CONFIG.dnsHook}`);
332
+ // controlExec, NOT exec: the DNS hook needs the Cloudflare token, which lives
333
+ // only on the control plane — it must never run on the (token-free) co-hosting
334
+ // box even when the box-mutating steps below go there over SSH (task 2065).
335
+ controlExec(CONFIG.dnsHook, { env: dnsEnv });
336
+ dnsUpserted = true;
337
+ // The clear half of the birth-path note (F16): a revive after the operator
338
+ // configured the hook must retire the stale "point your A record" sentence.
339
+ if (apply && provisioning.recordDnsNote) {
340
+ await provisioning.recordDnsNote(db, inst.id, null).catch(() => {});
341
+ }
342
+ }
343
+ } else {
344
+ log(' [dns] skipped (no domain, or BOX_DNS_HOOK unset)');
345
+ // Birth-path twin of the domain-attach note (F16): a domain with no hook is
346
+ // the same silent skip here, so the owner card gets the same sentence.
347
+ if (apply && inst.domain) {
348
+ if (provisioning.recordDnsNote) await provisioning.recordDnsNote(db, inst.id,
349
+ `Automatic DNS isn't set up on this platform — point ${inst.domain}'s A record at ${recordIp || 'your server'}, and this card will show when it answers.`
350
+ ).catch(() => {});
351
+ }
352
+ }
353
+
354
+ if (apply) await provisioning.setInstanceStatus(db, inst.id, 'provisioning');
355
+ const patch = {};
356
+
357
+ // ── cross-site identity: register hub federation (ADR 0141 §4, task #1002109) ──
358
+ // Computed BEFORE the shape steps so the co-tenant web.env write (step 3) bakes it
359
+ // in — the instance comes up already federated, no post-hoc restart. Dormant unless
360
+ // the control plane is hub-configured AND the shape/domain qualify (shouldFederate);
361
+ // otherwise the instance owns its auth (direct GitHub), byte-identically to today.
362
+ let federation = null;
363
+ {
364
+ const fed = shouldFederate(inst, deps);
365
+ if (fed.federate) {
366
+ federation = await federateInstance(inst, deps);
367
+ log(` [federate] ${inst.slug} → hub sign-in (auth.idp on) — no GitHub app of its own`);
368
+ if (apply) {
369
+ await provisioning.recordEvent(db, {
370
+ instanceId: inst.id, ownerBuilderId: inst.owner_builder_id, event: 'federated',
371
+ detail: `sign-in federated to the hub (client ${inst.slug}, origin https://${inst.domain})`, actor: 'runner',
372
+ }).catch(() => {});
373
+ }
374
+ } else {
375
+ log(` [federate] skipped (${fed.reason}) — instance owns its auth (direct GitHub)`);
376
+ }
377
+ }
378
+
379
+ // ── step 1: server ───────────────────────────────────────────────────────
380
+ // Co-tenant OR standalone: a process bound to an allocated port on the box it runs
381
+ // on. Co-tenant shares that box's checkout ($0, cloudbongos.com model — under
382
+ // remote-exec, the co-hosting box); standalone (ADR 0108) runs from its OWN repo
383
+ // dir with a pinned @bongos/core (synced in step 2) on the CONTROL PLANE.
384
+ // Allocate against the ports of whichever box it runs on (boxExec: remote for a
385
+ // co-tenant, local for a standalone — task 2074), then run the rest there.
386
+ if (!inst.port) {
387
+ const port = await allocatePort(db, provisioning, { exec: boxExec });
388
+ patch.port = port;
389
+ inst = { ...inst, port };
390
+ log(` [server] ${inst.hosting_shape} — allocated port ${port}`);
391
+ } else {
392
+ log(` [server] ${inst.hosting_shape} — port ${inst.port}`);
393
+ }
394
+ // Standalone: SCAFFOLD its own repo if absent — clone the (empty) target_ref →
395
+ // materialize config/ + .claude/ + file:-pinned core + lockfile → commit + push
396
+ // (ADR 0108 / 0125, task 2055). Replaces the old "repo must already be cloned"
397
+ // throw: the greenfield flow now has no manual clone step. Idempotent + dry-run-safe.
398
+ if (inst.hosting_shape === 'cloud-host') {
399
+ await scaffoldStandaloneRepo(inst, deps);
400
+ // A PRIVATE target_ref gets its own read-only deploy key BEFORE step 2's `git pull
401
+ // --ff-only origin main`, which failed unauthenticated on every private standup (N4; ADR 0176).
402
+ await ensurePrivateRepoAccess(inst, deps);
403
+ await require('./provision-pin-key.js').ensurePinWriteKey(inst, deps); // the hub's WRITE key for later pin pushes, public repos too (task 1004291, ADR 0350)
404
+ }
405
+ if (!inst.db_name) { patch.db_name = inst.slug; inst = { ...inst, db_name: inst.slug }; }
406
+
407
+ // The remaining steps run on the instance's box (boxExec/boxWriteFile — remote for
408
+ // co-tenant under remote-exec, control-plane-local for standalone; task 2074).
409
+
410
+ // ── step 2: code sync (standalone) + database ─────────────────────────────
411
+ if (standalone) {
412
+ // Standalone (ADR 0108 / deploy-instance.sh steps 1-2): sync the instance's own
413
+ // repo + install the pinned @bongos/core, so the migrate + service below run
414
+ // from the INSTANCE root, not this control-plane checkout. cwd = standaloneRoot.
415
+ // boxExec = the control plane (where scaffoldStandaloneRepo just created the dir).
416
+ // CORE RESOLUTION: the vendored default needs no network at all (the tarball is
417
+ // committed). A registry pin resolves from the public registry — @bongos/core is public
418
+ // (task 1003872), so `npm ci` needs no credential either way (task 1003878).
419
+ const root = standaloneRoot(inst);
420
+ log(' [code] standalone — pull instance repo + npm ci (pinned @bongos/core)');
421
+ boxExec(standalonePullCmd(), { cwd: root });
422
+ // W3 (task 1003050): heal a committed `file:` pin that trails this control plane's
423
+ // running core BEFORE npm ci, so a re-provision can never leave running != pinned
424
+ // (the silent-rollback hazard: the next `git reset --hard` + `npm ci` deploy would
425
+ // otherwise revert the instance against an already-migrated DB). Gated on apply —
426
+ // the refresh writes through upgrade.js fs/npm calls, not the dry-run-safe exec.
427
+ if (apply) await refreshStandaloneCorePin(inst, deps);
428
+ else log(' [code] would refresh the committed @bongos/core pin if it trails the running core (task 1003050)');
429
+ boxExec(standaloneInstallCmd(), { cwd: root });
430
+ }
431
+ // ── step 1c: the instance's OWN unix account (task 1003369, audit B3) ─────
432
+ // Idempotent; the group-add is what lets a fresh system account READ the checkout it runs
433
+ // from — without it the unit fails at exec with EACCES. Rationale: provision-repo.js.
434
+ log(` [user] ensure unix account ${instanceUser(inst)} + its state dir`);
435
+ boxExec(ensureInstanceUserCmd(inst, { privileged }));
436
+ boxExec(grantInstanceRepoReadCmd(inst, { privileged }));
437
+
438
+ log(' [db] create + GDS-only migrate');
439
+ boxExec(dbCreateCmd(inst, { privileged }), { allowFail: true }); // "already exists" is fine
440
+ if (standalone) boxExec(standaloneMigrateCmd(inst, { privileged }), { cwd: standaloneRoot(inst) }); // instance-root migrate
441
+ else boxExec(migrateCmd(inst, { privileged }));
442
+ // Seed the first version so the instance can take tasks from minute one (task 2119,
443
+ // goal 35). Only fires if the versions table is empty — idempotent, never disturbs an
444
+ // existing project. NOT allowFail: a re-run is a clean 0-row no-op, so a real failure
445
+ // here should surface.
446
+ log(' [seed] first version (only if versions is empty)');
447
+ boxExec(seedFirstVersionCmd(inst, { privileged }));
448
+ // Regenerate the whole-file navigation docs from the pinned core so a fresh/redeployed
449
+ // standalone instance's docs/ stays live (ADR 0110 §2, task 1002314) — the deploy-regen the
450
+ // bespoke pull scripts were missing. Best-effort: regen-instance-docs.js exits 0 even on a
451
+ // generator hiccup (LOUD ::ERROR::), so a stale nav doc never fails a provision.
452
+ if (standalone) {
453
+ log(' [docs] regenerate whole-file navigation docs (session-log index)');
454
+ boxExec(standaloneRegenDocsCmd(), { cwd: standaloneRoot(inst), allowFail: true });
455
+ }
456
+
457
+ // ── step 2b: the instance's OWN password-authed PG role (task 1003369, audit B3) ──
458
+ // AFTER the migrate: it runs as `postgres`, which therefore OWNS the tables it creates.
459
+ log(' [db] create/rotate the per-instance role + revoke PUBLIC connect');
460
+ // `shown` is MANDATORY: dbRoleCmd embeds the password, and run() logs the raw command
461
+ // on BOTH the dry-run and success paths — the exact leak this task exists to prevent
462
+ // (makeWriteFile passes `shown` for the web.env body for the same reason, and a `shown`
463
+ // call site also suppresses stderr). The GRANTS carry no secret and log in full.
464
+ boxExec(dbRoleCmd(inst, { password: dbPassword, privileged }), { shown: dbRoleCmdShown(inst) });
465
+ boxExec(dbRoleGrantsCmd(inst, { privileged }));
466
+ // Grants are not ownership: later moves migrate AS this role and must ALTER what the migrate
467
+ // above created as postgres (task 1004735). Non-fatal on purpose — if it ever fails, the
468
+ // project still serves, and the first move that needs it applies it (migrate_not_owner).
469
+ const own = boxExec(dbOwnershipCmd(inst, { privileged }), { allowFail: true });
470
+ if (own && own.softFailed) log(' ⚠ [db] could not hand the schema to the instance role — its first core move that ALTERs a table will retry it (migrate_not_owner)');
471
+
472
+ // ── step 3: config + per-instance secrets ─────────────────────────────────
473
+ log(' [config] per-instance web.env');
474
+ boxWriteFile(webEnvPath(inst), webEnvBody(inst, { federation, ownerLogin: await ownerLoginOf(inst, deps.db), dbPassword }), { mode: 0o600, sudo: true });
475
+
476
+ // ── step 4: install + enable the instance's service ───────────────────────
477
+ const pm = pmFor(deps);
478
+ log(` [service] install + enable the ${pm.key} service`);
479
+ pm.installService(inst, { exec: boxExec, writeFile: boxWriteFile, sudoP, log });
480
+
481
+ // ── step 4b: nightly DB backup — dir + consumer-layout timer/service (task 2213) ──
482
+ // Every instance must be born WITH backups (the task-2210 gap: cloudbongos was
483
+ // provisioned without any). Create the app-user-owned backup dir, install the
484
+ // per-instance timer/service (correct db + consumer-layout script path), enable
485
+ // the timer. Idempotent: `install -d` no-ops on an existing dir, unit writes are
486
+ // overwrites, `enable --now` is idempotent.
487
+ log(' [backup] create backup dir + install + enable nightly DB backup timer');
488
+ pm.installBackup(inst, { exec: boxExec, writeFile: boxWriteFile, sudoP, log });
489
+
490
+ // (step 5, the DNS UPSERT, moved to step 0 at the top — task 1004505.)
491
+ // A re-point on a PRE-ACTIVE instance records its release here and never
492
+ // reaches the attach leg (that one no-ops for anything but 'active'), so this
493
+ // run owns the drain — and an `error` row is frequently a partial standup that
494
+ // already UPSERTed the address it is now leaving (ADR 0181 §1, task 1003237).
495
+ await releaseAbandonedDomains(inst, { controlExec, log, apply, db, provisioning });
496
+
497
+ // ── step 6: Caddy per-instance site snippet + reload ──────────────────────
498
+ // Write the instance's OWN <slug>.caddy snippet (idempotent overwrite via the
499
+ // injected writeFile — no read-modify-append to a shared Caddyfile), then reload.
500
+ // The main Caddyfile carries a one-time `import <snippetDir>/*.caddy` (operator
501
+ // setup) — verified after the write, since only a written snippet proves the import
502
+ // reaches it. The reload is loud: Caddy keeps its old config when it rejects the new one.
503
+ log(' [caddy] write site snippet + reload');
504
+ boxWriteFile(caddySnippetPath(inst), caddyBlock(inst), { mode: 0o644, sudo: true });
505
+ const gap = inst.domain && apply ? caddyCheck(boxExec, { domain: inst.domain, requireHost: true }) : null;
506
+ if (gap) return { ok: false, error: `Caddy wiring failed: ${gap}` };
507
+ boxExec(`${sudoP}systemctl reload caddy`);
508
+
509
+ // ── step 7: healthz ───────────────────────────────────────────────────────
510
+ // The probe RESULT is recorded, not discarded (task 1002720 / QA-walk F15: a
511
+ // refused connection used to ride silently into 'active' and the card said
512
+ // live). Bounded retry so a slow first boot is not slandered — the
513
+ // placeManifestCreds loop shape. Still ok:true either way: a failed probe
514
+ // must not flip the intent to failed (cmdRunIntents would burn the 3-attempt
515
+ // budget on an instance that may simply be slow); the liveness overlay is
516
+ // what tells the owner the truth.
517
+ log(' [healthz] verify the instance answers');
518
+ const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
519
+ let healthy = null; // null = not observed (dry-run)
520
+ const tries = apply ? 10 : 1;
521
+ for (let i = 0; i < tries; i++) {
522
+ const res = boxExec(healthzCmd(inst), { allowFail: true });
523
+ if (!apply) break; // dry-run: exactly one plan line
524
+ if (res && !res.softFailed) { healthy = true; break; }
525
+ healthy = false;
526
+ if (i < tries - 1) await sleep(1000);
527
+ }
528
+
529
+ // ── step 7b: identity ─────────────────────────────────────────────────────
530
+ // Does it answer as ITSELF? A live instance that serves the platform's name instead of its own
531
+ // is invisible to healthz — it is perfectly healthy and telling every newcomer the wrong project
532
+ // (task 1003740, which went unnoticed across every hosted instance for months). A WARNING, never
533
+ // a failure: an owner may legitimately name a project after the platform, and a name must not
534
+ // fail a working standup.
535
+ if (apply && healthy !== false) {
536
+ const res = boxExec(identityCmd(inst), { allowFail: true });
537
+ const raw = res && !res.softFailed ? (res.stdout || '') : '';
538
+ let manifest = null;
539
+ try { manifest = raw ? JSON.parse(raw) : null; } catch (_) { manifest = null; }
540
+ if (manifest) {
541
+ let neutralName = null;
542
+ try {
543
+ neutralName = (JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'config/branding.neutral.json'), 'utf8'))
544
+ .identity || {}).productName || null;
545
+ } catch (_) { /* no neutral pack readable → the check simply cannot fire */ }
546
+ const verdict = identityVerdict({ manifest, slug: inst.slug, neutralName });
547
+ if (verdict.ok === false) {
548
+ log(` ⚠ ${inst.slug} ${verdict.reason} — check its config/branding.json and that no env var pins its branding pack`);
549
+ } else if (verdict.ok) {
550
+ log(` [identity] answers as "${verdict.name}"`);
551
+ }
552
+ }
553
+ }
554
+
555
+ // Mark active. error_note: null — a stale note from an earlier failed run
556
+ // must not ride into the active state (F15's second half; the co-tenant +
557
+ // standalone paths never cleared it). The runner hands its own observation
558
+ // to the liveness overlay so the card gets an honest verdict immediately
559
+ // instead of waiting ~5 min for the first sweep.
560
+ // ── step 7c: the platform's own lookup (task 1004505) ─────────────────────
561
+ // The record went in at step 0, minutes ago, so this lookup cannot race it into a
562
+ // cached miss. An answer lets the hub link the address the moment the row reads
563
+ // active, instead of waiting for the first liveness sweep. Only for a record this
564
+ // run created: a manual-DNS domain is the owner's to point, and the sweep finds it.
565
+ if (apply && dnsUpserted && provisioning.lookupAnswers && provisioning.recordDnsResolved) {
566
+ if (await provisioning.lookupAnswers(inst.domain, { lookup: deps.lookup })) {
567
+ await provisioning.recordDnsResolved(db, inst.id).catch(() => {});
568
+ log(` [dns] ${inst.domain} answers — the hub may link it now`);
569
+ } else {
570
+ log(` [dns] ${inst.domain} does not answer the control plane yet — the hub holds its link until the sweep sees it`);
571
+ }
572
+ }
573
+
574
+ if (apply) {
575
+ await provisioning.setInstanceStatus(db, inst.id, 'active', { ...patch, error_note: null, provisioned_at: new Date().toISOString() });
576
+ // The overlay column is DOMAIN-scoped when a domain exists (the sweep
577
+ // probes https://<domain>; loopback only when there is none), and the card
578
+ // renders up=true as "Answering at <domain>". This probe is loopback, so:
579
+ // a FAILURE is a verdict on any shape (a dead process answers nowhere),
580
+ // but a PASS proves only the process — for a domain-bearing instance the
581
+ // address claim stays "not checked yet" until the sweep earns it.
582
+ const probeIsVerdict = healthy === false || (healthy === true && !inst.domain);
583
+ if (probeIsVerdict && provisioning.recordLivenessProbe) {
584
+ await provisioning.recordLivenessProbe(db, inst.id, healthy).catch(() => {});
585
+ }
586
+ await provisioning.recordEvent(db, {
587
+ instanceId: inst.id, ownerBuilderId: inst.owner_builder_id, event: 'active',
588
+ detail: `provisioned (${inst.hosting_shape}, port ${inst.port || '-'})` +
589
+ (healthy === false ? ' — not answering /healthz yet' : ''),
590
+ actor: 'runner',
591
+ });
592
+ }
593
+ log(healthy === false
594
+ ? ` ⚠ ${inst.slug} provisioned, but it is not answering /healthz yet — its card will read "unreachable"`
595
+ : ` ✓ ${inst.slug} provisioned`);
596
+ return { ok: true, port: inst.port, healthy };
597
+ }
598
+
599
+ // The addresses a re-point left behind and this run still owes a release on
600
+ // (task 1003237). The one guard this tier can make on its own, and the one that
601
+ // matters: the address the instance is CURRENTLY on is never released, so a row
602
+ // that somehow queued its own domain can never blackhole a live project.
603
+ // WHICH addresses reach the queue is the ENQUEUE site's job (the route's
604
+ // `releasing`, ADR 0200 as amended by task 1002902). PURE.
605
+ function releasableDomains(inst) {
606
+ const here = String((inst && inst.domain) || '').toLowerCase();
607
+ return (Array.isArray(inst && inst.released_domains) ? inst.released_domains : [])
608
+ .map((d) => String(d || '').toLowerCase())
609
+ .filter((d) => d && d !== here);
610
+ }
611
+
612
+ // Park every address a re-point moved off at the blackhole sentinel, then empty
613
+ // the queue (task 1003237, ADR 0200). Called by BOTH legs that wire a domain —
614
+ // domain-attach for an ACTIVE instance, and the provision run that owns
615
+ // PRE-ACTIVE ones, since an `error` row is frequently a partial standup that
616
+ // already UPSERTed the address it is now leaving (ADR 0181 §1).
617
+ //
618
+ // BEST-EFFORT in the teardown sense: a hook that fails leaves the queue intact
619
+ // so the next run retries, and never fails the wiring that already landed. EVERY
620
+ // queued address is attempted — one host whose zone the token cannot reach must
621
+ // not strand the others behind it — and the queue is emptied only when all of
622
+ // them landed; a re-park of an address already at the sentinel is an idempotent
623
+ // UPSERT, so retrying the whole list costs nothing but the call. The list is
624
+ // capped at the enqueue site (MAX_RELEASED_DOMAINS), which is what bounds this
625
+ // loop. Returns the first host that failed, or null. Runs on the CONTROL PLANE —
626
+ // the hook needs the Cloudflare token, which never leaves it (ADR 0111 §2).
627
+ async function releaseAbandonedDomains(inst, { controlExec, log, apply, db, provisioning }) {
628
+ const hosts = releasableDomains(inst);
629
+ if (!hosts.length) return null;
630
+ if (!CONFIG.dnsHook) {
631
+ log(` [dns] cannot release ${hosts.join(', ')} (BOX_DNS_HOOK unset) — remove their A records manually`);
632
+ return null;
633
+ }
634
+ let firstFailed = null;
635
+ for (const host of hosts) {
636
+ // task 1003954: the route frees a name once this row moves off it, so another
637
+ // project may have attached it since — parking it now would blackhole THAT
638
+ // project. A name we cannot check is kept for the next run, never parked on a guess.
639
+ let holder = null;
640
+ try { holder = provisioning.getInstanceByDomain ? await provisioning.getInstanceByDomain(db, host) : null; }
641
+ catch (e) {
642
+ log(` ⚠ could not check who holds ${host}: ${(e && e.message) || e} — left for the next run`);
643
+ if (!firstFailed) firstFailed = host;
644
+ continue;
645
+ }
646
+ if (holder && String(holder.id) !== String(inst.id) && holder.status !== 'torn_down') {
647
+ log(` [dns] leave ${host} alone — ${holder.slug} holds it now`);
648
+ continue;
649
+ }
650
+ const relEnv = dnsReleaseEnv(host);
651
+ log(` [dns] release ${host} -> ${relEnv.BOX_IP} (blackhole, zone ${relEnv.CLOUDFLARE_ZONE}) via ${CONFIG.dnsHook}`);
652
+ if (!apply) continue;
653
+ try { controlExec(CONFIG.dnsHook, { env: relEnv }); }
654
+ catch (e) {
655
+ log(` ⚠ could not release ${host}: ${(e && e.message) || e} — its A record still points here`);
656
+ if (!firstFailed) firstFailed = host;
657
+ }
658
+ }
659
+ if (firstFailed) return firstFailed;
660
+ if (apply && provisioning.clearReleasedDomains) await provisioning.clearReleasedDomains(db, inst.id).catch(() => {});
661
+ return null;
662
+ }
663
+
664
+ // ---------------------------------------------------------------------------
665
+ // Attach a late-arriving domain to an ALREADY-ACTIVE instance (task 1002692):
666
+ // re-assert exactly the domain-dependent runbook legs — web.env origins (+ a
667
+ // service RESTART so the process re-reads them), the Cloudflare A-record UPSERT
668
+ // (step 5), the Caddy site snippet (step 6) — then healthz. Expected failures
669
+ // RETURN { ok:false } instead of throwing: the drain loop's catch flips the
670
+ // instance to 'error', which must never happen to a healthy ACTIVE instance
671
+ // over a domain hiccup.
672
+ //
673
+ // This leg ALSO carries a RE-POINT (task 1003237, ADR 0200) — the same intent,
674
+ // because the wiring is the same wiring. Two things follow from that:
675
+ // • The origin lines are patched IN PLACE (upsertEnvVars), never regenerated
676
+ // from the row. A template re-write via webEnvBody would blank the OAuth
677
+ // creds placeManifestCreds placed, the App private key and the federation
678
+ // block — none of which the row knows (ADR 0190 §2). A first attach was
679
+ // MASKED by wizard order (address first, GitHub App second); a re-point runs
680
+ // on a project the founder is already signed in to, so the same move would
681
+ // break sign-in on the exact project they just corrected (idea 1000842).
682
+ // • Every address on the row's release queue is parked at the blackhole
683
+ // sentinel AFTER the new one is wired — best-effort, never fatal.
684
+ // ---------------------------------------------------------------------------
685
+ async function domainAttachInstance(inst, deps) {
686
+ const { db, provisioning, exec, writeFile, log, apply, privileged } = deps;
687
+ const controlExec = deps.controlExec || exec;
688
+ // Same box routing as provisionInstance: a standalone runs control-plane-local
689
+ // (task 2074), so its env/caddy/service live HERE, not on the co-hosting box.
690
+ const standalone = inst.hosting_shape === 'cloud-host';
691
+ const boxExec = standalone ? controlExec : exec;
692
+ const boxWriteFile = standalone ? (deps.controlWriteFile || writeFile) : writeFile;
693
+ const sudoP = privileged ? 'sudo ' : '';
694
+ log(`domain-attach ${inst.slug} (shape=${inst.hosting_shape}, status=${inst.status}, domain=${inst.domain || '-'})`);
695
+
696
+ if (!inst.domain) return { ok: false, error: 'no domain recorded on the instance — nothing to wire' };
697
+ // Pre-active states are the provision run's job; post-active states have nothing
698
+ // to address. A clean no-op (not an error) so a raced teardown/re-request never
699
+ // burns the intent's retry budget.
700
+ if (inst.status !== 'active') {
701
+ log(` instance is '${inst.status}' — nothing to wire now (provision owns pre-active domains)`);
702
+ return { ok: true, noop: true };
703
+ }
704
+ // DNS preflight (the task-1980 stance): verify token reach BEFORE touching the box.
705
+ if (CONFIG.dnsHook && apply) {
706
+ const scope = await (deps.checkDnsTokenScope || checkDnsTokenScope)(inst.domain);
707
+ if (!scope.reachable) {
708
+ return { ok: false, error: `DNS preflight failed for ${inst.domain}: ${scope.reason}. ${scope.guidance}` };
709
+ }
710
+ log(` [preflight] DNS token can reach zone ${scope.zone} ✓`);
711
+ }
712
+ const ingressGap = ingressPreflight({ exec: boxExec, apply, log, probe: deps.ingressPlaneGap, before: 're-writing the Caddy snippet' });
713
+ if (ingressGap) return { ok: false, error: ingressGap }; // Ingress preflight (task 1004017, ADR 0315)
714
+ const caddyCheck = deps.checkCaddyWiring || checkCaddyWiring; // Caddy preflight (task 1002860, ADR 0303)
715
+ if (apply) {
716
+ const gap = caddyCheck(boxExec, { domain: inst.domain });
717
+ if (gap) return { ok: false, error: `Caddy preflight failed: ${gap}` };
718
+ }
719
+
720
+ // The A-record target — the same derivation as provisionInstance step 1.
721
+ const recordIp = standalone
722
+ ? (CONFIG.controlPlaneIp || (CONFIG.remoteHost ? null : CONFIG.publicIp))
723
+ : CONFIG.publicIp;
724
+
725
+ // Patch ONLY the origin lines (the settingsApplyInstance shape, ADR 0190 §2).
726
+ // Dry-run has no file to read, so it PLANS the patch rather than writing a
727
+ // synthetic body. An unreadable or empty file is refused, never "patched":
728
+ // writing four origins over a missing PORT/PGDATABASE would take the instance
729
+ // down — and this leg runs against instances that are ALREADY serving.
730
+ const origins = originEnvVarsFor(inst);
731
+ const read = boxExec(`${sudoP}cat ${webEnvPath(inst)}`, { allowFail: true });
732
+ const currentEnv = String((read && read.stdout) || '');
733
+ if (!apply) {
734
+ log(` [config] would patch ${Object.keys(origins).join(', ')} in ${webEnvPath(inst)} in place (creds + federation untouched)`);
735
+ } else if (!read || read.softFailed || !currentEnv.trim()) {
736
+ return { ok: false, error: `cannot read ${webEnvPath(inst)} — nothing to patch (was this instance provisioned by the runner?)` };
737
+ } else {
738
+ log(' [config] patch the origin lines of web.env in place (creds + federation untouched)');
739
+ boxWriteFile(webEnvPath(inst), upsertEnvVars(currentEnv, origins), { mode: 0o600, sudo: true });
740
+ }
741
+ log(' [service] restart to pick up the new origins');
742
+ pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
743
+
744
+ // DNS A-record UPSERT — mirrors provisionInstance step 5 (control-plane exec only:
745
+ // the hook needs the Cloudflare token, which never leaves the control plane).
746
+ let dnsSkipped = false;
747
+ if (CONFIG.dnsHook) {
748
+ if (!recordIp) {
749
+ const ipEnvHint = standalone ? 'PROVISION_CONTROL_PLANE_IP (standalone)' : 'PROVISION_PUBLIC_IP (co-tenant)';
750
+ if (apply) return { ok: false, error: `cannot UPSERT DNS for ${inst.domain}: no target IP — set ${ipEnvHint} in the control-plane box.env` };
751
+ log(` [dns] would upsert A ${inst.domain} once the target IP is known (set ${ipEnvHint})`);
752
+ } else {
753
+ const dnsEnv = dnsUpsertEnv(inst, { publicIp: recordIp, proxied: standalone });
754
+ log(` [dns] upsert A ${inst.domain} -> ${recordIp} (zone ${dnsEnv.CLOUDFLARE_ZONE}, proxied=${dnsEnv.BOX_PROXIED === '1'}) via ${CONFIG.dnsHook}`);
755
+ controlExec(CONFIG.dnsHook, { env: dnsEnv });
756
+ }
757
+ } else {
758
+ // Still ok:true below — an unset hook is unretryable, and ok:false would
759
+ // burn the intent's retry budget on it. The skip is surfaced to the OWNER
760
+ // via dns_note (F16), not just this operator log line.
761
+ dnsSkipped = true;
762
+ log(` [dns] skipped (BOX_DNS_HOOK unset) — point ${inst.domain}'s A record at ${recordIp || 'this server'} manually`);
763
+ }
764
+
765
+ // The snippet is per-SLUG and always carries the CURRENT domain, so a re-point
766
+ // takes the old vhost away in the same write — Caddy stops serving the old host
767
+ // (and tls-check stops vouching for it) with no separate un-wire step.
768
+ log(' [caddy] write site snippet + reload');
769
+ boxWriteFile(caddySnippetPath(inst), caddyBlock(inst), { mode: 0o644, sudo: true });
770
+ const gap = apply ? caddyCheck(boxExec, { domain: inst.domain, requireHost: true }) : null;
771
+ if (gap) return { ok: false, error: `Caddy wiring failed: ${gap}` };
772
+ boxExec(`${sudoP}systemctl reload caddy`);
773
+
774
+ // Release what the re-point moved off, AFTER the new address is wired: the DNS
775
+ // record is the only half of an abandoned address the platform still holds, and
776
+ // it points at platform infra.
777
+ const releaseFailed = await releaseAbandonedDomains(inst, { controlExec, log, apply, db, provisioning });
778
+ if (releaseFailed) return { ok: true, releaseFailed };
779
+
780
+ log(' [healthz] verify the instance answers');
781
+ boxExec(healthzCmd(inst), { allowFail: true });
782
+
783
+ if (apply) {
784
+ // Written BOTH ways so a later successful attach clears a stale note.
785
+ if (provisioning.recordDnsNote) await provisioning.recordDnsNote(db, inst.id, dnsSkipped
786
+ ? `Automatic DNS isn't set up on this platform — point ${inst.domain}'s A record at ${recordIp || 'your server'}, and this card will show when it answers.`
787
+ : null).catch(() => {});
788
+ // Release the F19 latch: this run SUCCEEDED, so a "we stopped trying" note
789
+ // from an earlier retired attach must not outlive its own recovery.
790
+ if (provisioning.clearErrorNote) await provisioning.clearErrorNote(db, inst.id, 'domain-attach').catch(() => {});
791
+ await provisioning.recordEvent(db, {
792
+ instanceId: inst.id, ownerBuilderId: inst.owner_builder_id, event: 'domain-attached',
793
+ detail: dnsSkipped
794
+ ? `domain ${inst.domain} wired (env + caddy); DNS SKIPPED — no hook configured`
795
+ : `domain ${inst.domain} wired (env + DNS + caddy re-asserted)`,
796
+ actor: 'runner',
797
+ }).catch(() => {});
798
+ }
799
+ log(dnsSkipped
800
+ ? ` ✓ ${inst.slug} configured for ${inst.domain} — DNS still needs your A record`
801
+ : ` ✓ ${inst.slug} addressed as ${inst.domain}`);
802
+ return { ok: true };
803
+ }
804
+
805
+ // ---------------------------------------------------------------------------
806
+ // Push a changed per-project setting to an ALREADY-ACTIVE instance (task
807
+ // 1003140 — the live-push half of the platform→instance settings channel; task
808
+ // 1003139 built the standup half). The branding pack is memoized + deep-frozen
809
+ // for the process lifetime, so a change reaches a running instance only by
810
+ // re-writing its web.env and RESTARTING the service: the domain-attach shape
811
+ // minus the DNS legs. The settings lines are patched IN PLACE (upsertEnvVars) —
812
+ // a template re-write via webEnvBody would blank the OAuth creds that
813
+ // placeManifestCreds placed, which this leg has no way to know. EVERY expected
814
+ // failure — an unreadable file, a refused write, a restart that exits non-zero —
815
+ // RETURNS { ok:false } instead of throwing: the drain loop's catch flips the
816
+ // instance to 'error', which must never happen to a healthy ACTIVE instance over
817
+ // a settings push (ADR 0190).
818
+ // ---------------------------------------------------------------------------
819
+ async function settingsApplyInstance(inst, deps) {
820
+ const { db, provisioning, exec, writeFile, log, apply, privileged } = deps;
821
+ // A standalone runs control-plane-local; the control plane's own row IS this box (task 1004334).
822
+ const onControlBox = inst.hosting_shape === 'cloud-host' || inst.hosting_shape === 'control-plane';
823
+ const boxExec = onControlBox ? (deps.controlExec || exec) : exec;
824
+ const boxWriteFile = onControlBox ? (deps.controlWriteFile || writeFile) : writeFile;
825
+ const sudoP = privileged ? 'sudo ' : '';
826
+ const vars = settingsEnvVarsFor(inst); // the self row's prefix is re-read from the live file at the write
827
+ const shown = Object.entries(vars).map(([k, v]) => `${k}=${v}`).join(' ');
828
+ log(`settings-apply ${inst.slug} (shape=${inst.hosting_shape}, status=${inst.status}): ${shown}`);
829
+
830
+ // Pre-active states get these values at standup from the same row; post-active
831
+ // ones have nothing to restart. A clean no-op (not an error) so a raced
832
+ // teardown never burns the intent's retry budget.
833
+ if (inst.status !== 'active') {
834
+ log(` instance is '${inst.status}' — nothing to push now (standup writes the same values)`);
835
+ return { ok: true, noop: true };
836
+ }
837
+ // Read the file as it stands so ONLY the settings lines change. Dry-run runs no
838
+ // command, so it has no file to patch — it PLANS the patch instead of writing a
839
+ // synthetic two-line body (which would read as the exact overwrite this leg
840
+ // exists to avoid). An unreadable or empty file is refused, never "patched":
841
+ // writing two lines over a missing PORT/PGDATABASE would take the instance down.
842
+ const read = boxExec(`${sudoP}cat ${webEnvPath(inst)}`, { allowFail: true });
843
+ const current = String((read && read.stdout) || ''), carry = renamedEnvCarry(current); // task 1004395: only its NAMES are ever logged
844
+ if (!apply) {
845
+ log(` [config] would patch ${Object.keys(vars).join(', ')} in ${webEnvPath(inst)} in place (every other line untouched)`);
846
+ pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
847
+ boxExec(healthzCmd(inst), { allowFail: true });
848
+ log(` ✓ would restart ${inst.slug} with the new settings (dry-run — nothing ran)`);
849
+ return { ok: true, healthy: null, consumed: null };
850
+ }
851
+ if (!read || read.softFailed || !current.trim()) {
852
+ return { ok: false, error: `cannot read ${webEnvPath(inst)} — nothing to patch (was this instance provisioned by the runner?)` };
853
+ }
854
+ try {
855
+ log(` [config] patch the settings lines of web.env in place (creds + origins untouched)${carry.carried.length ? `; carry the renamed env forward: ${carry.carried.join(', ')} (value copied on the box, never shown)` : ''}`);
856
+ boxWriteFile(webEnvPath(inst), upsertEnvVars(current, { ...settingsEnvVarsFor(inst, current), ...carry.vars }), { mode: 0o600, sudo: true });
857
+ log(' [service] restart so the memoized branding pack re-reads them');
858
+ pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
859
+ } catch (e) {
860
+ // ok:false, never a throw: the retry re-reads the file (the upsert is
861
+ // idempotent) and restarts again; the instance's status is left alone.
862
+ return { ok: false, error: `could not apply the settings on ${inst.slug}: ${(e && e.message) || e}` };
863
+ }
864
+
865
+ // Verify it came back. PATIENT (the F23 lesson — the manifest probe's budget):
866
+ // the child must boot before it can answer, and a false "down" here is not
867
+ // cosmetic — it arms the owner's re-kick paths. Silence after the WHOLE budget
868
+ // is a verdict on any shape and is recorded immediately (provisionInstance step
869
+ // 7's rule); a loopback PASS proves the process, not the address — the
870
+ // domain-scoped sweep earns that claim.
871
+ log(' [healthz] verify the instance answers after the restart');
872
+ const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
873
+ let healthy = false;
874
+ for (let i = 0; i < MANIFEST_VERIFY_TRIES; i++) {
875
+ const res = boxExec(healthzCmd(inst), { allowFail: true });
876
+ if (res && !res.softFailed) { healthy = true; break; }
877
+ if (i < MANIFEST_VERIFY_TRIES - 1) await sleep(MANIFEST_VERIFY_INTERVAL_MS);
878
+ }
879
+ // Answering is not the same as having READ the new values: a core that predates
880
+ // task 1003139 restarts fine and ignores them. Ask the instance's own manifest
881
+ // (the read-back ADR 0190 names) and record what it says — never a success it
882
+ // did not earn. null = the manifest carries no project block (unknown).
883
+ let consumed = null;
884
+ if (healthy) {
885
+ const m = boxExec(instanceManifestCmd(inst), { allowFail: true });
886
+ consumed = settingsConsumed(m && m.stdout, inst);
887
+ }
888
+ const outcome = !healthy ? 'restarted, but not answering /healthz yet'
889
+ : consumed === true ? 'restarted and reads the new values'
890
+ : consumed === false ? 'restarted, but still reports the OLD values — its core may predate the settings channel'
891
+ : 'restarted; its core does not publish its settings yet (pin predates task 1003139), so whether it reads them is unknown';
892
+ const probeIsVerdict = healthy === false || (healthy === true && !inst.domain);
893
+ if (probeIsVerdict && provisioning.recordLivenessProbe) await provisioning.recordLivenessProbe(db, inst.id, healthy).catch(() => {});
894
+ // Release the F19 latch: this run SUCCEEDED, so a "we stopped trying" note
895
+ // from an earlier retired push must not outlive its own recovery.
896
+ if (provisioning.clearErrorNote) await provisioning.clearErrorNote(db, inst.id, 'settings-apply').catch(() => {});
897
+ await provisioning.recordEvent(db, {
898
+ instanceId: inst.id, ownerBuilderId: inst.owner_builder_id, event: 'settings-applied',
899
+ detail: `settings pushed (${shown}) — web.env patched in place${carry.carried.length ? `; carried ${carry.carried.join(', ')}` : ''}; ${outcome}`,
900
+ actor: 'runner',
901
+ }).catch(() => {});
902
+ log(healthy && consumed !== false
903
+ ? ` ✓ ${inst.slug} ${outcome}`
904
+ : ` ⚠ ${inst.slug} ${outcome}${healthy ? '' : ' — its card will read "unreachable"'}`);
905
+ return { ok: true, healthy, consumed };
906
+ }
907
+
908
+ // ---------------------------------------------------------------------------
909
+ // Restart an ALREADY-ACTIVE instance the heartbeat cannot reach (task 1003238 —
910
+ // the honest owner action behind the card's "unreachable" line). This is
911
+ // settingsApplyInstance minus the env patch: nothing about the instance's
912
+ // configuration changes, so there is no file to read and no file to write — just
913
+ // the restart and the same PATIENT healthz verify, whose result is recorded on
914
+ // the liveness overlay so the card that sent the owner here updates itself.
915
+ //
916
+ // Why its own leg rather than a re-provision: PROVISION_ENQUEUE_FROM deliberately
917
+ // excludes 'active' (task 1002691), and provisionInstance is a no-op on an active
918
+ // instance by design — a full standup is the wrong tool for "turn it off and on
919
+ // again". Why not settings-apply: that leg only runs behind a value that actually
920
+ // CHANGED, which is exactly the case a stuck project does not have.
921
+ //
922
+ // Like the settings push, EVERY expected failure RETURNS { ok:false } instead of
923
+ // throwing: the drain loop's catch flips the instance to 'error', and an instance
924
+ // that is merely unreachable must not be relabelled broken by the very action
925
+ // meant to recover it — that would drop it off the liveness sweep and leave a
926
+ // re-provision as its only exit (ADR 0190 §2).
927
+ // ---------------------------------------------------------------------------
928
+ async function restartInstance(inst, deps) {
929
+ const { db, provisioning, exec, log, apply, privileged } = deps;
930
+ // Same box routing as settingsApplyInstance: a standalone runs control-plane-local, and the
931
+ // control plane's own row IS this box (task 1004463).
932
+ const onControlBox = inst.hosting_shape === 'cloud-host' || inst.hosting_shape === 'control-plane';
933
+ const boxExec = onControlBox ? (deps.controlExec || exec) : exec;
934
+ const sudoP = privileged ? 'sudo ' : '';
935
+ log(`restart ${inst.slug} (shape=${inst.hosting_shape}, status=${inst.status})`);
936
+
937
+ // A clean no-op (not an error) so a teardown that raced the restart never burns
938
+ // the intent's retry budget: every non-active state either has nothing running
939
+ // yet or is being reclaimed.
940
+ if (inst.status !== 'active') {
941
+ log(` instance is '${inst.status}' — nothing running to restart`);
942
+ return { ok: true, noop: true };
943
+ }
944
+ if (!apply) {
945
+ pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
946
+ boxExec(healthzCmd(inst), { allowFail: true });
947
+ log(` ✓ would restart ${inst.slug} and wait for it to answer (dry-run — nothing ran)`);
948
+ return { ok: true, healthy: null };
949
+ }
950
+ try {
951
+ log(' [service] restart');
952
+ pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
953
+ } catch (e) {
954
+ // ok:false, never a throw — the retry simply restarts again (idempotent).
955
+ return { ok: false, error: `could not restart ${inst.slug}: ${(e && e.message) || e}` };
956
+ }
957
+
958
+ // Verify it came back, with the manifest probe's patience (the F23 lesson). A
959
+ // false "down" is never cosmetic here: this instance is ALREADY flagged
960
+ // unreachable, and a hasty verdict would tell the owner their restart failed.
961
+ log(' [healthz] verify the instance answers after the restart');
962
+ const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
963
+ let healthy = false;
964
+ for (let i = 0; i < MANIFEST_VERIFY_TRIES; i++) {
965
+ const res = boxExec(healthzCmd(inst), { allowFail: true });
966
+ if (res && !res.softFailed) { healthy = true; break; }
967
+ if (i < MANIFEST_VERIFY_TRIES - 1) await sleep(MANIFEST_VERIFY_INTERVAL_MS);
968
+ }
969
+ // Same narrowness as the settings push: a loopback PASS proves the process, not
970
+ // the address, so only the domain-scoped sweep may claim "up" for an instance
971
+ // that HAS a domain. Silence after the whole budget is a verdict on any shape.
972
+ const probeIsVerdict = healthy === false || (healthy === true && !inst.domain);
973
+ if (probeIsVerdict && provisioning.recordLivenessProbe) await provisioning.recordLivenessProbe(db, inst.id, healthy).catch(() => {});
974
+ // A restart that worked retires the "we stopped trying" note a failed restart
975
+ // left (the F19 latch); another action's note stays (task 1004197).
976
+ // DELIBERATELY gated on `healthy`, NOT on probeIsVerdict: the two answer
977
+ // different questions. probeIsVerdict asks "may this run claim the instance is
978
+ // UP?" — a loopback pass may not, for an instance whose address the sweep owns.
979
+ // The note asks "did work stop partway?", and a process that restarted and
980
+ // answered is that work finishing, whatever the public address does next.
981
+ // Holding the note until the sweep agrees would leave a stale "nothing more
982
+ // will happen" on the card of a project that just visibly recovered.
983
+ if (healthy && provisioning.clearErrorNote) await provisioning.clearErrorNote(db, inst.id, 'restart').catch(() => {});
984
+ // Name WHAT was observed, not a verdict this probe cannot reach. healthzCmd is
985
+ // loopback (127.0.0.1:<port>) while the liveness sweep probes the PUBLIC
986
+ // https://<domain>/healthz — so an instance can answer here while DNS, Caddy or
987
+ // TLS is the thing actually broken. "Answering again" on a domained instance
988
+ // would contradict the card still reading "unreachable"; the same asymmetry is
989
+ // why probeIsVerdict above declines to write an up-verdict for that case.
990
+ const outcome = !healthy ? 'restarted, but it is not answering on the box — something there needs a look'
991
+ : inst.domain ? 'restarted and answering on the box; whether its public address answers is for the next liveness sweep to say'
992
+ : 'restarted and answering again';
993
+ await provisioning.recordEvent(db, {
994
+ instanceId: inst.id, ownerBuilderId: inst.owner_builder_id, event: 'restarted',
995
+ detail: `restart requested — ${outcome}`,
996
+ actor: 'runner',
997
+ }).catch(() => {});
998
+ log(healthy ? ` ✓ ${inst.slug} ${outcome}` : ` ⚠ ${inst.slug} ${outcome}`);
999
+ // ok:true either way: the restart RAN. A project that stays down is a truthful
1000
+ // observation for the card, not a failed intent to retry MAX_INTENT_ATTEMPTS
1001
+ // times — restarting a broken instance four more times fixes nothing and would
1002
+ // end by stamping an error_note over the owner's own deliberate action.
1003
+ return { ok: true, healthy };
1004
+ }
1005
+
1006
+ // ---------------------------------------------------------------------------
1007
+ // GitHub App Manifest conversion + cred placement (task 2080, ADR 0133).
1008
+ // The web tier stored GitHub's temporary `code` on a provisioning_oauth_manifests
1009
+ // row (status 'code_received'); THIS runner — the sole token-holder — exchanges it
1010
+ // for the client creds and places them in the instance's web.env, then verifies
1011
+ // /healthz. The secret is fetched here and written straight to disk: it never
1012
+ // touches the web tier or the browser (ADR 0111 §2 / ADR 0016 trust boundary).
1013
+ // ---------------------------------------------------------------------------
1014
+
1015
+ // Exchange GitHub's manifest `code` for the App's client creds. UNAUTHENTICATED by
1016
+ // design — the single-use code IS the auth (GitHub docs). `fetchImpl` injectable for
1017
+ // tests. Throws on a non-2xx or a response missing client_id/client_secret.
1018
+ async function convertManifestCode(code, { fetchImpl = globalThis.fetch, userAgent = MANIFEST_UA } = {}) {
1019
+ if (!code) throw new Error('no manifest code to convert');
1020
+ const res = await fetchImpl(`https://api.github.com/app-manifests/${encodeURIComponent(code)}/conversions`, {
1021
+ method: 'POST',
1022
+ headers: {
1023
+ Accept: 'application/vnd.github+json',
1024
+ 'User-Agent': userAgent,
1025
+ 'X-GitHub-Api-Version': '2022-11-28',
1026
+ },
1027
+ });
1028
+ const text = await res.text();
1029
+ let data; try { data = JSON.parse(text); } catch { data = null; }
1030
+ if (!res.ok) {
1031
+ const msg = (data && (data.message || data.error)) || `HTTP ${res.status}`;
1032
+ throw new Error(`GitHub manifest conversion failed: ${msg}`);
1033
+ }
1034
+ if (!data || !data.client_id || !data.client_secret) {
1035
+ throw new Error('GitHub manifest conversion returned no client_id/client_secret');
1036
+ }
1037
+ return {
1038
+ clientId: String(data.client_id),
1039
+ clientSecret: String(data.client_secret),
1040
+ appId: data.id != null ? Number(data.id) : null,
1041
+ appSlug: data.slug || null,
1042
+ // The App private key (task 1003049): with the manifest now requesting the
1043
+ // ADR 0055 repo scopes, this pem + the app id ARE the instance's publish
1044
+ // credential — placed into web.env alongside the OAuth creds, never logged.
1045
+ appPem: data.pem ? String(data.pem) : null,
1046
+ };
1047
+ }
1048
+
1049
+ // The sign-in probe: the project's JSON health answer, which carries auth_configured
1050
+ // (src/bongos/routes/healthz.js). NOT healthzCmd — the root /healthz answers a plain "ok",
1051
+ // which never parses, so every setup that worked was recorded FAILED (task 1004577).
1052
+ function authHealthzCmd(inst) {
1053
+ return `curl -sS --max-time 5 http://127.0.0.1:${inst.port}/api/bongos/healthz`;
1054
+ }
1055
+
1056
+ // Convert one manifest row's code → creds and PLACE them (rewrite web.env + restart +
1057
+ // verify /healthz). Uses the SAME executors provisionInstance does, so co-tenant (local
1058
+ // or remote-cohost) and standalone (control-plane-local) are handled identically. Returns { ok, appId, appSlug } | { ok:false, error }.
1059
+ async function placeManifestCreds(inst, mrow, deps) {
1060
+ const { log, apply } = deps;
1061
+ if (!mrow.github_code) return { ok: false, error: 'manifest row carries no github_code to convert' };
1062
+ const standalone = inst.hosting_shape === 'cloud-host';
1063
+ const boxExec = standalone ? (deps.controlExec || deps.exec) : deps.exec;
1064
+ const boxWriteFile = standalone ? (deps.controlWriteFile || deps.writeFile) : deps.writeFile;
1065
+ const sudoP = deps.privileged ? 'sudo ' : '';
1066
+
1067
+ // 1) exchange the code with GitHub (control-plane-side; the secret lands here only).
1068
+ let creds;
1069
+ if (apply) {
1070
+ creds = await (deps.convertManifestCode || convertManifestCode)(mrow.github_code, {});
1071
+ } else {
1072
+ log(` [oauth-manifest] would POST api.github.com/app-manifests/<code>/conversions for ${inst.slug}`);
1073
+ creds = { clientId: '0'.repeat(20), clientSecret: '0'.repeat(40), appId: null, appSlug: null, appPem: null };
1074
+ }
1075
+
1076
+ // 2) validate the returned creds BEFORE writing (catch a malformed GitHub response —
1077
+ // the same length/charset guard oauth-secret.js uses against silent truncation).
1078
+ // From here on the single-use manifest code is SPENT (the conversion POST
1079
+ // consumed it) — every return carries codeSpent:true so the drain never
1080
+ // resets the row for a retry that would re-convert a burned code and 404.
1081
+ const idV = oauthSecret.validateClientId(creds.clientId);
1082
+ const secV = oauthSecret.validateClientSecret(creds.clientSecret);
1083
+ if (!idV.ok || !secV.ok) {
1084
+ const why = [!idV.ok ? `id: ${idV.reason}` : null, !secV.ok ? `secret: ${secV.reason}` : null].filter(Boolean).join('; ');
1085
+ return { ok: false, codeSpent: apply, error: `GitHub returned unusable creds (${why})` };
1086
+ }
1087
+
1088
+ // 3) rewrite web.env with the creds + restart the unit (never logs the secret — the
1089
+ // writeFile logs only a byte count; the value is base64/temp-file placed).
1090
+ log(` [oauth-manifest] place creds in ${webEnvPath(inst)} + restart ${inst.slug}`);
1091
+ try {
1092
+ boxWriteFile(webEnvPath(inst), webEnvBody(inst, {
1093
+ clientId: creds.clientId, clientSecret: creds.clientSecret, ownerLogin: await ownerLoginOf(inst, deps.db),
1094
+ // The App id + private key double as the ADR 0055 publish credential (task
1095
+ // 1003049); older Apps (pre-permission manifests) return a pem too — placing
1096
+ // it is harmless and heals once the owner grants the permissions + installs.
1097
+ appId: creds.appId, appPem: creds.appPem,
1098
+ }), { mode: 0o600, sudo: true });
1099
+ pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
1100
+ } catch (e) {
1101
+ // The code is spent and the keys are not placed: a retry would re-convert a burned
1102
+ // code, so say so (codeSpent) and name the app GitHub made for the platform team
1103
+ // (task 1004652 — the wizard offers to send it to them; the slug stays OFF the row,
1104
+ // because a recorded slug reads as "keys placed").
1105
+ return { ok: false, codeSpent: apply, error: `GitHub made the app ${creds.appSlug || creds.appId || '?'}, but placing its keys failed: ${e.message}` };
1106
+ }
1107
+ // The app now exists AND its credentials are placed — the checklist's first tick, and the
1108
+ // slug the callback page needs to send the founder on to installing it (task 1004649).
1109
+ if (apply && deps.onCredsPlaced) await Promise.resolve().then(() => deps.onCredsPlaced({ appId: creds.appId, appSlug: creds.appSlug })).catch(() => {});
1110
+
1111
+ // 4) verify the project reports auth_configured=true (only meaningful under --apply).
1112
+ // PATIENT (F23): the child must boot before it can answer; losing this race
1113
+ // used to record a failure for a setup that had succeeded. The JSON answer lives
1114
+ // at /api/bongos/healthz — the root /healthz says a plain "ok" (task 1004577).
1115
+ if (apply) {
1116
+ const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
1117
+ let authConfigured = false;
1118
+ for (let i = 0; i < MANIFEST_VERIFY_TRIES; i++) {
1119
+ const out = boxExec(authHealthzCmd(inst), { allowFail: true });
1120
+ if (oauthSecret.parseHealthzAuth(out && out.stdout).authConfigured) { authConfigured = true; break; }
1121
+ await sleep(MANIFEST_VERIFY_INTERVAL_MS);
1122
+ }
1123
+ if (!authConfigured) {
1124
+ // The App now EXISTS on the owner's account — keep its id + slug on the row so
1125
+ // the wizard can name it and say why "try again" makes a second one (task 1004572).
1126
+ // Say WHY when we can see it: an adopted repo's own pack may read other names (task 1004573).
1127
+ // A cloud-host checkout lives on the control plane (the scaffold reads it the same way), so
1128
+ // this is a plain file read — no shell. An unreadable or non-JSON pack names nothing.
1129
+ let packText = null;
1130
+ if (standalone) { try { packText = (deps.readFile || fs.readFileSync)(path.join(standaloneRoot(inst), 'config', 'branding.json'), 'utf8'); } catch { packText = null; } }
1131
+ const why = envPrefixMismatch(packText);
1132
+ return { ok: false, codeSpent: true, appId: creds.appId, appSlug: creds.appSlug, error: `the credentials are placed, but the project did not report sign-in ready in time${why ? ` — ${why}` : ''}` };
1133
+ }
1134
+ log(' [oauth-manifest] ✓ /healthz auth_configured=true — sign-in is live');
1135
+ } else {
1136
+ log(' [oauth-manifest] would probe /healthz for auth_configured after restart');
1137
+ }
1138
+ return { ok: true, codeSpent: apply, appId: creds.appId, appSlug: creds.appSlug };
1139
+ }
1140
+
1141
+ // Drain provisioning_oauth_manifests rows awaiting conversion (status 'code_received').
1142
+ // Runs on the SAME timer tick as the intent queue (called from cmdRunIntents). Each row
1143
+ // is claimed atomically (status → 'converting', SKIP LOCKED) so overlapping ticks are
1144
+ // safe; a retryable failure resets it to 'code_received' for a future tick until the
1145
+ // attempt cap, then 'error'. A `seen` set stops a hot re-claim loop within one tick.
1146
+ async function drainOAuthManifests(deps) {
1147
+ const { db, provisioning, log, apply } = deps;
1148
+ if (!apply) {
1149
+ const { rows } = await db.query(
1150
+ `SELECT m.id, i.slug FROM provisioning_oauth_manifests m
1151
+ JOIN provisioning_instances i ON i.id = m.instance_id
1152
+ WHERE m.status = 'code_received' ORDER BY m.created_at`);
1153
+ if (rows.length) {
1154
+ log(`oauth-manifest runner (dry-run): ${rows.length} awaiting conversion`);
1155
+ for (const r of rows) log(` would convert + place creds for ${r.slug} (manifest #${r.id})`);
1156
+ }
1157
+ return { converted: 0, errors: 0 };
1158
+ }
1159
+ let converted = 0, errors = 0;
1160
+ const seen = new Set();
1161
+ for (;;) {
1162
+ const mrow = await provisioning.claimNextOAuthManifest(db);
1163
+ if (!mrow) break;
1164
+ if (seen.has(mrow.id)) {
1165
+ // Already tried this row this tick (a retryable failure reset it). Put it back and
1166
+ // stop, so a persistent failure retries on the NEXT tick instead of hot-looping.
1167
+ await provisioning.resolveOAuthManifest(db, mrow.id, 'code_received', {}).catch(() => {});
1168
+ break;
1169
+ }
1170
+ seen.add(mrow.id);
1171
+ if (mrow.attempts > MAX_INTENT_ATTEMPTS) {
1172
+ await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: `exceeded ${MAX_INTENT_ATTEMPTS} attempts` });
1173
+ errors++; log(` ✖ oauth-manifest #${mrow.id} retired (too many attempts)`); continue;
1174
+ }
1175
+ try {
1176
+ const inst = await provisioning.getInstanceById(db, mrow.instance_id);
1177
+ if (!inst) { await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: 'instance row vanished' }); errors++; continue; }
1178
+ // the slug lands on the row the moment the credentials are placed (task 1004649)
1179
+ const onCredsPlaced = (c) => provisioning.resolveOAuthManifest(db, mrow.id, 'converting', { app_id: c.appId, app_slug: c.appSlug });
1180
+ const result = await placeManifestCreds(inst, mrow, { ...deps, onCredsPlaced });
1181
+ if (result && result.ok === false) {
1182
+ // A SPENT code is terminal, always (F23): GitHub's manifest code is
1183
+ // single-use, so resetting the row to 'code_received' guarantees the
1184
+ // next tick re-converts a burned code and 404s forever. Only failures
1185
+ // that happened BEFORE the conversion stay retryable.
1186
+ if (result.codeSpent) await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: result.error, github_code: null, ...(result.appSlug ? { app_id: result.appId, app_slug: result.appSlug } : {}) });
1187
+ else if (mrow.attempts >= MAX_INTENT_ATTEMPTS) await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: result.error });
1188
+ else await provisioning.resolveOAuthManifest(db, mrow.id, 'code_received', { last_error: result.error });
1189
+ errors++; log(` ✖ oauth-manifest #${mrow.id}: ${result.error}`);
1190
+ } else {
1191
+ // github_code: null on done too — a finished row holds no re-convertible code.
1192
+ await provisioning.resolveOAuthManifest(db, mrow.id, 'done', { app_id: result.appId, app_slug: result.appSlug, github_code: null });
1193
+ await provisioning.recordEvent(db, {
1194
+ instanceId: inst.id, ownerBuilderId: mrow.requested_by, event: 'oauth-manifest-done',
1195
+ detail: `sign-in wired via GitHub App Manifest (app ${result.appSlug || result.appId || '?'})`, actor: 'runner',
1196
+ }).catch(() => {});
1197
+ converted++;
1198
+ }
1199
+ } catch (e) {
1200
+ // GitHub's 404 means the code is used or past its hour: drop it, so "pick up where it
1201
+ // stopped" (task 1004652) never re-queues a dead code. Any other failure keeps it.
1202
+ const dead = /conversion failed: (Not Found|HTTP 404)/.test(String(e.message));
1203
+ await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: e.message, ...(dead ? { github_code: null } : {}) }).catch(() => {});
1204
+ errors++; log(` ✖ oauth-manifest #${mrow.id}: ${e.message}`);
1205
+ }
1206
+ }
1207
+ if (converted || errors) log(`oauth-manifest — converted=${converted} errors=${errors}`);
1208
+ return { converted, errors };
1209
+ }
1210
+
1211
+ // ---------------------------------------------------------------------------
1212
+ // run-intents — drain provisioning_intents (the systemd-timer entry point).
1213
+ // Dry-run lists the pending queue; --apply claims +
1214
+ // executes each, with a poison-attempt cap. FOR UPDATE SKIP LOCKED (in
1215
+ // provisioning.claimNextIntent) makes overlapping ticks safe. It ALSO drains the
1216
+ // GitHub-App-Manifest conversion queue on the same tick (task 2080).
1217
+ // ---------------------------------------------------------------------------
1218
+ async function cmdRunIntents(deps) {
1219
+ const { db, provisioning, log, apply } = deps;
1220
+ const instanceRoot = deps.instanceRoot || process.env.BONGOS_INSTANCE_ROOT || process.cwd();
1221
+ log(runnerBanner({ runnerVersion: coreVersionSafe(), runnerRoot: REPO_ROOT, instanceVersion: installedCoreVersion(instanceRoot), instanceRoot }));
1222
+ if (!apply) {
1223
+ const { rows } = await db.query(
1224
+ `SELECT pi.*, i.slug FROM provisioning_intents pi
1225
+ JOIN provisioning_instances i ON i.id = pi.instance_id
1226
+ WHERE pi.state = 'pending' ORDER BY pi.created_at`);
1227
+ log(`provision intent runner (dry-run): ${rows.length} pending intent(s)`);
1228
+ for (const r of rows) log(` would ${r.action} ${r.slug} (intent #${r.id}, attempts=${r.attempts})`);
1229
+ if (rows.length === 0) log(' queue empty — exit clean');
1230
+ await drainOAuthManifests(deps);
1231
+ return { ran: 0 };
1232
+ }
1233
+ let ran = 0, ok = 0, errors = 0;
1234
+ for (;;) {
1235
+ const intent = await provisioning.claimNextIntent(db);
1236
+ if (!intent) break;
1237
+ ran++;
1238
+ if (intent.attempts > retry.maxAttemptsFor(intent, MAX_INTENT_ATTEMPTS)) { // a transient core move gets one try per backoff step (task 1004447)
1239
+ await provisioning.resolveIntent(db, intent.id, 'error', `exceeded ${retry.maxAttemptsFor(intent, MAX_INTENT_ATTEMPTS)} attempts`);
1240
+ errors++; log(` ✖ intent #${intent.id} retired (too many attempts)`); continue;
1241
+ }
1242
+ try {
1243
+ const inst = await provisioning.getInstanceById(db, intent.instance_id);
1244
+ if (!inst) { await provisioning.resolveIntent(db, intent.id, 'error', 'instance row vanished'); errors++; continue; }
1245
+ // Dispatch EXPLICITLY per action — an unrecognized action must surface as an
1246
+ // error, never fall through to the destructive teardown leg (task 1002692).
1247
+ let result;
1248
+ if (intent.action === 'provision') result = await require('./provision-render.js').afterProvision(inst, deps, intent, await provisionInstance(inst, deps)); // then the app on Render, when the wizard lent a key (task 1004352)
1249
+ else if (intent.action === 'teardown') result = await teardownInstance(inst, deps);
1250
+ else if (intent.action === 'domain-attach') result = await domainAttachInstance(inst, deps);
1251
+ else if (intent.action === 'settings-apply') result = await settingsApplyInstance(inst, deps);
1252
+ else if (intent.action === 'restart') result = await restartInstance(inst, deps);
1253
+ else if (intent.action === 'disconnect') result = await disconnectInstance(inst, deps);
1254
+ else if (intent.action === 'core-upgrade') result = await coreUpgradeInstance(inst, deps, intent); // also reads the INTENT: the version the owner previewed (ADR 0293 D4)
1255
+ else if (intent.action === 'core-upgrade-preflight') result = await preflightCoreUpgrade(inst, deps, intent); // changes nothing; caches what the move WOULD say (task 1004121)
1256
+ else if (intent.action === 'repo-private' || intent.action === 'render-standup') result = intent.action === 'repo-private' ? await require('./provision-repo-private.js').repoPrivateInstance(inst, deps) : await require('./provision-render.js').renderStandupInstance(inst, deps, intent); // wire the deploy key, THEN flip (task 1004193) · spend the borrowed Render key once (task 1004183); required here — this file is at the size ratchet
1257
+ else result = { ok: false, error: `unknown_action '${intent.action}' — runner core ${coreVersionSafe() || '?'} does not know it (runner too old?); refusing to guess` };
1258
+ if (result && result.ok === false) {
1259
+ // WAIT OR RETIRE (task 1004447, intent-retry.js): a transient failure waits ~15m/1h/6h; a
1260
+ // classified non-transient one is retired at once (retrying cannot change its answer);
1261
+ // an unclassified one keeps its budget but never re-runs in the same tick.
1262
+ const fix = await require('./wedge-remedy.js').remedyAfterFailure({ intent, inst, result, deps }); // a KNOWN snag: fix it, try once more (task 1004448)
1263
+ if (fix.repended) { errors++; continue; }
1264
+ Object.assign(result, { failure: fix.failure, error: fix.error });
1265
+ const next = retry.retryDecision({ attempts: intent.attempts, failure: result.failure, terminal: result.terminal, maxAttempts: MAX_INTENT_ATTEMPTS });
1266
+ if (next.action === 'retire') { // terminal: a leg that must not be retried unattended (the Render leg's key is already gone — task 1004183)
1267
+ await provisioning.resolveIntent(db, intent.id, 'error', result.error || 'op failed');
1268
+ // A retired intent stops silently otherwise (F19): record the reason
1269
+ // on the instance WITHOUT changing its lifecycle status — passing the
1270
+ // CURRENT status makes the status write a no-op, so a healthy active
1271
+ // instance is never marked broken over a domain hiccup, but the card
1272
+ // can say why the work stopped.
1273
+ await provisioning.setInstanceStatus(db, intent.instance_id, inst.status, { error_note: result.error || 'op failed', error_note_action: intent.action }).catch(() => {});
1274
+ if (result.failure) await recordFailureClass(db, intent.id, result.failure); // WHAT KIND of failure, beside the sentence (task 1004446)
1275
+ await require('./move-escalation.js').afterRetire({ intent, inst, result, deps }); // nobody can heal it: one blocker + one notice per move (task 1004449)
1276
+ } else {
1277
+ await db.query(`UPDATE provisioning_intents SET state='pending', last_error=$2, not_before = now() + ($3 * interval '1 millisecond'), updated_at=now() WHERE id=$1`, [intent.id, result.error || 'op failed', next.delayMs]);
1278
+ if (result.failure) await recordFailureClass(db, intent.id, result.failure);
1279
+ }
1280
+ errors++;
1281
+ } else { await provisioning.resolveIntent(db, intent.id, 'done', (result && result.doneNote) || null); ok++; await require('./move-escalation.js').afterSuccess({ intent, inst, result, deps }); } // done WITH a note: it landed, and this is what it could not finish (task 1004065)
1282
+ } catch (e) {
1283
+ await provisioning.resolveIntent(db, intent.id, 'error', e.message).catch(() => {});
1284
+ // Surface the failure on the instance too, so a stuck instance is visible.
1285
+ await provisioning.setInstanceStatus(db, intent.instance_id, 'error', { error_note: e.message, error_note_action: intent.action }).catch(() => {});
1286
+ errors++; log(` ✖ intent #${intent.id}: ${e.message}`);
1287
+ }
1288
+ }
1289
+ await snapshotAllCoreVersions(deps); // refresh what the deploy control reads, AFTER the drain (ADR 0293 D3/D6)
1290
+ await require('./provision-auto-upgrade.js').autoUpgradeTick(deps); // then act on each hosted project's update rule: preflight, then move, one per tick (task 1004729, ADR 0381)
1291
+ const m = await drainOAuthManifests(deps); // same tick: convert + place any GitHub-App-Manifest creds awaiting the control plane
1292
+ await require('./provision-github-setup.js').verifyGithubAppSetups(deps).catch((e) => log(` github-setup check failed (non-blocking): ${e.message}`)); // install + device flow, asked of GitHub with the app's key (task 1004649)
1293
+ log(`done — ran=${ran} ok=${ok} errors=${errors}; oauth-manifest converted=${m.converted} errors=${m.errors}`);
1294
+ return { ran, ok, errors, manifests: m };
1295
+ }
1296
+
1297
+ // A light drift reconcile: surface instances stuck mid-flight (provisioning /
1298
+ // tearing_down) with no open intent — a runner died mid-op. Reports; --apply
1299
+ // re-enqueues a fresh intent so the next tick retries. (Full box-vs-DB drift
1300
+ // is a later hardening.)
1301
+ async function cmdReconcile(deps) {
1302
+ const { db, provisioning, log, apply } = deps;
1303
+ const { rows } = await db.query(
1304
+ `SELECT i.* FROM provisioning_instances i
1305
+ WHERE i.status IN ('provisioning','tearing_down')
1306
+ AND NOT EXISTS (SELECT 1 FROM provisioning_intents pi
1307
+ WHERE pi.instance_id = i.id AND pi.state IN ('pending','running'))`);
1308
+ log(`reconcile: ${rows.length} instance(s) stuck mid-flight with no open intent`);
1309
+ for (const inst of rows) {
1310
+ const action = inst.status === 'tearing_down' ? 'teardown' : 'provision';
1311
+ log(` ${inst.slug} [${inst.status}] → re-enqueue ${action}`);
1312
+ if (apply) await provisioning.enqueueIntent(db, inst.id, action, 'runner:reconcile');
1313
+ }
1314
+ return { stuck: rows.length };
1315
+ }
1316
+
1317
+ async function cmdStatus(deps) {
1318
+ const { db, log } = deps;
1319
+ const { rows } = await db.query(
1320
+ `SELECT i.slug, i.status, i.hosting_shape, i.port, i.domain, i.cost_estimate_usd, b.github_login
1321
+ FROM provisioning_instances i JOIN builders b ON b.id = i.owner_builder_id
1322
+ ORDER BY i.updated_at DESC LIMIT 200`);
1323
+ if (rows.length === 0) { log('no provisioned instances yet.'); return { count: 0 }; }
1324
+ log('provisioned instances:');
1325
+ let monthly = 0;
1326
+ for (const r of rows) {
1327
+ monthly += Number(r.cost_estimate_usd || 0);
1328
+ log(` ${String(r.slug).padEnd(20)} ${String(r.status).padEnd(13)} ${r.hosting_shape.padEnd(10)} port=${r.port || '-'} @${r.github_login} $${Number(r.cost_estimate_usd || 0).toFixed(2)}/mo ${r.domain || ''}`);
1329
+ }
1330
+ log(`\n estimated run-rate: ~$${monthly.toFixed(2)}/mo\n${await statusCapacityLine(db)}`); // + "box at N of M" (ADR 0285, task 1003927)
1331
+ return { count: rows.length };
1332
+ }
1333
+
1334
+ // ---------------------------------------------------------------------------
1335
+ // CLI
1336
+ // ---------------------------------------------------------------------------
1337
+ function usage() {
1338
+ console.log(`provision.js — instance-provisioning control-plane runner (ADR 0111, #1944)
1339
+
1340
+ Usage:
1341
+ node scripts/bongos/provision.js <command> [slug] [--apply]
1342
+
1343
+ Commands:
1344
+ run-intents drain provisioning_intents (the systemd-timer entry point):
1345
+ execute each pending provision / teardown / domain-attach /
1346
+ settings-apply intent
1347
+ provision <slug> provision one instance by slug (composes the standup runbook)
1348
+ teardown <slug> tear down one instance by slug
1349
+ reconcile re-enqueue instances stuck mid-flight with no open intent
1350
+ status list provisioned instances + estimated run-rate
1351
+ apply-caps cap every active hosted project's memory/CPU/tasks live (task 1004414)
1352
+ dns-check <domain> PREFLIGHT: verify the control-plane Cloudflare token can reach
1353
+ the domain's zone BEFORE the address step (network-only, no DB)
1354
+
1355
+ DRY-RUN by default — nothing mutates without --apply. The LIVE leg (real createdb /
1356
+ migrate / systemctl / caddy / DigitalOcean) needs the DO/Cloudflare tokens from
1357
+ /etc/<inst>/box.env (systemd EnvironmentFile=) and runs on the control plane only.
1358
+
1359
+ Set PROVISION_SUDO=1 (the control-plane runner unit does) to escalate the privileged
1360
+ steps via sudo — required when the runner runs as a non-root user (ADR 0128).
1361
+
1362
+ Set PROVISION_REMOTE_HOST=<user@host> (+ PROVISION_REMOTE_CHECKOUT, PROVISION_PUBLIC_IP)
1363
+ to run the box-mutating steps on a REMOTE co-hosting box over ssh, so the control
1364
+ plane can orchestrate co-tenant workloads that live there without that box holding
1365
+ any cloud token (ADR 0130). The Cloudflare DNS hook always stays on the control plane.
1366
+
1367
+ STANDALONE instances (their own repo dir, ADR 0108) run control-plane-LOCAL even under
1368
+ PROVISION_REMOTE_HOST — their scaffolded dir lives on the control plane, not the remote
1369
+ box (task 2074). Set PROVISION_CONTROL_PLANE_IP to this box's own public IP so a
1370
+ standalone instance's A record points HERE, not at the co-hosting box's PROVISION_PUBLIC_IP.`);
1371
+ }
1372
+
1373
+ // Store an intent's failure class + reason code (migration provisioning_030, task 1004446),
1374
+ // only for a result that carries one. Best-effort: a runner on a schema without the columns
1375
+ // must still drain, so a failed write is swallowed — last_error still says it either way. A
1376
+ // class left on a row that later lands 'done' is harmless: readers ignore it on a done row.
1377
+ async function recordFailureClass(db, intentId, failure) {
1378
+ await db.query('UPDATE provisioning_intents SET failure_class = $2, failure_reason = $3 WHERE id = $1',
1379
+ [intentId, failure ? failure.class : null, failure ? failure.reason : null]).catch(() => {});
1380
+ }
1381
+
1382
+ async function main() {
1383
+ const args = process.argv.slice(2);
1384
+ const cmd = args[0];
1385
+ const apply = hasFlag(args, '--apply');
1386
+ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { usage(); process.exit(0); }
1387
+
1388
+ // dns-check <domain> — the DNS token-scope PREFLIGHT (task 1980). Network-only + no
1389
+ // DB, so onboarding can run it BEFORE the connect-address step (and before any
1390
+ // provision). Exits 0 if the token can reach the domain's zone, 1 with guidance if not.
1391
+ if (cmd === 'dns-check') {
1392
+ const domain = args[1] && !args[1].startsWith('--') ? args[1] : null;
1393
+ if (!domain) { console.error('dns-check needs a <domain>'); process.exit(2); }
1394
+ const scope = await checkDnsTokenScope(domain);
1395
+ if (scope.reachable) { console.log(`✓ DNS token can reach zone ${scope.zone} for ${domain} — ${scope.reason}`); process.exit(0); }
1396
+ console.error(`✖ DNS token cannot reach zone ${scope.zone} for ${domain}: ${scope.reason}`);
1397
+ console.error(` → ${scope.guidance}`);
1398
+ process.exit(1);
1399
+ }
1400
+
1401
+ const known = ['run-intents', 'provision', 'teardown', 'reconcile', 'status', 'apply-caps'];
1402
+ if (!known.includes(cmd)) { console.error(`unknown command: ${cmd}\n`); usage(); process.exit(2); }
1403
+ const slug = args[1] && !args[1].startsWith('--') ? args[1] : null;
1404
+ if ((cmd === 'provision' || cmd === 'teardown') && !slug) { console.error(`${cmd} needs a <slug>`); process.exit(2); }
1405
+
1406
+ let pool, provisioning, platformIdentity, hubKeys, projectCatalog;
1407
+ try { ({ pool, provisioning, platformIdentity, hubKeys, projectCatalog } = loadDeps()); }
1408
+ catch (e) { console.error(`cannot load DB layer (run this on the control plane): ${e.message}`); process.exit(1); }
1409
+
1410
+ // Box-target executor (remote over SSH when PROVISION_REMOTE_HOST is set, else
1411
+ // local) for the box-mutating steps; controlExec is ALWAYS local — the token
1412
+ // step (Cloudflare DNS hook) + the standalone repo scaffold run on the control
1413
+ // plane where the token + packaging tools live (task 2065, ADR 0130).
1414
+ // Remote mode needs an explicit checkout dir (task 1002756 — no founder default).
1415
+ if (CONFIG.remoteHost && !CONFIG.remoteCwd) { console.error('PROVISION_REMOTE_HOST is set but PROVISION_REMOTE_CHECKOUT is not — name the co-hosting box\'s checkout dir (the old hardcoded default was removed, task 1002756).'); process.exit(2); }
1416
+ const exec = makeExec({ apply, remoteHost: CONFIG.remoteHost, remoteCwd: CONFIG.remoteCwd, sshOpts: CONFIG.sshOpts });
1417
+ const controlExec = makeExec({ apply });
1418
+ const deps = {
1419
+ db: pool, provisioning, apply, privileged: CONFIG.privileged, log: console.log,
1420
+ exec, controlExec,
1421
+ writeFile: makeWriteFile({ apply, privileged: CONFIG.privileged, exec, remoteHost: CONFIG.remoteHost }),
1422
+ controlWriteFile: makeWriteFile({ apply, privileged: CONFIG.privileged, exec: controlExec }),
1423
+ // Cross-site identity (ADR 0141 §4). Null on a non-hub control plane → federation
1424
+ // dormant; shouldFederate() then skips and the instance owns its auth (as today).
1425
+ platformIdentity, hubKeys,
1426
+ projectCatalog, // the disconnect leg's catalog forget (task 1003249); null off the hub
1427
+ };
1428
+
1429
+ let result;
1430
+ try {
1431
+ if (cmd === 'run-intents') result = await cmdRunIntents(deps);
1432
+ else if (cmd === 'provision') { const inst = await provisioning.getInstanceBySlug(pool, slug); if (!inst) { console.error(`no instance with slug '${slug}'`); await pool.end().catch(() => {}); process.exit(1); } result = await provisionInstance(inst, deps); }
1433
+ else if (cmd === 'teardown') { const inst = await provisioning.getInstanceBySlug(pool, slug); if (!inst) { console.error(`no instance with slug '${slug}'`); await pool.end().catch(() => {}); process.exit(1); } result = await teardownInstance(inst, deps); }
1434
+ else if (cmd === 'reconcile') result = await cmdReconcile(deps);
1435
+ else if (cmd === 'status') result = await cmdStatus(deps);
1436
+ else if (cmd === 'apply-caps') result = await require('./provision-caps.js').cmdApplyCaps(deps);
1437
+ } catch (e) {
1438
+ console.error('fatal:', e && e.message ? e.message : e);
1439
+ await pool.end().catch(() => {});
1440
+ process.exit(1);
1441
+ }
1442
+ await pool.end().catch(() => {});
1443
+ process.exit(result && result.ok === false ? 1 : 0);
1444
+ }
1445
+
1446
+ if (require.main === module) main();
1447
+
1448
+ module.exports = {
1449
+ CONFIG, MAX_INTENT_ATTEMPTS, provisionerBotEmail,
1450
+ // pure step generators (tested directly)
1451
+ dbName, dbCreateCmd, migrateCmd, seedFirstVersionCmd, serviceUnit, serviceUnitPath, webEnvBody, webEnvPath,
1452
+ // per-instance unix account + password-authed PG role (task 1003369, audit B3)
1453
+ instanceUser, instanceStateDir, instanceDbRole, generateDbPassword, ensureInstanceUserCmd, grantInstanceRepoReadCmd, dbRoleCmd, dbRoleCmdShown, dbRoleGrantsCmd,
1454
+ backupUnitName, backupServicePath, backupTimerPath, backupScriptPath, backupService, backupTimer,
1455
+ standaloneRoot, standalonePullCmd, standaloneInstallCmd, standaloneMigrateCmd,
1456
+ // standalone scaffold (ADR 0108 / 0125, task 2055; consumer-core fix task 1002275)
1457
+ parseTargetRef, instanceRepoRemote, instanceInitSpec, installedCoreTarball, installedCorePackDir, scaffoldStandaloneRepo, coreVersionSafe,
1458
+ // re-provision core-pin refresh (gap-list W3, task 1003050)
1459
+ resolveVendorableCoreTarball, planCorePinRefresh, safeVersionLabel, buildCorePinRefreshCommit, refreshStandaloneCorePin,
1460
+ // the git-less core checkout — the self-host image's rung (task 1003995)
1461
+ coreCheckoutHasGit,
1462
+ // adopt vs greenfield onboarding (ADR 0121, task 1002562)
1463
+ onboardMode, alreadyScaffolded,
1464
+ // private-repo deploy key (ADR 0176, task 1002735)
1465
+ resolveOwnerGithubToken, deployKeyTitle, deployKeyPath, ensurePrivateRepoAccess,
1466
+ zoneForDomain, dnsUpsertEnv, classifyZoneScope, checkDnsTokenScope,
1467
+ caddyBlock, caddySnippetPath, healthzCmd, nextFreePort,
1468
+ // executors + orchestration (injectable deps for tests)
1469
+ makeExec, makeWriteFile, allocatePort, boundLocalPorts, provisionInstance, teardownInstance,
1470
+ domainAttachInstance, releasableDomains, cmdRunIntents, cmdReconcile, cmdStatus,
1471
+ // live settings push (task 1003140) + the in-place env patch + read-back it rides on
1472
+ settingsApplyInstance, upsertEnvVars, renamedEnvCarry, settingsEnvVarsFor, settingsConsumed, instanceManifestCmd,
1473
+ // restart an unreachable ACTIVE instance (task 1003238) — the settings leg minus the env patch
1474
+ restartInstance,
1475
+ // GitHub App Manifest flow (task 2080, ADR 0133)
1476
+ convertManifestCode, placeManifestCreds, drainOAuthManifests, authHealthzCmd, MANIFEST_UA,
1477
+ // cross-site identity federation (ADR 0141 §4, task #1002109)
1478
+ federationHubOrigin, shouldFederate, federateInstance,
1479
+ };