@bongos/core 1.21.174 → 1.21.176
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.
- package/.bongos-core.json +3565 -1515
- package/.claude/skills/ask-for-help/SKILL.md +4 -4
- package/.claude/skills/backlog-review/SKILL.md +1 -1
- package/.claude/skills/blocker-review/SKILL.md +2 -2
- package/.claude/skills/blocker-solve/SKILL.md +4 -4
- package/.claude/skills/bongos-feedback/SKILL.md +1 -1
- package/.claude/skills/bug-triage/SKILL.md +8 -8
- package/.claude/skills/builder-backup/SKILL.md +3 -3
- package/.claude/skills/builder-claim/SKILL.md +5 -5
- package/.claude/skills/builder-cost/SKILL.md +5 -5
- package/.claude/skills/builder-end/SKILL.md +6 -6
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-key/SKILL.md +6 -6
- package/.claude/skills/builder-reauth/SKILL.md +4 -4
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-release/SKILL.md +3 -3
- package/.claude/skills/builder-sequence/SKILL.md +8 -8
- package/.claude/skills/builder-setup/SKILL.md +4 -4
- package/.claude/skills/builder-ship/SKILL.md +7 -7
- package/.claude/skills/builder-stage/SKILL.md +8 -8
- package/.claude/skills/builder-start/SKILL.md +4 -4
- package/.claude/skills/collab-review/SKILL.md +5 -5
- package/.claude/skills/design/SKILL.md +6 -4
- package/.claude/skills/design-sync/SKILL.md +3 -3
- package/.claude/skills/figma-design-sync/SKILL.md +4 -4
- package/.claude/skills/fix-task/SKILL.md +8 -8
- package/.claude/skills/goal-close/SKILL.md +9 -9
- package/.claude/skills/goal-create/SKILL.md +18 -18
- package/.claude/skills/grade-recover/SKILL.md +8 -8
- package/.claude/skills/grade-sweep/SKILL.md +15 -15
- package/.claude/skills/idea-triage/SKILL.md +15 -15
- package/.claude/skills/merge-mode/SKILL.md +14 -14
- package/.claude/skills/new-project/SKILL.md +5 -5
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +14 -14
- package/.claude/skills/priority-session/SKILL.md +6 -6
- package/.claude/skills/read-session-export/SKILL.md +3 -3
- package/.claude/skills/recall/SKILL.md +7 -7
- package/.claude/skills/scan-before-install/SKILL.md +3 -3
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/ship-check/SKILL.md +3 -3
- package/.claude/skills/status/SKILL.md +3 -3
- package/.claude/skills/strand-fix/SKILL.md +6 -6
- package/.claude/skills/worktree-clean/SKILL.md +4 -4
- package/.gitattributes +12 -32
- package/GOVERNMENT.md +1 -1
- package/README.md +8 -8
- package/bin/bongos.js +23 -23
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +22 -4
- package/clients/bongos-client/index.cjs +22 -4
- package/clients/bongos-client/index.d.ts +33 -4
- package/clients/bongos-client/index.mjs +21 -3
- package/config/branding.neutral.json +1 -1
- package/config/design-tokens.schema.json +1 -1
- package/config/modules.neutral.json +1 -3
- package/docs/adr/0029-status-page-outage-resilience.md +6 -6
- package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +1 -1
- package/docs/adr/0294-cli-sessions-do-not-expire.md +1 -1
- package/docs/adr/0383-sessions-end-after-ten-idle-days.md +35 -0
- package/docs/adr/0384-the-raise-page-is-a-hub-module-builders-edit-in-place.md +31 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +606 -44
- package/docs/api-reference.md +25 -12
- package/docs/architecture.md +8 -7
- package/docs/canonical-permissions.md +3 -3
- package/docs/copy-inventory.md +1389 -1437
- package/docs/copy-registry.json +2160 -2634
- package/docs/design/demonolith-plan.md +6 -6
- package/docs/design/modular-architecture/00-research-report.md +1 -1
- package/docs/design/project-startup-direction.md +2 -2
- package/docs/design/projects-hub-direction-v2.md +1 -1
- package/docs/design/reviews/hall-v2/README.md +2 -2
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +89 -88
- package/docs/handoff-template.md +4 -4
- package/docs/module-api-changelog.md +6 -2
- package/docs/modules-contract.md +8 -7
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +1 -1
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +1 -1
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
- package/docs/onboarding/diagrams/04-architecture.mmd +1 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -1
- package/docs/onboarding/diagrams/README.md +2 -2
- package/docs/onboarding/diagrams/assertions.json +2 -2
- package/docs/onboarding/slash-commands.md +2 -2
- package/docs/packs/artist.md +4 -4
- package/docs/packs/engineer.md +11 -10
- package/docs/packs/ideator.md +7 -7
- package/docs/page-inventory.json +25 -1
- package/docs/page-readings.json +4388 -4602
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/audit-replay.md +4 -4
- package/docs/recipes/autobongos-windows-host.md +6 -6
- package/docs/recipes/bongos-cli-release.md +2 -2
- package/docs/recipes/claude-design-loop.md +7 -7
- package/docs/recipes/core-release-pipeline.md +19 -19
- package/docs/recipes/failed-ship-recovery-proof.md +2 -2
- package/docs/recipes/figma-design-loop.md +6 -6
- package/docs/recipes/gds-db-backup.md +9 -9
- package/docs/recipes/instance-deploy.md +3 -3
- package/docs/recipes/instance-first-run.md +1 -1
- package/docs/recipes/multi-builder-merge.md +2 -2
- package/docs/recipes/ops-gotchas.md +10 -9
- package/docs/recipes/packaging-the-core.md +6 -6
- package/docs/recipes/parallel-goal-waves.md +4 -4
- package/docs/recipes/private-npm-distribution.md +9 -9
- package/docs/recipes/redteam-patrol.md +5 -5
- package/docs/recipes/render-pilot.md +2 -2
- package/docs/recipes/reranking-a-goals-board.md +2 -2
- package/docs/recipes/search-index-ingestion.md +5 -5
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/ship-and-regrade.md +4 -4
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/standalone-live-docs.md +1 -1
- package/docs/recipes/ui-look-before-you-ship.md +2 -2
- package/docs/recipes/upgrading-the-core.md +11 -11
- package/docs/recipes/windows-builders.md +18 -18
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +6 -6
- package/docs/research/task-network-map-readability.md +3 -3
- package/migrations/core_278_builder_tester_tag.sql +18 -0
- package/migrations/core_279_bongos_shipper_new_scripts_path.sql +13 -0
- package/modules/agents/lib/answer-hold.js +1 -1
- package/modules/agents/lib/gate.js +1 -1
- package/modules/agents/lib/validate.js +1 -1
- package/modules/autonomy/gauge.js +1 -1
- package/modules/autonomy/governor-docket.js +2 -2
- package/modules/autonomy/pollers/routine-timers.js +3 -3
- package/modules/autonomy/routes/autonomy.js +3 -3
- package/modules/builder-settings/interaction-prefs.js +1 -1
- package/modules/builder-settings/render-prefs.js +1 -1
- package/modules/builder-settings/routes/settings.js +1 -1
- package/modules/builder-settings/skill-prefs.js +2 -2
- package/modules/builder-settings/wandering-prefs.js +2 -2
- package/modules/copy-desk/module.json +2 -2
- package/modules/copy-desk/page-data.js +5 -5
- package/modules/copy-desk/page-decisions.js +13 -1
- package/modules/copy-desk/proposals.js +4 -4
- package/modules/copy-desk/registry.js +2 -2
- package/modules/copy-desk/routes/copy-desk-release.js +119 -0
- package/modules/copy-desk/routes/copy-desk.js +3 -12
- package/modules/copy-desk/skills/tweak/SKILL.md +12 -12
- package/modules/copy-desk/tests/copy_flags.mjs +1 -1
- package/modules/copy-desk/tests/copy_no_cms.mjs +7 -3
- package/modules/copy-desk/tests/copy_proposals.mjs +2 -2
- package/modules/copy-desk/tests/copy_queue.mjs +1 -1
- package/modules/design-styles/skills/README.md +4 -4
- package/modules/design-styles/skills/imagegen-frontend-mobile/SKILL.md +1 -1
- package/modules/design-styles/skills/imagegen-frontend-web/SKILL.md +1 -1
- package/modules/discord/bug-attachments.js +1 -1
- package/modules/discord/ship-broadcast.js +1 -1
- package/modules/economy/achievements.js +49 -7
- package/modules/economy/cost-classify.js +6 -6
- package/modules/economy/cost-report.js +1 -1
- package/modules/economy/cost.js +1 -1
- package/modules/economy/credits.js +2 -1
- package/modules/economy/idea-size.js +2 -2
- package/modules/economy/reward.js +2 -0
- package/modules/economy/routes/cost.js +1 -1
- package/modules/economy/streak.js +43 -4
- package/modules/government/asks.js +1 -1
- package/modules/government/board.js +19 -2
- package/modules/government/catalog.js +3 -0
- package/modules/government/migrations/government_033_idea_vote.sql +46 -0
- package/modules/government/protected-surfaces.json +48 -47
- package/modules/government/recommendations.js +6 -2
- package/modules/government/routes/government.js +1 -1
- package/modules/grading/generated-provenance.js +13 -5
- package/modules/grading/grade-engine.js +1 -1
- package/modules/grading/grader-rubric.json +1 -1
- package/modules/grading/grader-score.js +1 -1
- package/modules/grading/grader-workers/index.js +1 -1
- package/modules/grading/grader.js +1 -1
- package/modules/grading/routes/grade.js +1 -1
- package/modules/hall-ui/public/atlas.html +5 -7
- package/modules/hall-ui/public/atlas.js +1 -1
- package/modules/hall-ui/public/atlas.states.json +1 -1
- package/modules/hall-ui/public/blockers.css +9 -0
- package/modules/hall-ui/public/blockers.html +6 -4
- package/modules/hall-ui/public/blockers.js +39 -27
- package/modules/hall-ui/public/board-room.css +5 -12
- package/modules/hall-ui/public/board-room.html +6 -6
- package/modules/hall-ui/public/board-room.js +41 -23
- package/modules/hall-ui/public/board-room.states.json +1 -1
- package/modules/hall-ui/public/brand-apply.js +1 -1
- package/modules/hall-ui/public/brand-holes.js +1 -1
- package/modules/hall-ui/public/builders.js +40 -86
- package/modules/hall-ui/public/city-decide.js +18 -4
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +6 -9
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/copy-desk.js +2 -2
- package/modules/hall-ui/public/craft-mode.js +4 -0
- package/modules/hall-ui/public/deploy.html +6 -2
- package/modules/hall-ui/public/deploy.js +21 -13
- package/modules/hall-ui/public/diagrams.css +1 -1
- package/modules/hall-ui/public/diagrams.html +5 -8
- package/modules/hall-ui/public/diagrams.states.json +1 -1
- package/modules/hall-ui/public/drachmae.html +3 -7
- package/modules/hall-ui/public/fleet.html +3 -2
- package/modules/hall-ui/public/fleet.js +0 -4
- package/modules/hall-ui/public/gate.html +7 -7
- package/modules/hall-ui/public/gate.js +58 -77
- package/modules/hall-ui/public/goal-inbox.js +69 -86
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals-page.js +5 -5
- package/modules/hall-ui/public/goals-render.js +10 -11
- package/modules/hall-ui/public/goals.css +10 -0
- package/modules/hall-ui/public/goals.html +14 -7
- package/modules/hall-ui/public/government.html +3 -5
- package/modules/hall-ui/public/government.js +1 -5
- package/modules/hall-ui/public/hall-ask.css +57 -0
- package/modules/hall-ui/public/hall-ask.js +50 -0
- package/modules/hall-ui/public/hall-render.js +117 -57
- package/modules/hall-ui/public/hall-widgets.js +11 -11
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +3 -1
- package/modules/hall-ui/public/ideas.js +1 -0
- package/modules/hall-ui/public/index.html +15 -6
- package/modules/hall-ui/public/index.states.json +1 -1
- package/modules/hall-ui/public/mine-lens.js +1 -1
- package/modules/hall-ui/public/modules.html +7 -10
- package/modules/hall-ui/public/modules.js +0 -2
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/palette.js +5 -5
- package/modules/hall-ui/public/primer.html +4 -9
- package/modules/hall-ui/public/profile-connections.js +10 -2
- package/modules/hall-ui/public/profile-roles.js +3 -10
- package/modules/hall-ui/public/profile.html +12 -10
- package/modules/hall-ui/public/profile.js +13 -17
- package/modules/hall-ui/public/project-settings.html +3 -4
- package/modules/hall-ui/public/project-settings.js +0 -3
- package/modules/hall-ui/public/ranks.html +5 -11
- package/modules/hall-ui/public/ranks.js +6 -8
- package/modules/hall-ui/public/roadmap.css +0 -2
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roadmap.js +16 -51
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +6 -8
- package/modules/hall-ui/public/sessions.js +7 -7
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell-trail.js +1 -1
- package/modules/hall-ui/public/shell.js +17 -17
- package/modules/hall-ui/public/street-deck.css +2 -1
- package/modules/hall-ui/public/street-deck.js +10 -2
- package/modules/hall-ui/public/street-desk.js +16 -5
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio-lib.js +4 -0
- package/modules/hall-ui/public/studio.css +2 -0
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/studio.js +27 -1
- package/modules/hall-ui/public/style.css +43 -26
- package/modules/hall-ui/public/task-detail.js +10 -7
- package/modules/hall-ui/public/task.css +2 -0
- package/modules/hall-ui/public/task.html +3 -1
- package/modules/hall-ui/public/task.js +23 -7
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/vote-control.js +208 -0
- package/modules/hall-ui/public/watch.html +3 -5
- package/modules/hall-ui/public/watch.js +5 -4
- package/modules/hall-ui/public/welcome-boot.css +130 -0
- package/modules/hall-ui/public/welcome-boot.js +334 -0
- package/modules/hall-ui/public/welcome-day.webp +0 -0
- package/modules/hall-ui/public/welcome-night.webp +0 -0
- package/modules/hall-ui/public/work-summary.js +1 -1
- package/modules/hall-ui/public/work.css +1 -0
- package/modules/hall-ui/public/work.html +3 -1
- package/modules/hall-ui/public/work.js +2 -1
- package/modules/hall-ui/records/accomplishments.md +16 -0
- package/modules/hall-ui/records/hall-ask-card.md +33 -0
- package/modules/hall-ui/records/karma-vote-control.md +13 -0
- package/modules/hall-ui/records/oversight-family.md +1 -1
- package/modules/hall-ui/records/profile-connections.md +2 -0
- package/modules/hall-ui/records/profile-roles.md +1 -1
- package/modules/hall-ui/records/reading-rooms.md +1 -1
- package/modules/hall-ui/records/welcome-tower.md +50 -0
- package/modules/ideas/migrations/ideas_011_idea_votes.sql +31 -0
- package/modules/ideas/projection.js +1 -1
- package/modules/ideas/routes/blockers.js +3 -3
- package/modules/ideas/routes/inbox.js +29 -1
- package/modules/ideas/routes/ratify-goal.js +1 -1
- package/modules/ideas/routing.js +1 -1
- package/modules/ideas/templates.js +1 -1
- package/modules/ideas/trail-descent.js +1 -1
- package/modules/ideas/trail-size.js +1 -1
- package/modules/ideas/votes.js +93 -0
- package/modules/lifecycle/abandon-leftovers.js +1 -1
- package/modules/lifecycle/cascade.js +1 -1
- package/modules/lifecycle/conflict-resolve.js +13 -21
- package/modules/lifecycle/criterion-ref.js +1 -1
- package/modules/lifecycle/db-analytics.js +57 -4
- package/modules/lifecycle/db-deps-criteria.js +1 -1
- package/modules/lifecycle/db-ship.js +2 -2
- package/modules/lifecycle/db-tasks.js +2 -2
- package/modules/lifecycle/db-versions.js +1 -1
- package/modules/lifecycle/db.js +2 -0
- package/modules/lifecycle/dead-deps.js +3 -3
- package/modules/lifecycle/dependency-advisory.js +3 -3
- package/modules/lifecycle/goal-category.js +1 -1
- package/modules/lifecycle/goal-forest.js +1 -1
- package/modules/lifecycle/goal-task-relevance-advisory.js +1 -1
- package/modules/lifecycle/kickoff-checklist.js +1 -1
- package/modules/lifecycle/kickoff-seed.js +1 -1
- package/modules/lifecycle/lifecycle.js +2 -0
- package/modules/lifecycle/merge-reach.js +29 -15
- package/modules/lifecycle/missing-dep-classify.js +3 -3
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/page-tweak-approve.js +85 -1
- package/modules/lifecycle/page-tweak-hold.js +1 -1
- package/modules/lifecycle/publish-reconciler.js +1 -2
- package/modules/lifecycle/rot.js +1 -1
- package/modules/lifecycle/routes/artist-gate.js +1 -1
- package/modules/lifecycle/routes/gate-approvals.js +22 -1
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/task-route-helpers.js +1 -1
- package/modules/lifecycle/routes/task-votes.js +54 -0
- package/modules/lifecycle/routes/task-write-routes.js +1 -1
- package/modules/lifecycle/routes/tasks.js +2 -2
- package/modules/lifecycle/routes/versions.js +2 -2
- package/modules/lifecycle/ship-card.js +4 -4
- package/modules/lifecycle/ship-preflight.js +1 -1
- package/modules/lifecycle/task-classifier.js +1 -1
- package/modules/lifecycle/task-status.js +1 -1
- package/modules/lifecycle/task-visual-db.js +1 -1
- package/modules/memory/learnings.js +1 -1
- package/modules/memory/pollers/search-index.js +2 -2
- package/modules/memory/routes/learnings.js +1 -1
- package/modules/memory/routes/memory.js +1 -1
- package/modules/memory/search.js +2 -2
- package/modules/npm-release/release.js +1 -1
- package/modules/onboarding/howto-moments.js +1 -1
- package/modules/onboarding/newcomer-restock.js +3 -3
- package/modules/onboarding/onboarding.js +1 -1
- package/modules/pixel-art/skills/otb-tile-generate/SKILL.md +1 -1
- package/modules/platform-identity/origin-containment.js +1 -1
- package/modules/platform-identity/publish-gate.js +1 -1
- package/modules/platform-identity/tests/platform-identity.mjs +1 -1
- package/modules/provisioning/capacity.js +1 -1
- package/modules/provisioning/core-upgrade.js +15 -9
- package/modules/provisioning/credential-preflight.js +3 -3
- package/modules/provisioning/disconnect.js +1 -1
- package/modules/provisioning/dns-resolves.js +1 -1
- package/modules/provisioning/module.json +1 -1
- package/modules/provisioning/onboard-plan.js +1 -1
- package/modules/provisioning/paid-shape-gate.js +2 -2
- package/modules/provisioning/planet-physics.js +11 -2
- package/modules/provisioning/pollers/liveness-sweep.js +1 -1
- package/modules/provisioning/pollers/runner-drift-notify.js +5 -5
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/render-lookup.js +1 -1
- package/modules/provisioning/render-standup.js +3 -3
- package/modules/provisioning/repo-private.js +1 -1
- package/modules/provisioning/routes/callback-page.js +1 -1
- package/modules/provisioning/routes/core-upgrade.js +23 -3
- package/modules/provisioning/routes/provisioning.js +2 -2
- package/modules/provisioning/routes/render-standup.js +1 -1
- package/modules/provisioning/routes/repo-private.js +1 -1
- package/modules/provisioning/soft-limits.js +1 -1
- package/modules/provisioning/starter-bundles.js +4 -2
- package/modules/provisioning/tests/provisioning.mjs +1 -1
- package/modules/public-landing/public/account.html +4 -6
- package/modules/public-landing/public/assets/cosmos.css +13 -6
- package/modules/public-landing/public/connections.html +4 -6
- package/modules/public-landing/public/contact.html +4 -6
- package/modules/public-landing/public/guild.html +4 -6
- package/modules/public-landing/public/index.html +11 -9
- package/modules/public-landing/public/invitations.html +4 -6
- package/modules/public-landing/public/privacy.html +4 -6
- package/modules/public-landing/public/projects.html +168 -46
- package/modules/public-landing/public/terms.html +4 -6
- package/modules/raise/content/copy.json +40 -0
- package/modules/raise/content/nda.html +39 -0
- package/modules/raise/content/settings.json +49 -0
- package/modules/raise/db.js +95 -0
- package/modules/raise/migrations/raise_001_tables.sql +71 -0
- package/modules/raise/module.json +28 -0
- package/modules/raise/page.js +194 -0
- package/modules/raise/public/edit.js +152 -0
- package/modules/raise/public/favicon.svg +1 -0
- package/modules/raise/public/logo-ink.svg +1 -0
- package/modules/raise/public/logo-night.svg +1 -0
- package/modules/raise/public/mode.js +8 -0
- package/modules/raise/public/raise.css +234 -0
- package/modules/raise/public/raise.js +91 -0
- package/modules/raise/raise.js +174 -0
- package/modules/raise/routes/raise.js +152 -0
- package/modules/raise/views/gate.html +39 -0
- package/modules/raise/views/nda.html +21 -0
- package/modules/raise/views/page.html +133 -0
- package/modules/raise/web.js +72 -0
- package/modules/render-deploy/deploys.js +1 -1
- package/modules/render-deploy/render.js +2 -2
- package/modules/security/secret-scrub.js +3 -3
- package/modules/security/security.js +1 -1
- package/modules/security/tests/artifact_scan.mjs +1 -1
- package/modules/security/tests/mirror.mjs +1 -1
- package/modules/security/tests/scan_store.mjs +1 -1
- package/modules/sessions/db.js +2 -2
- package/modules/sessions/pulse.js +2 -2
- package/modules/sessions/routes/sessions.js +1 -1
- package/modules/specialities/specialities.js +1 -1
- package/modules/status-ui/public/README.md +3 -4
- package/modules/status-ui/public/index.html +5 -31
- package/modules/status-ui/public/status.js +6 -174
- package/modules/status-ui/public/style.css +4 -133
- package/modules/ui-design/docs/design-contract.md +3 -3
- package/modules/ui-design/kit/README.md +1 -1
- package/modules/ui-design/kit/serve.js +13 -4
- package/modules/ui-design/scripts/design-sync.js +1 -1
- package/modules/ui-design/scripts/figma-design-sync.js +1 -1
- package/modules/ui-design/scripts/validate-design.js +1 -1
- package/package-lock.json +27 -11
- package/package.json +2 -3
- package/release-notes.json +178 -0
- package/scripts/bongos/CLAUDE.md +15 -0
- package/scripts/bongos/abandon.js +137 -0
- package/scripts/bongos/accessibility-replay.js +195 -0
- package/scripts/bongos/address-park-sweep.sh +91 -0
- package/scripts/bongos/adopt-detect.js +123 -0
- package/scripts/bongos/adopt-import.js +111 -0
- package/scripts/bongos/adopt-preflight.js +187 -0
- package/scripts/bongos/adr-namespace.js +273 -0
- package/scripts/bongos/agent-invoke.js +689 -0
- package/scripts/bongos/agents-sync.js +502 -0
- package/scripts/bongos/api-alias-caller-scan.js +256 -0
- package/scripts/bongos/api-artifacts.js +91 -0
- package/scripts/bongos/api.js +86 -0
- package/scripts/bongos/api.ps1 +26 -0
- package/scripts/bongos/architect-audit.js +294 -0
- package/scripts/bongos/artifact-audit.js +326 -0
- package/scripts/bongos/artifact-format.js +203 -0
- package/scripts/bongos/artifact-staleness.js +267 -0
- package/scripts/bongos/ask.js +231 -0
- package/scripts/bongos/audit-authorship.sh +329 -0
- package/scripts/bongos/audit-deps.js +189 -0
- package/scripts/bongos/audit-gate.js +121 -0
- package/scripts/bongos/audit-rank-roles.js +172 -0
- package/scripts/bongos/audit-replay.sh +205 -0
- package/scripts/bongos/autobongos-digest.js +123 -0
- package/scripts/bongos/autobongos-grade-cap.js +187 -0
- package/scripts/bongos/autobongos-loop.js +210 -0
- package/scripts/bongos/autobongos-run.js +1361 -0
- package/scripts/bongos/autobongos-service.cmd +50 -0
- package/scripts/bongos/autobongos-service.ps1 +270 -0
- package/scripts/bongos/autobongos-tweak-lane.js +262 -0
- package/scripts/bongos/autobongos-verify.js +618 -0
- package/scripts/bongos/autonomy-gate.js +135 -0
- package/scripts/bongos/autonomy-gauge-statusline.js +124 -0
- package/scripts/bongos/autonomy-gauge.js +84 -0
- package/scripts/bongos/backfill-cost.js +176 -0
- package/scripts/bongos/backfill-disciplines.js +144 -0
- package/scripts/bongos/backfill-session-earnings.js +123 -0
- package/scripts/bongos/backfill-session-types.js +87 -0
- package/scripts/bongos/backfill-sessions.js +411 -0
- package/scripts/bongos/backfill-subagent-rewards.js +485 -0
- package/scripts/bongos/backfill-task-rewards.js +413 -0
- package/scripts/bongos/backlog-review.js +399 -0
- package/scripts/bongos/backup-dropin-sweep.sh +95 -0
- package/scripts/bongos/backup.js +116 -0
- package/scripts/bongos/baseline-staleness.js +217 -0
- package/scripts/bongos/bfg-principal-token.js +65 -0
- package/scripts/bongos/bug-attachment-audit.js +149 -0
- package/scripts/bongos/bug-attachment-recover.js +198 -0
- package/scripts/bongos/bug-triage.js +292 -0
- package/scripts/bongos/build-bongos.js +94 -0
- package/scripts/bongos/bump-version.js +221 -0
- package/scripts/bongos/capture.js +481 -0
- package/scripts/bongos/category-advisory-guard.js +206 -0
- package/scripts/bongos/ci-grade.js +82 -0
- package/scripts/bongos/claim-recall.js +112 -0
- package/scripts/bongos/claim.js +789 -0
- package/scripts/bongos/classify-discipline.js +153 -0
- package/scripts/bongos/claude-materialize.js +1200 -0
- package/scripts/bongos/claude-md-template.js +142 -0
- package/scripts/bongos/cleanup-orphans.js +74 -0
- package/scripts/bongos/cli-lib.js +1226 -0
- package/scripts/bongos/client-baseurl-guard.js +116 -0
- package/scripts/bongos/codemod-rename-scripts-gds.js +381 -0
- package/scripts/bongos/codemod-rename-src-gds.js +145 -0
- package/scripts/bongos/conductor-dispatch.js +535 -0
- package/scripts/bongos/context-pack.js +326 -0
- package/scripts/bongos/control-manifest.js +199 -0
- package/scripts/bongos/copy-apply.js +758 -0
- package/scripts/bongos/copy-inventory.js +571 -0
- package/scripts/bongos/core-origin-follow.js +185 -0
- package/scripts/bongos/core-pin-sync.js +260 -0
- package/scripts/bongos/coreB-carve-proof.sh +725 -0
- package/scripts/bongos/cost-report.js +63 -0
- package/scripts/bongos/cost.js +47 -0
- package/scripts/bongos/credential-presence.js +296 -0
- package/scripts/bongos/cutover-codemod.js +240 -0
- package/scripts/bongos/db-backup-nightly.sh +232 -0
- package/scripts/bongos/db-backup-offsite.sh +267 -0
- package/scripts/bongos/demote.js +45 -0
- package/scripts/bongos/deploy-divergence.js +454 -0
- package/scripts/bongos/deploy-main-audit-gate.sh +97 -0
- package/scripts/bongos/design-sync.js +12 -0
- package/scripts/bongos/dev-box-guard.js +166 -0
- package/scripts/bongos/dev-lib.js +722 -0
- package/scripts/bongos/dev.js +645 -0
- package/scripts/bongos/device-flow-help.js +72 -0
- package/scripts/bongos/diagnose-achievements.js +118 -0
- package/scripts/bongos/diagram-facts.js +355 -0
- package/scripts/bongos/discord-channels.js +77 -0
- package/scripts/bongos/do-api.js +109 -0
- package/scripts/bongos/doc-cli-guard.js +274 -0
- package/scripts/bongos/doc-comment-coverage.js +139 -0
- package/scripts/bongos/doc-control-claims.js +170 -0
- package/scripts/bongos/docs-entropy.js +453 -0
- package/scripts/bongos/doctor.js +493 -0
- package/scripts/bongos/enroll-control-plane.js +101 -0
- package/scripts/bongos/exec-path-guard.js +564 -0
- package/scripts/bongos/feedback-send.js +249 -0
- package/scripts/bongos/fetch-art-key.js +106 -0
- package/scripts/bongos/fetch-sound-prefs.js +126 -0
- package/scripts/bongos/figma-design-sync.js +11 -0
- package/scripts/bongos/find-untested.js +371 -0
- package/scripts/bongos/fitness-checks-error-envelope.js +134 -0
- package/scripts/bongos/fitness-checks-identity.js +343 -0
- package/scripts/bongos/fitness-checks-packaging.js +132 -0
- package/scripts/bongos/fitness-checks-route-pins.js +76 -0
- package/scripts/bongos/fitness-checks-vocabulary.js +392 -0
- package/scripts/bongos/fitness-checks-write-validation.js +157 -0
- package/scripts/bongos/fitness-lib.js +127 -0
- package/scripts/bongos/fitness-migration-contraction.js +50 -0
- package/scripts/bongos/fitness-ratchets.js +594 -0
- package/scripts/bongos/fitness.js +1498 -0
- package/scripts/bongos/forwarder.js +80 -0
- package/scripts/bongos/gate-review.js +330 -0
- package/scripts/bongos/gating.js +348 -0
- package/scripts/{gds → bongos}/gds-db-backup.service +6 -6
- package/scripts/{gds → bongos}/gds-db-backup.timer +4 -4
- package/scripts/{gds → bongos}/gds-db-offsite.service +4 -4
- package/scripts/{gds → bongos}/gds-db-offsite.timer +4 -4
- package/scripts/bongos/gds-literal-scan.js +196 -0
- package/scripts/bongos/gen-api-client.js +576 -0
- package/scripts/bongos/gen-api-docs.js +1111 -0
- package/scripts/bongos/gen-atlas.js +366 -0
- package/scripts/bongos/gen-diagrams.js +608 -0
- package/scripts/bongos/gen-file-map.js +337 -0
- package/scripts/bongos/gen-freshness.js +32 -0
- package/scripts/bongos/gen-interaction-posture.js +122 -0
- package/scripts/bongos/gen-repo-map.js +540 -0
- package/scripts/bongos/gen-role-responsibilities.js +134 -0
- package/scripts/bongos/gen-session-index.js +243 -0
- package/scripts/bongos/gen-sounds.js +176 -0
- package/scripts/bongos/git-merge-regen.js +254 -0
- package/scripts/bongos/go-live.js +1071 -0
- package/scripts/bongos/goal-names.js +102 -0
- package/scripts/bongos/goal.js +272 -0
- package/scripts/bongos/government-walk.js +1477 -0
- package/scripts/bongos/grade-correlation-audit.js +310 -0
- package/scripts/bongos/grade-replay.js +237 -0
- package/scripts/bongos/grade-smoke.js +205 -0
- package/scripts/bongos/home-isolation-guard.js +126 -0
- package/scripts/bongos/hooks/pre-push.js +211 -0
- package/scripts/bongos/howto.js +145 -0
- package/scripts/bongos/http-api-client.js +81 -0
- package/scripts/bongos/identity-strip.js +75 -0
- package/scripts/bongos/init.js +1159 -0
- package/scripts/bongos/install-git-hooks.js +200 -0
- package/scripts/bongos/instance-apex.js +38 -0
- package/scripts/bongos/intent-retry.js +52 -0
- package/scripts/bongos/key-card.js +222 -0
- package/scripts/bongos/key-status.js +126 -0
- package/scripts/bongos/land-watch.js +201 -0
- package/scripts/bongos/leak-scan-allowlist.js +293 -0
- package/scripts/bongos/learning-capture.js +145 -0
- package/scripts/bongos/lib.sh +74 -0
- package/scripts/bongos/linkify-refs.js +655 -0
- package/scripts/bongos/lint-infra-exec.js +70 -0
- package/scripts/bongos/lint-migrations.js +102 -0
- package/scripts/bongos/lint-workflow-exec.js +63 -0
- package/scripts/bongos/local-preview-lib.js +128 -0
- package/scripts/bongos/local-preview.js +82 -0
- package/scripts/bongos/login.js +360 -0
- package/scripts/bongos/main-audit.js +625 -0
- package/scripts/bongos/me.js +97 -0
- package/scripts/bongos/memory-map.js +176 -0
- package/scripts/bongos/merge-lock.js +71 -0
- package/scripts/bongos/migration-namespace.js +313 -0
- package/scripts/bongos/mirror-lineage-check.js +343 -0
- package/scripts/bongos/mirror-redact.js +350 -0
- package/scripts/bongos/module-artifact.js +289 -0
- package/scripts/bongos/module-assess-docs.js +238 -0
- package/scripts/bongos/module-assess-installs.js +228 -0
- package/scripts/bongos/module-assess-score.js +111 -0
- package/scripts/bongos/module-assess-security.js +252 -0
- package/scripts/bongos/module-assess-tests.js +278 -0
- package/scripts/bongos/module-assess-version.js +92 -0
- package/scripts/bongos/module-error-report.js +148 -0
- package/scripts/{gds → bongos}/module-test-runner/README.md +3 -3
- package/scripts/{gds → bongos}/module-test-runner/module-tests.yml +2 -2
- package/scripts/bongos/module-test-runner.js +414 -0
- package/scripts/bongos/module.js +1376 -0
- package/scripts/bongos/move-escalation.js +141 -0
- package/scripts/{gds → bongos}/newcomer-floor.config.json +1 -1
- package/scripts/bongos/newcomer-floor.js +421 -0
- package/scripts/{gds → bongos}/newcomer-floor.templates.json +13 -13
- package/scripts/bongos/newcomer-floor.test.js +235 -0
- package/scripts/bongos/next-number.js +163 -0
- package/scripts/bongos/oauth-secret.js +202 -0
- package/scripts/bongos/onboard.js +267 -0
- package/scripts/bongos/onboarding-analyze.js +226 -0
- package/scripts/bongos/onboarding-config.js +179 -0
- package/scripts/bongos/onboarding-ship.js +92 -0
- package/scripts/bongos/pack-env-prefix.js +73 -0
- package/scripts/bongos/package-core.js +1200 -0
- package/scripts/bongos/page-inventory.js +348 -0
- package/scripts/bongos/page-reader.js +722 -0
- package/scripts/bongos/paste-token.js +212 -0
- package/scripts/bongos/plain-cards.js +351 -0
- package/scripts/bongos/portability-smoke-art.sh +85 -0
- package/scripts/bongos/portability-smoke-memory.sh +176 -0
- package/scripts/bongos/preflight.js +214 -0
- package/scripts/bongos/probe-cache.js +246 -0
- package/scripts/bongos/prove-strict-versioning.js +284 -0
- package/scripts/bongos/provision-auto-upgrade.js +162 -0
- package/scripts/bongos/provision-byo-host.js +68 -0
- package/scripts/bongos/provision-capacity.js +79 -0
- package/scripts/bongos/provision-caps.js +63 -0
- package/scripts/bongos/provision-config.js +233 -0
- package/scripts/bongos/provision-core-upgrade.js +1166 -0
- package/scripts/bongos/provision-disconnect.js +88 -0
- package/scripts/bongos/provision-github-setup.js +168 -0
- package/scripts/bongos/provision-net.js +357 -0
- package/scripts/bongos/provision-pin-key.js +176 -0
- package/scripts/bongos/provision-pin-replay.js +126 -0
- package/scripts/bongos/provision-process.js +170 -0
- package/scripts/bongos/provision-render.js +201 -0
- package/scripts/bongos/provision-repo-auth.js +164 -0
- package/scripts/bongos/provision-repo-private.js +98 -0
- package/scripts/bongos/provision-repo.js +1337 -0
- package/scripts/bongos/provision-teardown.js +106 -0
- package/scripts/bongos/provision-units.js +469 -0
- package/scripts/bongos/provision.js +1479 -0
- package/scripts/bongos/publish-credential-check.js +66 -0
- package/scripts/bongos/publish-integration-gate.js +142 -0
- package/scripts/bongos/publish-manifest.js +440 -0
- package/scripts/bongos/publish-watch.js +268 -0
- package/scripts/bongos/pulse.js +336 -0
- package/scripts/bongos/push-main.js +56 -0
- package/scripts/bongos/push-path-brief.js +100 -0
- package/scripts/bongos/read-session-export.js +359 -0
- package/scripts/bongos/reauth.js +155 -0
- package/scripts/bongos/recall.js +183 -0
- package/scripts/bongos/redteam-patrol.js +437 -0
- package/scripts/bongos/regen-instance-docs.js +47 -0
- package/scripts/bongos/release-core.js +80 -0
- package/scripts/bongos/release-notes.js +271 -0
- package/scripts/bongos/release.js +139 -0
- package/scripts/bongos/rename-goals.js +109 -0
- package/scripts/bongos/rename-history-check.js +369 -0
- package/scripts/bongos/rename-ledger-prose.js +482 -0
- package/scripts/bongos/render-api.js +197 -0
- package/scripts/bongos/render-check.js +206 -0
- package/scripts/bongos/render-ideas.js +226 -0
- package/scripts/bongos/render-payload.js +85 -0
- package/scripts/bongos/repo-metrics.js +363 -0
- package/scripts/bongos/review-sheet.js +108 -0
- package/scripts/bongos/role-pack-guard.js +142 -0
- package/scripts/bongos/route-shadow-guard.js +175 -0
- package/scripts/bongos/routine-schedule.js +283 -0
- package/scripts/bongos/run-routine.js +257 -0
- package/scripts/bongos/run-unit-tests.js +654 -0
- package/scripts/bongos/runner-drift.js +151 -0
- package/scripts/bongos/sandbox-stage.js +459 -0
- package/scripts/bongos/scope-map-guard.js +300 -0
- package/scripts/bongos/scope-roster-guard.js +118 -0
- package/scripts/bongos/seam-wiring-guard.js +275 -0
- package/scripts/bongos/search-ingest-db.js +288 -0
- package/scripts/bongos/search-ingest-md.js +269 -0
- package/scripts/bongos/seed-adopt-tasks.js +75 -0
- package/scripts/bongos/seed-api-contract-goal-tasks.js +141 -0
- package/scripts/bongos/seed-board-room-tasks.js +196 -0
- package/scripts/bongos/seed-bongos-app-tasks.js +294 -0
- package/scripts/bongos/seed-bongos-coreB-tranche1-tasks.js +81 -0
- package/scripts/bongos/seed-bongos-coreB-tranche2-tasks.js +100 -0
- package/scripts/bongos/seed-bongos-goal-hierarchy-tasks.js +307 -0
- package/scripts/bongos/seed-bongos-module-system-tasks.js +346 -0
- package/scripts/bongos/seed-bongos-tasks.js +214 -0
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +152 -0
- package/scripts/bongos/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/bongos/seed-bv1-community-tasks.js +131 -0
- package/scripts/bongos/seed-bv1-privacy-invites-tasks.js +133 -0
- package/scripts/bongos/seed-bv1-profile-page-tasks.js +161 -0
- package/scripts/bongos/seed-bv1-profiles-tasks.js +135 -0
- package/scripts/bongos/seed-bv1-r01-delta-tasks.js +154 -0
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +390 -0
- package/scripts/bongos/seed-bv2-tweak-mode-tasks.js +277 -0
- package/scripts/bongos/seed-gds-v4-tasks.js +624 -0
- package/scripts/bongos/seed-governance-tasks.js +340 -0
- package/scripts/bongos/seed-medusa-tasks.js +440 -0
- package/scripts/bongos/seed-module-upstreaming.js +219 -0
- package/scripts/bongos/seed-productization-tasks.js +235 -0
- package/scripts/bongos/seed-project-admin-console-tasks.js +88 -0
- package/scripts/bongos/seed-provisioning-tasks.js +83 -0
- package/scripts/bongos/seed-staging.sh +117 -0
- package/scripts/bongos/seed-strict-versioning-tasks.js +275 -0
- package/scripts/bongos/seed-v2-world-tasks.js +283 -0
- package/scripts/bongos/seed-verify-criteria.js +124 -0
- package/scripts/bongos/sequence.js +597 -0
- package/scripts/bongos/session-cost.js +430 -0
- package/scripts/bongos/session-digest-build.js +293 -0
- package/scripts/bongos/session-digest.js +200 -0
- package/scripts/bongos/session-eval.js +631 -0
- package/scripts/bongos/session-transcript-build.js +155 -0
- package/scripts/bongos/session-transcript-upload.js +29 -0
- package/scripts/bongos/session.js +404 -0
- package/scripts/bongos/setup.js +646 -0
- package/scripts/bongos/ship-check.js +89 -0
- package/scripts/bongos/ship-deploy-target.js +393 -0
- package/scripts/bongos/ship-escalation.js +167 -0
- package/scripts/bongos/ship-finish.js +420 -0
- package/scripts/bongos/ship-flow.js +1217 -0
- package/scripts/bongos/ship-git.js +100 -0
- package/scripts/bongos/ship-grade-steps.js +547 -0
- package/scripts/bongos/ship-grade.js +426 -0
- package/scripts/bongos/ship-honesty.js +314 -0
- package/scripts/bongos/ship-io.js +139 -0
- package/scripts/bongos/ship-land.js +1130 -0
- package/scripts/bongos/ship-memory-sync.js +249 -0
- package/scripts/bongos/ship-merge-gate.js +225 -0
- package/scripts/bongos/ship-merge.js +466 -0
- package/scripts/bongos/ship-mode-ask.js +138 -0
- package/scripts/bongos/ship-mode-manual.js +54 -0
- package/scripts/bongos/ship-preflight-steps.js +399 -0
- package/scripts/bongos/ship-regen.js +292 -0
- package/scripts/bongos/ship-resume.js +201 -0
- package/scripts/bongos/ship-session-upload.js +313 -0
- package/scripts/bongos/ship-state.js +110 -0
- package/scripts/bongos/ship-surprise.js +84 -0
- package/scripts/bongos/ship-visual.js +75 -0
- package/scripts/bongos/ship.js +382 -0
- package/scripts/bongos/shipping-mode.js +71 -0
- package/scripts/bongos/skill-lint.js +250 -0
- package/scripts/bongos/skill-preflight.js +361 -0
- package/scripts/bongos/smoke-batch-claim.sh +238 -0
- package/scripts/bongos/smoke-batch-release.sh +316 -0
- package/scripts/bongos/smoke-claim-consistency.sh +222 -0
- package/scripts/bongos/smoke-dependencies.sh +143 -0
- package/scripts/bongos/smoke-gds.js +256 -0
- package/scripts/bongos/smoke-write.sh +275 -0
- package/scripts/bongos/source-text-test-scan.js +106 -0
- package/scripts/bongos/spark.js +294 -0
- package/scripts/bongos/stale-worktrees.js +212 -0
- package/scripts/bongos/start-goals.js +102 -0
- package/scripts/bongos/start.js +1090 -0
- package/scripts/bongos/status.js +545 -0
- package/scripts/bongos/strand-watch.js +315 -0
- package/scripts/bongos/stranded-rows.js +200 -0
- package/scripts/bongos/stray-unit-sweep.sh +71 -0
- package/scripts/bongos/surface.js +101 -0
- package/scripts/bongos/task.js +273 -0
- package/scripts/bongos/test-path-guard.js +99 -0
- package/scripts/bongos/triage.js +742 -0
- package/scripts/bongos/turn-end-sound.js +74 -0
- package/scripts/bongos/tweak-halt.js +70 -0
- package/scripts/bongos/tweak-renders.js +200 -0
- package/scripts/bongos/uat.js +120 -0
- package/scripts/bongos/ui-kit-transform.js +43 -0
- package/scripts/bongos/update-channel.js +512 -0
- package/scripts/bongos/update-sweep.js +829 -0
- package/scripts/bongos/upgrade-artist-gate.js +46 -0
- package/scripts/bongos/upgrade-commit-identity.js +32 -0
- package/scripts/bongos/upgrade-migrate.js +224 -0
- package/scripts/bongos/upgrade-operator.js +73 -0
- package/scripts/bongos/upgrade-outcome.js +174 -0
- package/scripts/bongos/upgrade.js +1499 -0
- package/scripts/bongos/validate-design.js +12 -0
- package/scripts/bongos/version-close.js +621 -0
- package/scripts/bongos/version-literal-guard.js +140 -0
- package/scripts/bongos/version-select.js +79 -0
- package/scripts/bongos/visual-review-nudge.js +39 -0
- package/scripts/bongos/wedge-remedy.js +174 -0
- package/scripts/bongos/worktree-claim-guard.js +130 -0
- package/scripts/bongos/worktree.js +449 -0
- package/scripts/discord/post.js +1 -1
- package/scripts/gds/README.md +9 -0
- package/scripts/gds/abandon.js +6 -136
- package/scripts/gds/accessibility-replay.js +5 -194
- package/scripts/gds/address-park-sweep.sh +7 -90
- package/scripts/gds/adopt-detect.js +5 -121
- package/scripts/gds/adopt-import.js +5 -109
- package/scripts/gds/adopt-preflight.js +5 -185
- package/scripts/gds/adr-namespace.js +5 -271
- package/scripts/gds/agent-invoke.js +5 -687
- package/scripts/gds/agents-sync.js +5 -500
- package/scripts/gds/api-alias-caller-scan.js +5 -255
- package/scripts/gds/api-artifacts.js +6 -0
- package/scripts/gds/api.js +6 -85
- package/scripts/gds/api.ps1 +8 -26
- package/scripts/gds/architect-audit.js +6 -293
- package/scripts/gds/artifact-audit.js +5 -324
- package/scripts/gds/artifact-format.js +6 -203
- package/scripts/gds/artifact-staleness.js +5 -265
- package/scripts/gds/ask.js +5 -229
- package/scripts/gds/audit-authorship.sh +7 -328
- package/scripts/gds/audit-deps.js +6 -188
- package/scripts/gds/audit-gate.js +5 -119
- package/scripts/gds/audit-rank-roles.js +6 -171
- package/scripts/gds/audit-replay.sh +7 -204
- package/scripts/gds/autobongos-digest.js +5 -121
- package/scripts/gds/autobongos-grade-cap.js +5 -186
- package/scripts/gds/autobongos-loop.js +5 -208
- package/scripts/gds/autobongos-run.js +5 -1359
- package/scripts/gds/autobongos-service.cmd +8 -49
- package/scripts/gds/autobongos-service.ps1 +8 -270
- package/scripts/gds/autobongos-tweak-lane.js +5 -261
- package/scripts/gds/autobongos-verify.js +5 -616
- package/scripts/gds/autonomy-gate.js +5 -133
- package/scripts/gds/autonomy-gauge-statusline.js +5 -122
- package/scripts/gds/autonomy-gauge.js +5 -82
- package/scripts/gds/backfill-cost.js +6 -175
- package/scripts/gds/backfill-disciplines.js +5 -142
- package/scripts/gds/backfill-session-earnings.js +5 -121
- package/scripts/gds/backfill-session-types.js +5 -85
- package/scripts/gds/backfill-sessions.js +5 -409
- package/scripts/gds/backfill-subagent-rewards.js +5 -483
- package/scripts/gds/backfill-task-rewards.js +5 -411
- package/scripts/gds/backlog-review.js +6 -398
- package/scripts/gds/backup-dropin-sweep.sh +7 -94
- package/scripts/gds/backup.js +6 -115
- package/scripts/gds/baseline-staleness.js +5 -215
- package/scripts/gds/bfg-principal-token.js +6 -64
- package/scripts/gds/bug-attachment-audit.js +5 -147
- package/scripts/gds/bug-attachment-recover.js +5 -196
- package/scripts/gds/bug-triage.js +6 -291
- package/scripts/gds/build-bongos.js +5 -92
- package/scripts/gds/bump-version.js +5 -219
- package/scripts/gds/capture.js +6 -480
- package/scripts/gds/category-advisory-guard.js +5 -205
- package/scripts/gds/ci-grade.js +6 -81
- package/scripts/gds/claim-recall.js +6 -111
- package/scripts/gds/claim.js +6 -788
- package/scripts/gds/classify-discipline.js +6 -152
- package/scripts/gds/claude-materialize.js +5 -1198
- package/scripts/gds/claude-md-template.js +5 -140
- package/scripts/gds/cleanup-orphans.js +6 -73
- package/scripts/gds/cli-lib.js +6 -1204
- package/scripts/gds/client-baseurl-guard.js +5 -114
- package/scripts/gds/codemod-rename-src-gds.js +6 -144
- package/scripts/gds/conductor-dispatch.js +6 -534
- package/scripts/gds/context-pack.js +6 -325
- package/scripts/gds/control-manifest.js +5 -197
- package/scripts/gds/copy-apply.js +5 -756
- package/scripts/gds/copy-inventory.js +5 -569
- package/scripts/gds/core-origin-follow.js +5 -184
- package/scripts/gds/core-pin-sync.js +5 -258
- package/scripts/gds/coreB-carve-proof.sh +7 -724
- package/scripts/gds/cost-report.js +6 -62
- package/scripts/gds/cost.js +6 -46
- package/scripts/gds/credential-presence.js +5 -294
- package/scripts/gds/cutover-codemod.js +5 -239
- package/scripts/gds/db-backup-nightly.sh +7 -231
- package/scripts/gds/db-backup-offsite.sh +7 -266
- package/scripts/gds/demote.js +6 -44
- package/scripts/gds/deploy-divergence.js +6 -445
- package/scripts/gds/deploy-main-audit-gate.sh +7 -96
- package/scripts/gds/design-sync.js +5 -10
- package/scripts/gds/dev-box-guard.js +5 -164
- package/scripts/gds/dev-lib.js +5 -721
- package/scripts/gds/dev.js +5 -643
- package/scripts/gds/device-flow-help.js +5 -70
- package/scripts/gds/diagnose-achievements.js +5 -116
- package/scripts/gds/diagram-facts.js +6 -355
- package/scripts/gds/discord-channels.js +5 -75
- package/scripts/gds/do-api.js +6 -109
- package/scripts/gds/doc-cli-guard.js +5 -266
- package/scripts/gds/doc-comment-coverage.js +5 -137
- package/scripts/gds/doc-control-claims.js +5 -166
- package/scripts/gds/docs-entropy.js +5 -443
- package/scripts/gds/doctor.js +6 -492
- package/scripts/gds/enroll-control-plane.js +5 -99
- package/scripts/gds/exec-path-guard.js +5 -562
- package/scripts/gds/feedback-send.js +5 -247
- package/scripts/gds/fetch-art-key.js +5 -104
- package/scripts/gds/fetch-sound-prefs.js +5 -124
- package/scripts/gds/figma-design-sync.js +5 -9
- package/scripts/gds/find-untested.js +5 -369
- package/scripts/gds/fitness-checks-error-envelope.js +5 -133
- package/scripts/gds/fitness-checks-identity.js +5 -342
- package/scripts/gds/fitness-checks-packaging.js +5 -131
- package/scripts/gds/fitness-checks-route-pins.js +5 -75
- package/scripts/gds/fitness-checks-vocabulary.js +5 -391
- package/scripts/gds/fitness-checks-write-validation.js +5 -156
- package/scripts/gds/fitness-lib.js +5 -126
- package/scripts/gds/fitness-migration-contraction.js +5 -49
- package/scripts/gds/fitness-ratchets.js +5 -563
- package/scripts/gds/fitness.js +5 -1493
- package/scripts/gds/gate-review.js +5 -328
- package/scripts/gds/gating.js +5 -346
- package/scripts/gds/gds-literal-scan.js +5 -194
- package/scripts/gds/gen-api-client.js +5 -555
- package/scripts/gds/gen-api-docs.js +5 -1100
- package/scripts/gds/gen-atlas.js +5 -364
- package/scripts/gds/gen-diagrams.js +6 -607
- package/scripts/gds/gen-file-map.js +5 -335
- package/scripts/gds/gen-freshness.js +5 -31
- package/scripts/gds/gen-interaction-posture.js +5 -120
- package/scripts/gds/gen-repo-map.js +5 -538
- package/scripts/gds/gen-role-responsibilities.js +5 -133
- package/scripts/gds/gen-session-index.js +5 -241
- package/scripts/gds/gen-sounds.js +5 -174
- package/scripts/gds/git-merge-regen.js +6 -291
- package/scripts/gds/go-live.js +5 -1069
- package/scripts/gds/goal-names.js +5 -100
- package/scripts/gds/goal.js +6 -271
- package/scripts/gds/government-walk.js +5 -1475
- package/scripts/gds/grade-correlation-audit.js +5 -308
- package/scripts/gds/grade-replay.js +5 -235
- package/scripts/gds/grade-smoke.js +6 -204
- package/scripts/gds/home-isolation-guard.js +5 -125
- package/scripts/gds/hooks/pre-push.js +5 -209
- package/scripts/gds/howto.js +5 -143
- package/scripts/gds/http-api-client.js +6 -81
- package/scripts/gds/identity-strip.js +5 -74
- package/scripts/gds/init.js +5 -1157
- package/scripts/gds/install-git-hooks.js +5 -198
- package/scripts/gds/instance-apex.js +5 -37
- package/scripts/gds/intent-retry.js +5 -51
- package/scripts/gds/key-card.js +6 -221
- package/scripts/gds/key-status.js +5 -124
- package/scripts/gds/land-watch.js +5 -199
- package/scripts/gds/leak-scan-allowlist.js +5 -291
- package/scripts/gds/learning-capture.js +6 -144
- package/scripts/gds/lib.sh +7 -73
- package/scripts/gds/linkify-refs.js +6 -654
- package/scripts/gds/lint-infra-exec.js +5 -68
- package/scripts/gds/lint-migrations.js +5 -100
- package/scripts/gds/lint-workflow-exec.js +5 -61
- package/scripts/gds/local-preview-lib.js +6 -128
- package/scripts/gds/local-preview.js +6 -81
- package/scripts/gds/login.js +5 -358
- package/scripts/gds/main-audit.js +5 -623
- package/scripts/gds/me.js +6 -96
- package/scripts/gds/memory-map.js +5 -174
- package/scripts/gds/merge-lock.js +6 -70
- package/scripts/gds/migration-namespace.js +5 -311
- package/scripts/gds/mirror-lineage-check.js +5 -341
- package/scripts/gds/mirror-redact.js +5 -348
- package/scripts/gds/module-artifact.js +6 -289
- package/scripts/gds/module-assess-docs.js +6 -237
- package/scripts/gds/module-assess-installs.js +6 -227
- package/scripts/gds/module-assess-score.js +6 -110
- package/scripts/gds/module-assess-security.js +6 -251
- package/scripts/gds/module-assess-tests.js +6 -277
- package/scripts/gds/module-assess-version.js +6 -91
- package/scripts/gds/module-error-report.js +5 -146
- package/scripts/gds/module-test-runner.js +6 -413
- package/scripts/gds/module.js +5 -1374
- package/scripts/gds/move-escalation.js +5 -140
- package/scripts/gds/newcomer-floor.js +6 -420
- package/scripts/gds/newcomer-floor.test.js +6 -234
- package/scripts/gds/next-number.js +6 -162
- package/scripts/gds/oauth-secret.js +5 -200
- package/scripts/gds/onboard.js +5 -265
- package/scripts/gds/onboarding-analyze.js +6 -225
- package/scripts/gds/onboarding-config.js +6 -179
- package/scripts/gds/onboarding-ship.js +6 -91
- package/scripts/gds/pack-env-prefix.js +5 -72
- package/scripts/gds/package-core.js +5 -1102
- package/scripts/gds/page-inventory.js +5 -346
- package/scripts/gds/page-reader.js +5 -720
- package/scripts/gds/paste-token.js +6 -211
- package/scripts/gds/plain-cards.js +5 -350
- package/scripts/gds/portability-smoke-art.sh +7 -84
- package/scripts/gds/portability-smoke-memory.sh +7 -175
- package/scripts/gds/preflight.js +5 -212
- package/scripts/gds/probe-cache.js +5 -244
- package/scripts/gds/prove-strict-versioning.js +6 -283
- package/scripts/gds/provision-auto-upgrade.js +5 -161
- package/scripts/gds/provision-byo-host.js +5 -67
- package/scripts/gds/provision-capacity.js +5 -78
- package/scripts/gds/provision-caps.js +5 -62
- package/scripts/gds/provision-config.js +5 -232
- package/scripts/gds/provision-core-upgrade.js +5 -1165
- package/scripts/gds/provision-disconnect.js +5 -87
- package/scripts/gds/provision-github-setup.js +5 -167
- package/scripts/gds/provision-net.js +5 -356
- package/scripts/gds/provision-pin-key.js +5 -175
- package/scripts/gds/provision-pin-replay.js +5 -125
- package/scripts/gds/provision-process.js +5 -169
- package/scripts/gds/provision-render.js +5 -200
- package/scripts/gds/provision-repo-auth.js +5 -163
- package/scripts/gds/provision-repo-private.js +5 -97
- package/scripts/gds/provision-repo.js +5 -1336
- package/scripts/gds/provision-teardown.js +5 -105
- package/scripts/gds/provision-units.js +5 -468
- package/scripts/gds/provision.js +5 -1477
- package/scripts/gds/publish-credential-check.js +6 -65
- package/scripts/gds/publish-integration-gate.js +5 -140
- package/scripts/gds/publish-manifest.js +5 -437
- package/scripts/gds/publish-watch.js +5 -266
- package/scripts/gds/pulse.js +6 -335
- package/scripts/gds/push-main.js +5 -54
- package/scripts/gds/push-path-brief.js +5 -98
- package/scripts/gds/read-session-export.js +5 -357
- package/scripts/gds/reauth.js +6 -154
- package/scripts/gds/recall.js +6 -182
- package/scripts/gds/redteam-patrol.js +5 -435
- package/scripts/gds/regen-instance-docs.js +5 -45
- package/scripts/gds/release-core.js +5 -78
- package/scripts/gds/release-notes.js +5 -270
- package/scripts/gds/release.js +6 -138
- package/scripts/gds/rename-goals.js +5 -107
- package/scripts/gds/rename-history-check.js +5 -367
- package/scripts/gds/rename-ledger-prose.js +5 -480
- package/scripts/gds/render-api.js +5 -196
- package/scripts/gds/render-check.js +5 -204
- package/scripts/gds/render-ideas.js +6 -225
- package/scripts/gds/render-payload.js +5 -84
- package/scripts/gds/repo-metrics.js +6 -362
- package/scripts/gds/review-sheet.js +5 -106
- package/scripts/gds/role-pack-guard.js +5 -141
- package/scripts/gds/route-shadow-guard.js +5 -173
- package/scripts/gds/routine-schedule.js +5 -281
- package/scripts/gds/run-routine.js +5 -255
- package/scripts/gds/run-unit-tests.js +6 -659
- package/scripts/gds/runner-drift.js +5 -149
- package/scripts/gds/sandbox-stage.js +6 -458
- package/scripts/gds/scope-map-guard.js +5 -299
- package/scripts/gds/scope-roster-guard.js +5 -117
- package/scripts/gds/seam-wiring-guard.js +5 -274
- package/scripts/gds/search-ingest-db.js +6 -287
- package/scripts/gds/search-ingest-md.js +6 -268
- package/scripts/gds/seed-adopt-tasks.js +6 -74
- package/scripts/gds/seed-api-contract-goal-tasks.js +6 -140
- package/scripts/gds/seed-board-room-tasks.js +6 -196
- package/scripts/gds/seed-bongos-app-tasks.js +6 -293
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +6 -80
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +6 -99
- package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +6 -306
- package/scripts/gds/seed-bongos-module-system-tasks.js +6 -345
- package/scripts/gds/seed-bongos-tasks.js +6 -213
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +6 -152
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +6 -104
- package/scripts/gds/seed-bv1-community-tasks.js +6 -131
- package/scripts/gds/seed-bv1-privacy-invites-tasks.js +6 -133
- package/scripts/gds/seed-bv1-profile-page-tasks.js +6 -161
- package/scripts/gds/seed-bv1-profiles-tasks.js +6 -135
- package/scripts/gds/seed-bv1-r01-delta-tasks.js +6 -154
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +6 -390
- package/scripts/gds/seed-bv2-tweak-mode-tasks.js +6 -277
- package/scripts/gds/seed-gds-v4-tasks.js +6 -623
- package/scripts/gds/seed-governance-tasks.js +6 -339
- package/scripts/gds/seed-medusa-tasks.js +6 -439
- package/scripts/gds/seed-module-upstreaming.js +5 -217
- package/scripts/gds/seed-productization-tasks.js +6 -234
- package/scripts/gds/seed-project-admin-console-tasks.js +6 -88
- package/scripts/gds/seed-provisioning-tasks.js +6 -82
- package/scripts/gds/seed-staging.sh +7 -116
- package/scripts/gds/seed-strict-versioning-tasks.js +6 -274
- package/scripts/gds/seed-v2-world-tasks.js +6 -282
- package/scripts/gds/seed-verify-criteria.js +6 -124
- package/scripts/gds/sequence.js +6 -596
- package/scripts/gds/session-cost.js +6 -429
- package/scripts/gds/session-digest-build.js +6 -293
- package/scripts/gds/session-digest.js +6 -199
- package/scripts/gds/session-eval.js +6 -630
- package/scripts/gds/session-transcript-build.js +6 -155
- package/scripts/gds/session-transcript-upload.js +6 -29
- package/scripts/gds/session.js +6 -403
- package/scripts/gds/setup.js +6 -645
- package/scripts/gds/ship-check.js +5 -85
- package/scripts/gds/ship-deploy-target.js +5 -392
- package/scripts/gds/ship-escalation.js +5 -166
- package/scripts/gds/ship-finish.js +5 -419
- package/scripts/gds/ship-flow.js +5 -1216
- package/scripts/gds/ship-git.js +5 -99
- package/scripts/gds/ship-grade-steps.js +5 -546
- package/scripts/gds/ship-grade.js +5 -425
- package/scripts/gds/ship-honesty.js +5 -312
- package/scripts/gds/ship-io.js +5 -138
- package/scripts/gds/ship-land.js +5 -1129
- package/scripts/gds/ship-memory-sync.js +5 -248
- package/scripts/gds/ship-merge-gate.js +5 -224
- package/scripts/gds/ship-merge.js +5 -465
- package/scripts/gds/ship-mode-ask.js +5 -137
- package/scripts/gds/ship-mode-manual.js +5 -53
- package/scripts/gds/ship-preflight-steps.js +5 -398
- package/scripts/gds/ship-regen.js +5 -313
- package/scripts/gds/ship-resume.js +5 -200
- package/scripts/gds/ship-session-upload.js +5 -312
- package/scripts/gds/ship-state.js +5 -109
- package/scripts/gds/ship-surprise.js +5 -83
- package/scripts/gds/ship-visual.js +5 -74
- package/scripts/gds/ship.js +5 -380
- package/scripts/gds/shipping-mode.js +5 -69
- package/scripts/gds/skill-lint.js +5 -248
- package/scripts/gds/skill-preflight.js +5 -359
- package/scripts/gds/smoke-batch-claim.sh +7 -237
- package/scripts/gds/smoke-batch-release.sh +7 -315
- package/scripts/gds/smoke-claim-consistency.sh +7 -221
- package/scripts/gds/smoke-dependencies.sh +7 -142
- package/scripts/gds/smoke-gds.js +6 -255
- package/scripts/gds/smoke-write.sh +7 -274
- package/scripts/gds/source-text-test-scan.js +7 -0
- package/scripts/gds/spark.js +6 -293
- package/scripts/gds/stale-worktrees.js +5 -210
- package/scripts/gds/start-goals.js +5 -101
- package/scripts/gds/start.js +6 -1089
- package/scripts/gds/status.js +6 -544
- package/scripts/gds/strand-watch.js +5 -313
- package/scripts/gds/stranded-rows.js +5 -198
- package/scripts/gds/stray-unit-sweep.sh +7 -70
- package/scripts/gds/surface.js +5 -100
- package/scripts/gds/task.js +6 -272
- package/scripts/gds/test-path-guard.js +5 -97
- package/scripts/gds/triage.js +6 -741
- package/scripts/gds/turn-end-sound.js +5 -72
- package/scripts/gds/tweak-halt.js +5 -68
- package/scripts/gds/tweak-renders.js +5 -198
- package/scripts/gds/uat.js +6 -119
- package/scripts/gds/ui-kit-transform.js +5 -41
- package/scripts/gds/update-channel.js +5 -510
- package/scripts/gds/update-sweep.js +5 -827
- package/scripts/gds/upgrade-artist-gate.js +5 -45
- package/scripts/gds/upgrade-commit-identity.js +5 -31
- package/scripts/gds/upgrade-migrate.js +5 -223
- package/scripts/gds/upgrade-operator.js +6 -73
- package/scripts/gds/upgrade-outcome.js +5 -173
- package/scripts/gds/upgrade.js +5 -1497
- package/scripts/gds/validate-design.js +5 -10
- package/scripts/gds/version-close.js +6 -620
- package/scripts/gds/version-literal-guard.js +5 -138
- package/scripts/gds/version-select.js +5 -77
- package/scripts/gds/visual-review-nudge.js +5 -38
- package/scripts/gds/wedge-remedy.js +5 -173
- package/scripts/gds/worktree-claim-guard.js +5 -129
- package/scripts/gds/worktree.js +6 -448
- package/scripts/hall-preview/refresh-fixtures.js +1 -1
- package/scripts/hall-preview/server.js +2 -2
- package/scripts/migrate.sh +3 -3
- package/scripts/public-mirror-export.js +3 -3
- package/scripts/setup-builder.js +7 -7
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/api-prefix.js +1 -1
- package/src/bongos/auth-github.js +19 -3
- package/src/bongos/auth-idp.js +7 -0
- package/src/bongos/claim-attribution.js +2 -2
- package/src/bongos/core-moves.js +1 -1
- package/src/bongos/core-update.js +3 -3
- package/src/bongos/craft-mode.js +2 -2
- package/src/bongos/db-kernel.js +15 -3
- package/src/bongos/db.js +30 -4
- package/src/bongos/github-repos.js +1 -1
- package/src/bongos/llm-cache.js +1 -1
- package/src/bongos/llm-pricing.js +1 -1
- package/src/bongos/metrics.js +1 -1
- package/src/bongos/middleware/rate-limit.js +2 -2
- package/src/bongos/module-listing.js +93 -1
- package/src/bongos/module-scope-map.js +90 -85
- package/src/bongos/module-store.js +1 -1
- package/src/bongos/module-submissions.js +1 -1
- package/src/bongos/optimizer.js +1 -1
- package/src/bongos/package-registry.js +1 -1
- package/src/bongos/path-match.js +1 -1
- package/src/bongos/permission-path-check.js +3 -3
- package/src/bongos/platform-visibility-gate.js +3 -5
- package/src/bongos/prelaunch-gate.js +7 -6
- package/src/bongos/route-rank-check.js +2 -2
- package/src/bongos/routes/auth.js +15 -2
- package/src/bongos/routes/backup.js +1 -1
- package/src/bongos/routes/builders.js +29 -1
- package/src/bongos/routes/me.js +49 -18
- package/src/bongos/routes/modules.js +29 -9
- package/src/bongos/routes/public.js +3 -72
- package/src/bongos/schema-lag.js +1 -1
- package/src/bongos/sensitive-surfaces.js +1 -1
- package/src/bongos/serve-internal.js +65 -16
- package/src/bongos/software-update.js +2 -2
- package/src/bongos/upgrade-hold.js +3 -3
- package/src/hall-look.js +6 -5
- package/src/module-api.js +3 -3
- package/src/module-loader/catalog.js +1 -1
- package/src/module-loader/error-tally.js +3 -3
- package/src/module-loader/manifest-schema.js +3 -3
- package/src/platform-server.js +10 -0
- package/src/role-responsibilities.js +1 -1
- package/src/stale-timer.js +2 -2
- package/tests/accessibility_replay.mjs +1 -1
- package/tests/activity_rollup_order_db.mjs +1 -1
- package/tests/activity_snapshots_db.mjs +1 -1
- package/tests/adopt_detect.mjs +1 -1
- package/tests/adopt_import.mjs +1 -1
- package/tests/adopt_preflight.mjs +1 -1
- package/tests/adr_0089_scope.mjs +10 -7
- package/tests/adr_0162_citation.mjs +1 -1
- package/tests/adr_0162_gate_citation.mjs +1 -1
- package/tests/agent_invoke.mjs +1 -1
- package/tests/agent_invoke_live.mjs +3 -3
- package/tests/agents_cron_trigger.mjs +2 -2
- package/tests/agents_roster.mjs +1 -1
- package/tests/agents_sync.mjs +4 -4
- package/tests/allow_rule_guard.mjs +20 -20
- package/tests/api_alias_caller_ratchet.mjs +9 -10
- package/tests/api_alias_exceptions.mjs +7 -6
- package/tests/api_artifacts.mjs +214 -0
- package/tests/api_cli_flags.mjs +3 -3
- package/tests/api_client.mjs +56 -54
- package/tests/api_docs.mjs +30 -9
- package/tests/api_error_envelope_buildcheck.mjs +1 -1
- package/tests/api_msys_path_repair.mjs +2 -2
- package/tests/architect.mjs +1 -1
- package/tests/art_stage.mjs +2 -2
- package/tests/artifact_audit.mjs +2 -2
- package/tests/artifact_staleness.mjs +9 -9
- package/tests/atlas.mjs +1 -1
- package/tests/audit_deps.mjs +1 -1
- package/tests/audit_gate.mjs +1 -1
- package/tests/auth_flow_slice1.mjs +3 -3
- package/tests/auth_inactive_builder.mjs +43 -5
- package/tests/auth_session_expiry_db.mjs +16 -4
- package/tests/auth_session_ttl.mjs +23 -0
- package/tests/authorship_audit.mjs +2 -2
- package/tests/autobongos_cadence.mjs +1 -1
- package/tests/autobongos_fence.mjs +1 -1
- package/tests/autobongos_grade_cap.mjs +2 -2
- package/tests/autobongos_heartbeat_head.mjs +1 -1
- package/tests/autobongos_host_access.mjs +3 -3
- package/tests/autobongos_loop.mjs +17 -17
- package/tests/autobongos_service_ps1.mjs +3 -3
- package/tests/autobongos_tweak_lane.mjs +2 -2
- package/tests/autobongos_verify.mjs +2 -2
- package/tests/autonomous_fit_protected_paths.mjs +3 -3
- package/tests/autonomy_config_idle.mjs +1 -1
- package/tests/autonomy_digest.mjs +1 -1
- package/tests/autonomy_gate.mjs +1 -1
- package/tests/autonomy_gauge.mjs +2 -2
- package/tests/autonomy_runner_identity.mjs +1 -1
- package/tests/backfill_sessions.mjs +8 -8
- package/tests/backfill_subagent_rewards.mjs +1 -1
- package/tests/backfill_task_rewards.mjs +2 -2
- package/tests/backlog_review.mjs +3 -3
- package/tests/baseline_staleness.mjs +2 -2
- package/tests/bfg.mjs +11 -5
- package/tests/bfg_write.mjs +20 -11
- package/tests/blockers_hall.mjs +17 -5
- package/tests/blockers_page_ui.mjs +56 -12
- package/tests/bongos_dispatch.mjs +5 -5
- package/tests/bongos_login.mjs +2 -2
- package/tests/bug_attachment_audit.mjs +1 -1
- package/tests/bug_attachment_recover.mjs +2 -2
- package/tests/bug_triage.mjs +3 -3
- package/tests/build_info.mjs +1 -1
- package/tests/builder_tester_tag.mjs +184 -0
- package/tests/bump_version.mjs +3 -3
- package/tests/canonical_profile_url.mjs +1 -1
- package/tests/capture_error_format.mjs +1 -1
- package/tests/capture_goal_routing.mjs +2 -2
- package/tests/category_advisory_guard.mjs +3 -3
- package/tests/claim_error_surface.mjs +2 -2
- package/tests/claim_from_task_and_home.mjs +1 -1
- package/tests/claim_gate_open_blocker.mjs +7 -4
- package/tests/claim_gate_rebase.mjs +12 -8
- package/tests/claim_recall.mjs +4 -4
- package/tests/claim_resolve_worktree_lock.mjs +1 -1
- package/tests/claude_materialize.mjs +58 -58
- package/tests/clawback_confirmed_backlog.mjs +8 -5
- package/tests/cli_allowlist_permissions.mjs +11 -11
- package/tests/cli_dispatcher.mjs +2 -2
- package/tests/cli_err_code.mjs +6 -6
- package/tests/cli_exit_no_abort.mjs +7 -7
- package/tests/cli_lib_formatters.mjs +2 -2
- package/tests/cli_sessions.mjs +5 -5
- package/tests/cli_surface.mjs +15 -15
- package/tests/cli_token_reissue.mjs +2 -2
- package/tests/client_baseurl_guard.mjs +1 -1
- package/tests/client_bundle_route.mjs +35 -4
- package/tests/cloud_bongos_pack.mjs +1 -1
- package/tests/community_leaderboard_db.mjs +1 -1
- package/tests/conductor.mjs +18 -18
- package/tests/conflict_resolve.mjs +14 -16
- package/tests/consumer_layout_boot.mjs +12 -12
- package/tests/context_pack.mjs +6 -6
- package/tests/control_manifest.mjs +4 -4
- package/tests/copy_desk_page_reads.mjs +1 -1
- package/tests/copy_desk_page_release.mjs +192 -0
- package/tests/copy_inventory.mjs +2 -2
- package/tests/core_auto_upgrade.mjs +1 -1
- package/tests/core_move_notice.mjs +2 -2
- package/tests/core_origin_follow.mjs +7 -7
- package/tests/core_pin_sync.mjs +3 -3
- package/tests/core_upgrade_door.mjs +2 -2
- package/tests/core_upgrade_owner_door.mjs +34 -1
- package/tests/core_upgrade_runner.mjs +19 -19
- package/tests/credential_presence.mjs +2 -2
- package/tests/credit_grant.mjs +3 -3
- package/tests/credit_log_single_writer.mjs +5 -5
- package/tests/criterion_ref_status.mjs +2 -2
- package/tests/cross_discipline_priority_direction.mjs +3 -3
- package/tests/cutover_codemod.mjs +2 -4
- package/tests/db_backup_nightly.mjs +1 -1
- package/tests/db_backup_offsite.mjs +1 -1
- package/tests/db_ownership_handover.mjs +2 -2
- package/tests/dead_deps.mjs +3 -3
- package/tests/dependency_advisory.mjs +1 -1
- package/tests/deploy_audit_gate.mjs +4 -4
- package/tests/deploy_divergence_line.mjs +36 -16
- package/tests/deploy_page_projects.mjs +35 -4
- package/tests/dev_lib.mjs +2 -2
- package/tests/dev_windows.mjs +2 -2
- package/tests/diagram_facts_offset.mjs +1 -1
- package/tests/diagram_repo_slug.mjs +1 -1
- package/tests/diff_derived_signals.mjs +2 -2
- package/tests/dns_preflight.mjs +2 -2
- package/tests/do_api.mjs +3 -3
- package/tests/doc_cli_guard.mjs +28 -28
- package/tests/doc_comment_coverage.mjs +1 -1
- package/tests/doc_control_claims.mjs +11 -11
- package/tests/docs_entropy.mjs +2 -2
- package/tests/docs_openapi_route.mjs +8 -6
- package/tests/doctor_hooks.mjs +1 -1
- package/tests/edit_hints.mjs +3 -2
- package/tests/effective_visibility_predicate.mjs +3 -3
- package/tests/env_knobs_neutral_prefix.mjs +13 -13
- package/tests/exec_path_guard.mjs +4 -4
- package/tests/executed_closure.mjs +18 -12
- package/tests/file_map.mjs +1 -1
- package/tests/find_untested.mjs +13 -13
- package/tests/first_admin_bootstrap.mjs +1 -1
- package/tests/fitness.mjs +16 -16
- package/tests/fitness_gds_ratchet.mjs +2 -2
- package/tests/fitness_installed_core.mjs +3 -3
- package/tests/fitness_no_raw_nul.mjs +1 -1
- package/tests/fitness_ratchets.mjs +3 -3
- package/tests/fitness_scope_roster.mjs +2 -2
- package/tests/fitness_two_root.mjs +8 -8
- package/tests/fixtures/accessibility-replay.json +5 -5
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +1 -1
- package/tests/fixtures/ideator-prefold/README.md +1 -1
- package/tests/fixtures/page-ledger.mjs +22 -0
- package/tests/fixtures/session-506-digest.json +5 -5
- package/tests/gate_approvals.mjs +50 -0
- package/tests/gate_review.mjs +8 -8
- package/tests/gating_review_retired.mjs +2 -2
- package/tests/gen_freshness.mjs +3 -3
- package/tests/git_guard.mjs +3 -3
- package/tests/git_merge_regen.mjs +41 -65
- package/tests/github_one_trip.mjs +4 -4
- package/tests/github_pick_up.mjs +2 -2
- package/tests/github_push_merge_driver.mjs +1 -1
- package/tests/go_live.mjs +3 -2
- package/tests/goal_category.mjs +1 -1
- package/tests/goal_cli.mjs +2 -2
- package/tests/goal_create_spec_entry.mjs +1 -1
- package/tests/goal_invite_consent.mjs +3 -1
- package/tests/goal_move.mjs +1 -1
- package/tests/goal_names.mjs +2 -2
- package/tests/goal_working_area.mjs +1 -1
- package/tests/government_agent_asks.mjs +7 -7
- package/tests/government_board_room_copy.mjs +6 -1
- package/tests/government_board_view.mjs +39 -0
- package/tests/government_board_vote_ui.mjs +39 -1
- package/tests/government_config.mjs +2 -2
- package/tests/government_live_proof_shape.mjs +1 -1
- package/tests/government_protected_surfaces.mjs +54 -53
- package/tests/government_require_permission.mjs +9 -1
- package/tests/government_vanilla_boot.mjs +1 -1
- package/tests/government_vocabulary.mjs +2 -2
- package/tests/government_walk.mjs +2 -2
- package/tests/grade_body_limit.mjs +6 -4
- package/tests/grade_correlation_audit.mjs +3 -3
- package/tests/grade_diff_hash.mjs +1 -1
- package/tests/grade_engine_replay.mjs +2 -2
- package/tests/grade_server_authoritative.mjs +1 -1
- package/tests/grade_unavailable_display.mjs +1 -1
- package/tests/grader.mjs +8 -24
- package/tests/grader_generated_provenance.mjs +14 -14
- package/tests/grader_no_artifact.mjs +1 -1
- package/tests/grader_prompt_fencing.mjs +1 -1
- package/tests/grader_quality_window_overflow.mjs +1 -1
- package/tests/grader_reader_lens.mjs +3 -3
- package/tests/grader_regrade_delta.mjs +1 -1
- package/tests/grader_unbacked_score.mjs +2 -2
- package/tests/hall_acclaim_vote_control.mjs +113 -0
- package/tests/hall_asset_cache.mjs +127 -0
- package/tests/hall_audit.mjs +63 -0
- package/tests/hall_board_world.mjs +20 -0
- package/tests/hall_city_card_words.mjs +61 -0
- package/tests/hall_client_request_shape.mjs +5 -1
- package/tests/hall_home_ships_bounded.mjs +160 -0
- package/tests/hall_landing_boot.mjs +15 -2
- package/tests/hall_nav.mjs +33 -0
- package/tests/hall_onboarding_bongos_setup.mjs +1 -1
- package/tests/hall_oversight_world.mjs +44 -4
- package/tests/hall_palette.mjs +12 -9
- package/tests/hall_preview_security.mjs +1 -1
- package/tests/hall_profile_world.mjs +17 -0
- package/tests/hall_reading_world.mjs +16 -1
- package/tests/hall_roster_world.mjs +2 -1
- package/tests/hall_studio_world.mjs +4 -1
- package/tests/hall_work_history_id.mjs +39 -0
- package/tests/held_tweak_reporting.mjs +4 -4
- package/tests/helpers.mjs +3 -3
- package/tests/home_attention_cards.mjs +2 -0
- package/tests/home_bounded_reads.mjs +1 -1
- package/tests/home_isolation_guard.mjs +1 -1
- package/tests/hook_core_resolve.mjs +7 -7
- package/tests/hook_user_agents.mjs +1 -1
- package/tests/hooks_drain.mjs +2 -2
- package/tests/host_topology_skips.mjs +1 -1
- package/tests/howto_guide.mjs +1 -1
- package/tests/howto_moments.mjs +3 -3
- package/tests/hub_account_page.mjs +1 -1
- package/tests/idea_capture_grades.mjs +2 -2
- package/tests/idea_develop_spark.mjs +1 -1
- package/tests/idea_filed_via.mjs +2 -2
- package/tests/idea_ideate_full.mjs +1 -1
- package/tests/idea_spark_mark.mjs +1 -1
- package/tests/idea_spark_queue.mjs +1 -1
- package/tests/idea_triage.mjs +1 -1
- package/tests/idea_triage_grade.mjs +3 -3
- package/tests/idea_vote_permission.mjs +113 -0
- package/tests/idea_votes_fold_db.mjs +123 -0
- package/tests/infra_exec_paths.mjs +12 -12
- package/tests/init.mjs +2 -2
- package/tests/instance_db_migrated_on_code_move.mjs +5 -5
- package/tests/instance_feedback.mjs +9 -9
- package/tests/instance_mismatch_guard.mjs +2 -2
- package/tests/integration_parked_ratchet.mjs +24 -2
- package/tests/intent_retry.mjs +1 -1
- package/tests/interaction_posture.mjs +3 -3
- package/tests/invite_expiry_db.mjs +1 -1
- package/tests/joinability_invite_only.mjs +1 -1
- package/tests/karma_vote_lists.mjs +270 -0
- package/tests/key-card.mjs +2 -2
- package/tests/key-status.mjs +2 -2
- package/tests/knip_ratchet_ambient.mjs +2 -2
- package/tests/known_failing_ratchet.mjs +1 -1
- package/tests/land_watch.mjs +2 -2
- package/tests/landing_legal_pages.mjs +1 -1
- package/tests/landing_page.mjs +2 -2
- package/tests/leaderboard_roles.mjs +4 -1
- package/tests/leak_scan_allowlist.mjs +8 -8
- package/tests/leak_scan_fail_closed.mjs +1 -1
- package/tests/lib_sh_resolution.mjs +9 -9
- package/tests/lifecycle-card.mjs +7 -7
- package/tests/linkify_check.mjs +2 -2
- package/tests/linkify_refs.mjs +1 -1
- package/tests/llm-cache-route.mjs +1 -1
- package/tests/local_preview.mjs +2 -2
- package/tests/main_audit.mjs +3 -3
- package/tests/main_audit_attestation.mjs +1 -1
- package/tests/main_audit_db_roster.mjs +2 -2
- package/tests/main_worktree_busy_reason.mjs +3 -3
- package/tests/markdown_line_endings.mjs +2 -2
- package/tests/me_streak_leaderboard_queries.mjs +193 -0
- package/tests/membership_kind_lockstep.mjs +1 -1
- package/tests/memory_isolation.mjs +16 -8
- package/tests/memory_map.mjs +2 -2
- package/tests/memory_pull.mjs +33 -25
- package/tests/merge_driver_register.mjs +3 -3
- package/tests/migration_allocation.mjs +2 -2
- package/tests/migration_namespace.mjs +1 -1
- package/tests/migrations_forward_safe.mjs +1 -1
- package/tests/mingle.mjs +1 -1
- package/tests/mirror_lineage_fulltree.mjs +3 -3
- package/tests/mirror_lineage_proof.mjs +2 -2
- package/tests/mirror_redact.mjs +4 -4
- package/tests/missing_dep_classify.mjs +3 -3
- package/tests/module-scope-map.mjs +22 -22
- package/tests/module_api_lazy.mjs +1 -1
- package/tests/module_assess_docs.mjs +2 -2
- package/tests/module_assess_installs.mjs +3 -3
- package/tests/module_assess_price_parity.mjs +5 -5
- package/tests/module_assess_score.mjs +5 -5
- package/tests/module_assess_security.mjs +4 -4
- package/tests/module_assess_tests.mjs +3 -3
- package/tests/module_assessments_migration.mjs +1 -1
- package/tests/module_catalog.mjs +2 -2
- package/tests/module_cli.mjs +3 -3
- package/tests/module_entitlements.mjs +1 -1
- package/tests/module_error_counts.mjs +1 -1
- package/tests/module_install.mjs +4 -4
- package/tests/module_loader.mjs +3 -1
- package/tests/module_owned_tests.mjs +1 -1
- package/tests/module_release_channels.mjs +1 -1
- package/tests/module_store_howto.mjs +3 -3
- package/tests/module_store_listing.mjs +139 -2
- package/tests/module_store_publish.mjs +4 -4
- package/tests/module_store_publish_route.mjs +1 -1
- package/tests/module_store_reassess.mjs +1 -1
- package/tests/module_store_registry_migration.mjs +1 -1
- package/tests/module_test_runner.mjs +3 -3
- package/tests/module_update.mjs +4 -4
- package/tests/module_upgrade_updates.mjs +1 -1
- package/tests/move_escalation.mjs +2 -2
- package/tests/nav_permission_atoms.mjs +3 -2
- package/tests/needs_rebase.mjs +7 -3
- package/tests/newcomer_floor.mjs +1 -1
- package/tests/newcomer_generator.mjs +48 -0
- package/tests/next_number.mjs +1 -1
- package/tests/no_artifact_escalation.mjs +3 -3
- package/tests/npm_release_where.mjs +1 -1
- package/tests/oauth_secret.mjs +2 -2
- package/tests/onboard.mjs +6 -6
- package/tests/onboarding_gate.mjs +2 -2
- package/tests/opt_in_skill_modules.mjs +3 -3
- package/tests/own_scope_guard.mjs +1 -1
- package/tests/own_session_revoke.mjs +4 -1
- package/tests/package_core.mjs +10 -11
- package/tests/page_inventory.mjs +5 -5
- package/tests/page_reader.mjs +7 -7
- package/tests/page_tweak_hold.mjs +6 -6
- package/tests/parallel-load.mjs +1 -1
- package/tests/paste_token_api_base.mjs +1 -1
- package/tests/paste_token_session_store.mjs +2 -2
- package/tests/pending_invite_purge_db.mjs +1 -1
- package/tests/permission_path.mjs +1 -1
- package/tests/pin_replay.mjs +1 -1
- package/tests/pin_write_key.mjs +7 -7
- package/tests/plain_cards.mjs +15 -15
- package/tests/pr_assign.mjs +1 -1
- package/tests/pre_push_hook.mjs +3 -3
- package/tests/preflight_checks.mjs +2 -2
- package/tests/prelaunch_gate.mjs +1 -1
- package/tests/privacy_matrix.mjs +1 -1
- package/tests/probe_cache.mjs +6 -6
- package/tests/profile_connections_ui.mjs +4 -3
- package/tests/profile_fields.mjs +11 -1
- package/tests/profile_privacy_owner_view.mjs +1 -1
- package/tests/profile_role_headline.mjs +10 -9
- package/tests/project_admin_console_model.mjs +1 -1
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_cloud_hop.mjs +1 -1
- package/tests/promote_goal_id.mjs +6 -6
- package/tests/promotion_sweep.mjs +1 -1
- package/tests/provision.mjs +24 -24
- package/tests/provision_account_identity.mjs +2 -2
- package/tests/provision_byo_host.mjs +1 -1
- package/tests/provision_caddy_wiring.mjs +2 -2
- package/tests/provision_disconnect.mjs +2 -2
- package/tests/provision_dns_first.mjs +1 -1
- package/tests/provision_render.mjs +9 -9
- package/tests/provision_repo_private.mjs +3 -3
- package/tests/provision_resource_caps.mjs +5 -5
- package/tests/provision_restart.mjs +3 -3
- package/tests/provision_settings_apply.mjs +7 -7
- package/tests/provision_teardown_address.mjs +2 -2
- package/tests/provisioning_capacity.mjs +5 -5
- package/tests/provisioning_credential_preflight.mjs +1 -1
- package/tests/provisioning_disconnect.mjs +1 -1
- package/tests/provisioning_error_note_scope.mjs +4 -4
- package/tests/provisioning_paid_shape_gate.mjs +2 -2
- package/tests/provisioning_planet_physics.mjs +16 -0
- package/tests/provisioning_runner_drift_notice.mjs +2 -2
- package/tests/provisioning_settings_env.mjs +1 -1
- package/tests/pruned_lockfile_integrity.mjs +1 -1
- package/tests/public_mirror_export.mjs +1 -1
- package/tests/publish_integration_gate.mjs +2 -2
- package/tests/publish_manifest.mjs +6 -6
- package/tests/publish_watch.mjs +1 -1
- package/tests/push_path_brief.mjs +2 -2
- package/tests/raise.mjs +248 -0
- package/tests/rank_roles_audit.mjs +1 -1
- package/tests/rank_tier_single_source.mjs +2 -2
- package/tests/rate_limit_public.mjs +1 -1
- package/tests/read_default_meter.mjs +2 -2
- package/tests/read_session_export.mjs +1 -1
- package/tests/rebase_gate_set_aside.mjs +1 -1
- package/tests/recall_cli.mjs +1 -1
- package/tests/recall_corpus_health.mjs +3 -2
- package/tests/recall_hints.mjs +1 -1
- package/tests/recruiter_sliver_boundary.mjs +1 -1
- package/tests/redteam_patrol.mjs +2 -2
- package/tests/regrade_actual_minutes.mjs +2 -2
- package/tests/regrade_eligibility.mjs +2 -2
- package/tests/release_core.mjs +2 -2
- package/tests/release_notes.mjs +4 -4
- package/tests/rename_history_restraint.mjs +3 -3
- package/tests/rename_ledger_prose.mjs +6 -6
- package/tests/render_api.mjs +1 -1
- package/tests/render_check.mjs +4 -4
- package/tests/render_deploy.mjs +1 -1
- package/tests/repo_info_binding.mjs +1 -1
- package/tests/repo_map.mjs +5 -5
- package/tests/reward_mode_cost_plus_only.mjs +7 -3
- package/tests/reward_mode_surfacing.mjs +1 -1
- package/tests/role_responsibilities.mjs +1 -1
- package/tests/rot_inbox.mjs +10 -7
- package/tests/route_permission_check.mjs +1 -1
- package/tests/route_shadow_guard.mjs +1 -1
- package/tests/route_validation.mjs +1 -1
- package/tests/routine_capture_kind.mjs +6 -6
- package/tests/routine_schedule.mjs +9 -9
- package/tests/run_routine.mjs +5 -5
- package/tests/run_routine_two_root.mjs +5 -5
- package/tests/runner_drift.mjs +17 -17
- package/tests/sandbox_stage.mjs +5 -5
- package/tests/scope_map_guard.mjs +7 -7
- package/tests/scouting_owner_access_and_reach.mjs +1 -1
- package/tests/scripts_gds_forwarders.mjs +224 -0
- package/tests/scrubber_corpus.mjs +2 -2
- package/tests/seam_wiring_guard.mjs +1 -1
- package/tests/search_ingest.mjs +2 -2
- package/tests/search_ingest_db.mjs +1 -1
- package/tests/search_ingest_nul.mjs +6 -6
- package/tests/seed_verify_criteria.mjs +3 -3
- package/tests/sequence.mjs +3 -3
- package/tests/served_paths_shipped.mjs +3 -3
- package/tests/session_506_regression.mjs +1 -1
- package/tests/session_accessibility.mjs +1 -1
- package/tests/session_caps.mjs +1 -1
- package/tests/session_cost.mjs +1 -1
- package/tests/session_digest.mjs +2 -2
- package/tests/session_eval.mjs +6 -6
- package/tests/session_index.mjs +1 -1
- package/tests/session_records.mjs +4 -4
- package/tests/session_rename_fallback.mjs +2 -2
- package/tests/session_reward_on_land.mjs +7 -149
- package/tests/session_reward_on_land_db.mjs +191 -0
- package/tests/session_reward_reconcile.mjs +6 -88
- package/tests/session_reward_reconcile_db.mjs +132 -0
- package/tests/session_transcript.mjs +1 -1
- package/tests/sessions_kit_pager.mjs +2 -1
- package/tests/setup_device_flow_fallback.mjs +2 -2
- package/tests/setup_smoke.mjs +12 -5
- package/tests/setup_status_facts.mjs +54 -12
- package/tests/ship_autolinkify.mjs +2 -2
- package/tests/ship_automerge_injection.mjs +1 -1
- package/tests/ship_base_flag.mjs +1 -1
- package/tests/ship_broadcast_routing.mjs +1 -1
- package/tests/ship_broadcast_server.mjs +2 -2
- package/tests/ship_builder_model.mjs +1 -1
- package/tests/ship_cannot_lie.mjs +9 -9
- package/tests/ship_card.mjs +2 -2
- package/tests/ship_ci_deploy.mjs +7 -7
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/ship_land_ci_not_started.mjs +3 -3
- package/tests/ship_land_conflict.mjs +3 -3
- package/tests/ship_land_proof.mjs +3 -3
- package/tests/ship_land_red_pr.mjs +5 -5
- package/tests/ship_merge_gate.mjs +14 -14
- package/tests/ship_mode.mjs +1 -1
- package/tests/ship_mode_first_ship.mjs +4 -4
- package/tests/ship_mode_hands_off.mjs +8 -8
- package/tests/ship_post_land_verify.mjs +1 -1
- package/tests/ship_preflight.mjs +19 -19
- package/tests/ship_premerge.mjs +1 -1
- package/tests/ship_publish_error.mjs +2 -2
- package/tests/ship_push_diagnosis.mjs +1 -1
- package/tests/ship_regen_copy_inventory.mjs +3 -3
- package/tests/ship_resume.mjs +4 -4
- package/tests/ship_smoke_gate.mjs +1 -1
- package/tests/ship_upload_skip.mjs +1 -1
- package/tests/ship_upstream_deletion.mjs +1 -1
- package/tests/ship_vendored_instance.mjs +8 -8
- package/tests/ship_visual_regrade.mjs +3 -3
- package/tests/ship_zero_commit.mjs +1 -1
- package/tests/skill_docs_unavailable_regrade.mjs +3 -3
- package/tests/skill_friction_kind.mjs +1 -1
- package/tests/skill_grade_recover.mjs +1 -1
- package/tests/skill_grader_health.mjs +1 -1
- package/tests/skill_lint.mjs +1 -1
- package/tests/skill_menu_ruling.mjs +1 -1
- package/tests/skill_preflight.mjs +7 -7
- package/tests/skip_is_not_pass.mjs +22 -1
- package/tests/smoke_dependencies_witness.mjs +2 -2
- package/tests/smoke_preflight.mjs +1 -1
- package/tests/smoke_target.mjs +2 -2
- package/tests/smoke_write_contract.mjs +2 -2
- package/tests/sound_player.mjs +8 -8
- package/tests/source_text_test_ratchet.mjs +97 -0
- package/tests/speciality_corpus_scope.mjs +1 -1
- package/tests/speciality_session_skills.mjs +1 -1
- package/tests/sso_checkin_legacy_field_gone.mjs +4 -2
- package/tests/stale_timer.mjs +3 -3
- package/tests/stale_worktrees.mjs +1 -1
- package/tests/start_env_guard.mjs +1 -1
- package/tests/start_goal_first.mjs +3 -3
- package/tests/start_inbox_count.mjs +1 -1
- package/tests/start_rebase_warning.mjs +1 -1
- package/tests/status_default_version.mjs +3 -3
- package/tests/status_mirror_sync.mjs +1 -1
- package/tests/status_page_no_dead_uptime.mjs +80 -0
- package/tests/stop_refcheck.mjs +1 -1
- package/tests/strand_watch.mjs +1 -1
- package/tests/stranded_rows.mjs +1 -1
- package/tests/stuck_detector.mjs +14 -14
- package/tests/sweep_app_account.mjs +3 -3
- package/tests/task_cli.mjs +2 -2
- package/tests/task_detail_ui.mjs +11 -1
- package/tests/task_discipline_required.mjs +2 -2
- package/tests/task_list_paging.mjs +1 -1
- package/tests/task_visual_slots.mjs +2 -2
- package/tests/terms_acceptance.mjs +1 -1
- package/tests/terms_grandfather_db.mjs +1 -1
- package/tests/test_path_guard.mjs +1 -1
- package/tests/triage_development.mjs +1 -1
- package/tests/tweak_batch_apply.mjs +2 -2
- package/tests/tweak_land_heal.mjs +3 -3
- package/tests/tweak_unattended.mjs +3 -3
- package/tests/ui_design_skills.mjs +2 -2
- package/tests/unit_report_status.mjs +2 -2
- package/tests/unit_runner_serial.mjs +2 -2
- package/tests/unit_runner_teardown_abort.mjs +1 -1
- package/tests/update_channel.mjs +2 -2
- package/tests/update_channel_db.mjs +4 -4
- package/tests/update_subscription_engine.mjs +26 -26
- package/tests/update_sweep_home.mjs +14 -14
- package/tests/upgrade.mjs +11 -11
- package/tests/upgrade_artist_gate.mjs +4 -4
- package/tests/upgrade_commit_identity.mjs +2 -2
- package/tests/upgrade_migrate.mjs +7 -7
- package/tests/upgrade_operator.mjs +2 -2
- package/tests/upgrade_outcome.mjs +2 -2
- package/tests/upgrade_persist_pin.mjs +5 -5
- package/tests/upkeep_home.mjs +1 -1
- package/tests/version_close.mjs +2 -2
- package/tests/version_close_cli.mjs +2 -2
- package/tests/version_close_route.mjs +1 -1
- package/tests/version_literals.mjs +23 -23
- package/tests/visual_review.mjs +5 -5
- package/tests/wedge_remedy.mjs +5 -5
- package/tests/welcome_boot.mjs +116 -0
- package/tests/welcome_boot_browser.mjs +271 -0
- package/tests/wizard_born_first_board.mjs +3 -3
- package/tests/wizard_demo.mjs +1 -1
- package/tests/wizard_one_page.mjs +4 -3
- package/tests/wizard_physics.mjs +3 -1
- package/tests/wizard_physics_more.mjs +42 -3
- package/tests/work_title_wrap.mjs +1 -1
- package/tests/workflow_exec_paths.mjs +6 -6
- package/tests/worktree_claim_guard.mjs +1 -1
- package/tests/worktree_helper.mjs +2 -2
- package/tests/write_validation_gaps.mjs +3 -3
- package/scripts/gds/api.sh +0 -6
- package/scripts/gds/audit-exports.js +0 -128
- package/scripts/gds/resolve-skill-model.js +0 -57
- package/scripts/gds/security-baseline-check.sh +0 -180
- package/scripts/gds/spike-task-map.js +0 -899
- package/scripts/status-probe.mjs +0 -213
- package/src/bongos/uptime-poller.js +0 -93
- package/tests/government_fold_bench.mjs +0 -50
- package/tests/spike_task_map.mjs +0 -422
- package/tests/status-probe.mjs +0 -89
- /package/{scripts/gds → modules/raise}/CLAUDE.md +0 -0
- /package/scripts/{gds → bongos}/cloudbongos-token-isolation.conf +0 -0
- /package/scripts/{gds → bongos}/discipline-modes.json +0 -0
- /package/scripts/{gds → bongos}/retired-core-skills.json +0 -0
package/scripts/gds/provision.js
CHANGED
|
@@ -1,1479 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/provision.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
+
// The real script is scripts/bongos/provision.js. This forwarder keeps callers that still name
|
|
5
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
2
6
|
'use strict';
|
|
3
|
-
|
|
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/gds/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
|
-
};
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'provision.js');
|