@bongos/core 1.17.1 → 1.17.3
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 +521 -281
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +9 -6
- package/docs/api/openapi.json +65 -26
- package/docs/api-reference.md +3 -2
- package/docs/architecture.md +2 -2
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +18 -17
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +6 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/private-npm-distribution.md +17 -0
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +55 -0
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/modules/discord/module.json +2 -1
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/hall-ui/public/builders.js +137 -620
- package/modules/hall-ui/public/goal-inbox.js +232 -0
- package/modules/hall-ui/public/goals-page.js +4 -1
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/hall-kit.js +474 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -5
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/instances-panel.js +24 -3
- package/modules/hall-ui/public/new-project-wizard.js +7 -5
- package/modules/hall-ui/public/style.css +168 -0
- package/modules/hall-ui/public/work.css +194 -0
- package/modules/hall-ui/public/work.html +46 -374
- package/modules/hall-ui/public/work.js +137 -102
- package/modules/lifecycle/criterion-suggest.js +80 -0
- package/modules/lifecycle/db.js +42 -5
- package/modules/lifecycle/routes/goals.js +26 -0
- package/modules/lifecycle/routes/tasks.js +21 -0
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/secret-scrub.js +10 -0
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +30 -9
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/init.js +59 -3
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/onboard.js +4 -0
- package/scripts/gds/package-core.js +18 -3
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +116 -9
- package/scripts/hall-preview/README.md +103 -0
- package/scripts/hall-preview/fixtures/access-requests.json +1 -0
- package/scripts/hall-preview/fixtures/achievements.json +76 -0
- package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
- package/scripts/hall-preview/fixtures/audit-log.json +1 -0
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
- package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
- package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
- package/scripts/hall-preview/fixtures/goals.json +502 -0
- package/scripts/hall-preview/fixtures/goals__25.json +96 -0
- package/scripts/hall-preview/fixtures/goals__26.json +67 -0
- package/scripts/hall-preview/fixtures/goals__3.json +103 -0
- package/scripts/hall-preview/fixtures/goals__35.json +49 -0
- package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
- package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
- package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
- package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
- package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
- package/scripts/hall-preview/fixtures/inbox.json +330 -0
- package/scripts/hall-preview/fixtures/instance.json +124 -0
- package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
- package/scripts/hall-preview/fixtures/me.json +476 -0
- package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
- package/scripts/hall-preview/fixtures/override-requests.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
- package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
- package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
- package/scripts/hall-preview/fixtures/public__grades.json +50 -0
- package/scripts/hall-preview/fixtures/public__progress.json +124 -0
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
- package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
- package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
- package/scripts/hall-preview/fixtures/security__docs.json +1 -0
- package/scripts/hall-preview/fixtures/security__reports.json +1 -0
- package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
- package/scripts/hall-preview/fixtures/tasks.json +2420 -0
- package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
- package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
- package/scripts/hall-preview/fixtures/versions.json +89 -0
- package/scripts/hall-preview/refresh-fixtures.js +77 -0
- package/scripts/hall-preview/server.js +178 -0
- package/scripts/migrate.sh +29 -4
- package/src/bongos/auth.js +14 -1
- package/src/bongos/optimizer.js +37 -2
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/module-api.js +12 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +20 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/criterion_suggest.mjs +106 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +25 -2
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +47 -0
- package/tests/migrate_root_resolution.mjs +97 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/onboard.mjs +13 -0
- package/tests/package_core.mjs +8 -0
- package/tests/provisioning_github_app_routes.mjs +15 -0
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +131 -0
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
|
@@ -44,7 +44,9 @@ const PUBLISH_ALLOWLIST = [
|
|
|
44
44
|
'src/', // platform engine + GDS core (the game residue is denied below)
|
|
45
45
|
'scripts/', // build + methodology tooling (deploy is denied below)
|
|
46
46
|
'bin/', // the `cloudbongos`/`bongos` CLI entrypoint
|
|
47
|
-
|
|
47
|
+
// NB: repo-root server.js is the GAME+platform entry (top-level require('colyseus'),
|
|
48
|
+
// a pruned core dep) — HOST-excluded in CONTENT_EXCLUDE below; the core boots
|
|
49
|
+
// game-less via src/platform-server.js (package-core sets that as `main`). task 2134.
|
|
48
50
|
// --- the build system's shared schema + its tests ---
|
|
49
51
|
'migrations/',
|
|
50
52
|
'tests/',
|
|
@@ -63,6 +65,7 @@ const PUBLISH_ALLOWLIST = [
|
|
|
63
65
|
'modules/status-ui/',
|
|
64
66
|
'modules/dev-box/',
|
|
65
67
|
'modules/discord/',
|
|
68
|
+
'modules/provisioning/', // instance-hosting control plane — CORE like its sibling dev-box (same do-api/cf-tunnel toolchain, all core). Reclassified IN so the core CLI scripts/gds/provision.js resolves it from the core package post-split, not a MODULE_NOT_FOUND (task 2135). default:false — shipped ≠ enabled.
|
|
66
69
|
// --- neutral (customer-0) config starters + their schemas (the filled-in *.json twins are denied) ---
|
|
67
70
|
'config/branding.neutral.json',
|
|
68
71
|
'config/modules.neutral.json',
|
|
@@ -77,7 +80,7 @@ const PUBLISH_ALLOWLIST = [
|
|
|
77
80
|
'CONTRIBUTING.md',
|
|
78
81
|
'CODE_OF_CONDUCT.md',
|
|
79
82
|
'docs/adr/', // sanitized — ADR 0065 §4 "docs/ADRs (sanitized)"
|
|
80
|
-
'docs/recipes/',
|
|
83
|
+
'docs/recipes/', // portable METHODOLOGY recipes only; project-specific ones (art/staging/load-test/standup/decouple) are denied below — task 2153
|
|
81
84
|
'docs/design/',
|
|
82
85
|
'docs/research/',
|
|
83
86
|
'docs/architecture.md',
|
|
@@ -85,6 +88,11 @@ const PUBLISH_ALLOWLIST = [
|
|
|
85
88
|
// renders broken onboarding + API-reference pages (task 2078). Redacted like the
|
|
86
89
|
// rest of this docs section; the diagrams' .png/.svg regenerate on the box.
|
|
87
90
|
'docs/onboarding/', // primer.md + drachmae-and-karma.md + diagram sources (/builders/primer · /drachmae · /diagrams)
|
|
91
|
+
// API artifacts (classification settled, task 2141): CORE-shipped STARTERS so a fresh
|
|
92
|
+
// hall/docs render before first regen — but per-instance GENERATED. init + `bongos
|
|
93
|
+
// upgrade` regenerate them INTO the instance; the instance-root fitness gate
|
|
94
|
+
// (gen-api-docs --check) + tests/api_client.mjs guard drift. They ship; they are not a
|
|
95
|
+
// monolith-only artifact.
|
|
88
96
|
'docs/api/openapi.json', // the generated OpenAPI 3.1 spec served at /docs/openapi.json
|
|
89
97
|
'docs/api-reference.md', // the markdown API reference
|
|
90
98
|
'docs/modules-contract.md',
|
|
@@ -161,12 +169,33 @@ const CONTENT_EXCLUDE = [
|
|
|
161
169
|
'src/world/',
|
|
162
170
|
'src/rooms/',
|
|
163
171
|
'src/preview-server.js',
|
|
172
|
+
'server.js', // repo-root game+platform entry: top-level require('colyseus') (a pruned core dep) → un-runnable in the game-less package; the core boots via src/platform-server.js. Mirrors src/preview-server.js. (task 2134)
|
|
164
173
|
// the art feature's CLI tooling lives under scripts/ (legacy top-level) but belongs
|
|
165
174
|
// to the excluded art module — exclude it with the feature it serves.
|
|
166
175
|
'scripts/art/',
|
|
176
|
+
'scripts/gds/art-stage.js', // art sandbox-staging CLI: requires modules/art-pipeline's build_tilesheet.py; excluded with the art feature it serves so a core file never requires an excluded module (task 2135)
|
|
167
177
|
'scripts/tile-add.js',
|
|
168
178
|
'scripts/tile-find.js',
|
|
169
179
|
'scripts/tile-recommend.js',
|
|
180
|
+
// The art pipeline's operational RECIPE — belongs to the excluded art module; exclude it
|
|
181
|
+
// with the feature it serves (mirrors scripts/art/ above). docs/recipes/ is allowlisted
|
|
182
|
+
// wholesale, so this deny is what keeps the game-art recipe out of a non-art core. (task 2153)
|
|
183
|
+
'docs/recipes/pixel-art-pipeline.md',
|
|
184
|
+
// Host-feature TESTS that statically import an EXCLUDED module's source. tests/ is
|
|
185
|
+
// allowlisted (the platform/methodology suite is core), but these cover the game/art
|
|
186
|
+
// HOST modules and import their source directly — so package-core (which selects via
|
|
187
|
+
// isPublishable) would ship them WITHOUT the module they need → MODULE_NOT_FOUND when a
|
|
188
|
+
// core CI/consumer runs them, and the R89 cutover would DELETE OTB's own game/art tests
|
|
189
|
+
// (they cover kept host modules; they cannot live in core). Exclude each with the feature
|
|
190
|
+
// it serves — mirrors scripts/gds/art-stage.js above. The guard in tests/publish_manifest.mjs
|
|
191
|
+
// derives this set from disk and fails if a new host-module test is added but not listed. (task 2147)
|
|
192
|
+
'tests/walkability.mjs', // → modules/game/world
|
|
193
|
+
'tests/terrain_picker.mjs', // → modules/game/public/game/world
|
|
194
|
+
'tests/snapshot_picker.mjs', // → modules/game/public/game/world
|
|
195
|
+
'tests/sprite_facing.mjs', // → modules/game/public/game/player
|
|
196
|
+
'tests/turn_in_place.mjs', // → modules/game/public/game/player
|
|
197
|
+
'tests/day_cycle.mjs', // → modules/game/public/game/fx
|
|
198
|
+
'tests/art_key.mjs', // → modules/art-pipeline
|
|
170
199
|
];
|
|
171
200
|
|
|
172
201
|
// (4) INSTANCE IDENTITY + HISTORY — this host's literals, ops config, build history, and
|
|
@@ -178,6 +207,11 @@ const INSTANCE_EXCLUDE = [
|
|
|
178
207
|
'config/devbox-app.json',
|
|
179
208
|
'config/hierarchy.json',
|
|
180
209
|
'CODEOWNERS', // names the owner (a protected surface, excluded as identity)
|
|
210
|
+
// instance SEED migrations — this host's scope-truth SQL (planning seeds, world data).
|
|
211
|
+
// ADR 0105: a consumer's core must not carry them; migrate.sh reads instance seeds from
|
|
212
|
+
// resolveInstanceRoot() (SEED_INSTANCE), so the consumer repo owns them. Core migrations
|
|
213
|
+
// (migrations/core_NNN_*.sql + bare legacy migrations/NNN_*.sql) still publish. (task 2140)
|
|
214
|
+
'migrations/instance/',
|
|
181
215
|
// instance-specific docs + project history
|
|
182
216
|
'docs/project-context.md',
|
|
183
217
|
'docs/lore/',
|
|
@@ -192,6 +226,30 @@ const INSTANCE_EXCLUDE = [
|
|
|
192
226
|
'docs/graphics-stress-test-plan.md',
|
|
193
227
|
'docs/security-baseline.md',
|
|
194
228
|
'docs/routes-permissions.md',
|
|
229
|
+
// instance-specific OPERATIONAL recipes — this host's staging + load-test artifacts, its
|
|
230
|
+
// specific control-plane / vanity-door standups, and its one-time monolith-decouple runbook.
|
|
231
|
+
// docs/recipes/ is allowlisted wholesale for the PORTABLE methodology recipes; these teach
|
|
232
|
+
// nothing a different instance would use, so they stay in this repo and never ship in the
|
|
233
|
+
// core (mirrors migrations/instance above). Redaction only de-brands them — this stops the
|
|
234
|
+
// irrelevant-noise at the source. The module-owned RECIPE redesign is deferred (idea 536). (task 2153)
|
|
235
|
+
'docs/recipes/staging-data.md',
|
|
236
|
+
'docs/recipes/parallel-load-test.md',
|
|
237
|
+
'docs/recipes/parallel-load-results-2026-05-30.md',
|
|
238
|
+
'docs/recipes/claude-gemini-bongos-doors.md',
|
|
239
|
+
'docs/recipes/cloudbongos-standup.md',
|
|
240
|
+
'docs/recipes/cloudbongos-staging-gate.md',
|
|
241
|
+
'docs/recipes/amazonprimea-decouple-r89.md',
|
|
242
|
+
'docs/recipes/amazonprimea-decouple-rollback-r89.md',
|
|
243
|
+
// instance-specific GAME-DESIGN specs — this host's pixel-art / level briefs (the container
|
|
244
|
+
// buildings, its palette, the world-map scroll). docs/design/ is allowlisted wholesale for the
|
|
245
|
+
// PORTABLE design docs (modular-architecture/*, the vanilla-hall-ui redesign scope); these four
|
|
246
|
+
// describe THIS instance's game and genericize to nothing, so they stay in this repo and never
|
|
247
|
+
// ship in the core. Redaction only de-brands NAMES — the world-narrative prose in them is not a
|
|
248
|
+
// name and survives redaction, so exclude at the source (mirrors the recipe carve-out above). (task 2155)
|
|
249
|
+
'docs/design/container-exterior.md',
|
|
250
|
+
'docs/design/container-exterior-tiles.md',
|
|
251
|
+
'docs/design/container-interior.md',
|
|
252
|
+
'docs/design/world-map-scroll.md',
|
|
195
253
|
'blockers/',
|
|
196
254
|
'limitations/',
|
|
197
255
|
'generative-ideas.md',
|
|
@@ -23,9 +23,20 @@
|
|
|
23
23
|
const fs = require('fs');
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const { execFileSync } = require('child_process');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-config');
|
|
27
|
+
|
|
28
|
+
// TWO roots (ADR 0108 configurable-root; task 2138). run-routine.js is a CORE script, so it
|
|
29
|
+
// ships alongside src/, scripts/gds/*, scripts/discord/*, and modules/ — all under
|
|
30
|
+
// resolveCoreRoot(), which is where core step scripts + the grader resolve from. The ROUTINES
|
|
31
|
+
// themselves are TOPOLOGY: `bongos init/upgrade` materializes .claude/scheduled-tasks/ INTO the
|
|
32
|
+
// instance (claude-materialize.js), so they're found under resolveInstanceRoot(). Today both
|
|
33
|
+
// roots are the one checkout — BYTE-IDENTICAL to the old `path.resolve(__dirname,'..','..')`;
|
|
34
|
+
// post-split TASKS_DIR points at the instance repo while core scripts resolve inside
|
|
35
|
+
// node_modules/@bongos/core. (Do NOT reintroduce a hardcoded __dirname/../.. — see scripts/gds/CLAUDE.md.)
|
|
36
|
+
const CORE_ROOT = resolveCoreRoot();
|
|
37
|
+
const INSTANCE_ROOT = resolveInstanceRoot();
|
|
38
|
+
const TASKS_DIR = path.join(INSTANCE_ROOT, '.claude', 'scheduled-tasks');
|
|
39
|
+
const DISCORD_POST = path.join(CORE_ROOT, 'scripts', 'discord', 'post.js');
|
|
29
40
|
const VALID_MODES = new Set(['deterministic', 'hybrid', 'judgment']);
|
|
30
41
|
const VALID_SINKS = new Set(['discord', 'gds', 'none']);
|
|
31
42
|
const ALERT_CHANNEL = process.env.OTB_ROUTINE_ALERT_CHANNEL || 'ship_feed';
|
|
@@ -104,10 +115,36 @@ function tokenize(cmd) {
|
|
|
104
115
|
return out;
|
|
105
116
|
}
|
|
106
117
|
|
|
118
|
+
function isNodeExec(a) {
|
|
119
|
+
const base = path.basename(String(a || '')).toLowerCase();
|
|
120
|
+
return a === process.execPath || base === 'node' || base === 'node.exe';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Pure: resolve a routine step's argv so it runs in BOTH layouts (ADR 0108; task 2138).
|
|
124
|
+
// • `bongos exec <rel> [args]` (the materialized invocation form) → run <rel> under the CORE
|
|
125
|
+
// root, exactly like `bongos exec` itself does (bin/bongos.js resolveExecTarget).
|
|
126
|
+
// • `node <path> [args]` → resolve <path>: a `scripts/...` core path under coreRoot; any other
|
|
127
|
+
// relative path (notably the `.claude/scheduled-tasks/<name>/<x>.js` companions) under the
|
|
128
|
+
// instance root. Absolute paths and non-node commands pass through untouched.
|
|
129
|
+
// Commands come from committed SKILL.md (trusted), so this is path resolution, not sandboxing.
|
|
130
|
+
// Today coreRoot === instanceRoot (single checkout) so this is byte-identical to the old run;
|
|
131
|
+
// only after the split do the two roots diverge. Exported + unit-tested.
|
|
132
|
+
function resolveStepArgv(argv, { coreRoot = CORE_ROOT, instanceRoot = INSTANCE_ROOT } = {}) {
|
|
133
|
+
if (!argv || !argv.length) return argv || [];
|
|
134
|
+
if (argv[0] === 'bongos' && argv[1] === 'exec' && argv[2]) {
|
|
135
|
+
return [process.execPath, path.join(coreRoot, argv[2]), ...argv.slice(3)];
|
|
136
|
+
}
|
|
137
|
+
if (isNodeExec(argv[0]) && argv[1] && !path.isAbsolute(argv[1])) {
|
|
138
|
+
const base = argv[1].startsWith('scripts/') ? coreRoot : instanceRoot;
|
|
139
|
+
return [process.execPath, path.join(base, argv[1]), ...argv.slice(2)];
|
|
140
|
+
}
|
|
141
|
+
return argv;
|
|
142
|
+
}
|
|
143
|
+
|
|
107
144
|
function opsAlert(message) {
|
|
108
145
|
try {
|
|
109
|
-
execFileSync(process.execPath, [
|
|
110
|
-
{ cwd:
|
|
146
|
+
execFileSync(process.execPath, [DISCORD_POST, '--channel', ALERT_CHANNEL, String(message).slice(0, 1800)],
|
|
147
|
+
{ cwd: INSTANCE_ROOT, stdio: 'ignore' });
|
|
111
148
|
} catch (_) { /* best-effort: the alert must never mask the real failure */ }
|
|
112
149
|
}
|
|
113
150
|
|
|
@@ -116,19 +153,20 @@ function routeOutput(sink, stdout, routineName) {
|
|
|
116
153
|
if (text) console.log(text);
|
|
117
154
|
if (sink === 'discord' && text) {
|
|
118
155
|
try {
|
|
119
|
-
execFileSync(process.execPath, [
|
|
120
|
-
{ cwd:
|
|
156
|
+
execFileSync(process.execPath, [DISCORD_POST, '--channel', ALERT_CHANNEL, `[${routineName}] ${text}`.slice(0, 1800)],
|
|
157
|
+
{ cwd: INSTANCE_ROOT, stdio: 'ignore' });
|
|
121
158
|
} catch (_) { /* best-effort */ }
|
|
122
159
|
}
|
|
123
160
|
// sink 'gds' / 'none': the step's command performed its own GDS side effects.
|
|
124
161
|
}
|
|
125
162
|
|
|
126
163
|
function runStep(step, routineName, { dryRun }) {
|
|
127
|
-
const
|
|
128
|
-
if (!
|
|
164
|
+
const tokens = tokenize(step.command);
|
|
165
|
+
if (!tokens.length) return '';
|
|
129
166
|
if (dryRun) { console.log(`[dry-run] would run (${step.sink}): ${step.command}`); return ''; }
|
|
167
|
+
const argv = resolveStepArgv(tokens);
|
|
130
168
|
const out = execFileSync(argv[0], argv.slice(1), {
|
|
131
|
-
cwd:
|
|
169
|
+
cwd: INSTANCE_ROOT, // routines run in the instance repo (companions + git-touching steps)
|
|
132
170
|
encoding: 'utf8',
|
|
133
171
|
env: { ...process.env, OTB_COST_CRON_ROUTINE: routineName },
|
|
134
172
|
});
|
|
@@ -140,7 +178,7 @@ function runJudgment(routineName, skillBody, { dryRun }) {
|
|
|
140
178
|
if (dryRun) { console.log(`[dry-run] judgment mode → would spawn claude -p over ${routineName}/SKILL.md`); return; }
|
|
141
179
|
// Status quo: a wrapped session reading the SKILL body. Reuse the shared spawn.
|
|
142
180
|
const grader = require('../../modules/grading/grader');
|
|
143
|
-
return grader.runSubagent({ prompt: skillBody, opts: { cwd:
|
|
181
|
+
return grader.runSubagent({ prompt: skillBody, opts: { cwd: INSTANCE_ROOT } });
|
|
144
182
|
}
|
|
145
183
|
|
|
146
184
|
async function main() {
|
|
@@ -176,7 +214,7 @@ async function main() {
|
|
|
176
214
|
try { const j = JSON.parse(String(out).trim().split('\n').pop() || '{}'); if (j && j.needs_judgment) payload = j.needs_judgment; } catch (_) {}
|
|
177
215
|
if (payload) {
|
|
178
216
|
const grader = require('../../modules/grading/grader');
|
|
179
|
-
await grader.runSubagent({ prompt: String(payload), opts: { cwd:
|
|
217
|
+
await grader.runSubagent({ prompt: String(payload), opts: { cwd: INSTANCE_ROOT } });
|
|
180
218
|
}
|
|
181
219
|
}
|
|
182
220
|
}
|
|
@@ -194,4 +232,4 @@ if (require.main === module) {
|
|
|
194
232
|
main();
|
|
195
233
|
}
|
|
196
234
|
|
|
197
|
-
module.exports = { parseRoutineFrontmatter, tokenize, VALID_MODES, VALID_SINKS };
|
|
235
|
+
module.exports = { parseRoutineFrontmatter, tokenize, resolveStepArgv, isNodeExec, VALID_MODES, VALID_SINKS };
|
|
@@ -65,6 +65,10 @@ const SUPPORT_MODULES = new Set(['helpers']);
|
|
|
65
65
|
const INTEGRATION = new Set([
|
|
66
66
|
'asset_version', 'auth_flow_slice1', 'backfill_sessions', 'bfg', 'bfg_write',
|
|
67
67
|
'box_connect_e2e', 'box_onboard', 'boxes', 'cf_tunnel', 'cli_token_reissue',
|
|
68
|
+
// consumer_layout_boot builds the core .tgz, `npm install`s it, and boots a live
|
|
69
|
+
// platform-server — network + package build + a spawned server, run by its own
|
|
70
|
+
// .github/workflows/consumer-layout-boot.yml, not this DB-free gate (task 2142).
|
|
71
|
+
'consumer_layout_boot',
|
|
68
72
|
'discord_autojoin', 'discord_oauth', 'linkify_refs', 'memory_isolation',
|
|
69
73
|
'memory_pull', 'parallel-load', 'session_records', 'setup_smoke',
|
|
70
74
|
'status-probe', 'subdomain_auth',
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
//
|
|
4
|
+
// scripts/gds/update-channel.js — the update-subscription POLICY core (task 2150, ADR 0136).
|
|
5
|
+
//
|
|
6
|
+
// "How does a consumer instance subscribe to regular @bongos/core updates?" The owner-chosen
|
|
7
|
+
// answer (2026-07-09): **patch-only by default, health-gated, auto-rollback, opt-in, autonomy-gated.**
|
|
8
|
+
// This module is the PURE, testable heart of that policy — the semver + channel math + the
|
|
9
|
+
// subscription-manifest loader. It launches nothing and mutates nothing; the scheduled routine
|
|
10
|
+
// (.claude/scheduled-tasks/core-update-subscription/subscribe.js) drives the actual `bongos upgrade`
|
|
11
|
+
// per subscribed instance using what this module resolves.
|
|
12
|
+
//
|
|
13
|
+
// A per-instance UPDATE CHANNEL bounds how far an unattended upgrade may jump:
|
|
14
|
+
// pinned — never auto-upgrade (manual `bongos upgrade` only).
|
|
15
|
+
// patch — take a higher PATCH within the same major.minor (1.17.1 → 1.17.4, never 1.18.0). DEFAULT.
|
|
16
|
+
// minor — take a higher MINOR within the same major (1.17.1 → 1.18.0, never 2.0.0).
|
|
17
|
+
// A MAJOR bump is NEVER automatic under any channel — a major is a breaking change that wants a human.
|
|
18
|
+
// Prereleases (1.17.2-rc.1) are never auto-target either — only stable releases.
|
|
19
|
+
//
|
|
20
|
+
// WHY the registry as the source (not vendored tarballs): a subscription needs a LIVE source of
|
|
21
|
+
// truth for "what's the newest published core?" — that's `npm view @bongos/core versions` against the
|
|
22
|
+
// private registry (ADR 0134, task 2090). The vendored path is for a one-off manual bump.
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const { spawnSync } = require('node:child_process');
|
|
27
|
+
|
|
28
|
+
const CORE_PKG = '@bongos/core';
|
|
29
|
+
const VALID_CHANNELS = ['pinned', 'patch', 'minor'];
|
|
30
|
+
const DEFAULT_CHANNEL = 'patch';
|
|
31
|
+
// The subscription manifest an operator edits to opt instances in. Ships EMPTY (instances: []) in the
|
|
32
|
+
// core template — a fresh instance subscribes nothing until the operator lists a target here.
|
|
33
|
+
const SUBSCRIPTIONS_REL = path.join('config', 'update-subscriptions.json');
|
|
34
|
+
|
|
35
|
+
// ---- semver (stable x.y.z, optional prerelease tag) -------------------------
|
|
36
|
+
// A deliberately small parser — the repo avoids an npm-semver dependency in the CLI path (see
|
|
37
|
+
// upgrade.js versionFromDep). We only need compare + the major/minor/patch fields.
|
|
38
|
+
|
|
39
|
+
// Parse "1.17.2" / "v1.17.2" / "1.17.2-rc.1" → { major, minor, patch, pre } or null. `pre` is the
|
|
40
|
+
// prerelease tag ('' for a plain stable release), used only to EXCLUDE prereleases from auto-targets.
|
|
41
|
+
function parseSemver(v) {
|
|
42
|
+
const m = String(v == null ? '' : v).trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
return { major: +m[1], minor: +m[2], patch: +m[3], pre: m[4] || '' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isStable(v) {
|
|
48
|
+
const p = parseSemver(v);
|
|
49
|
+
return !!p && p.pre === '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Compare two version strings by (major, minor, patch). Returns -1 / 0 / 1. A stable release sorts
|
|
53
|
+
// ABOVE its own prereleases (1.17.2 > 1.17.2-rc.1), matching semver precedence — enough for "is this
|
|
54
|
+
// candidate newer than what's installed?" Unparseable inputs sort as -Infinity (never a target).
|
|
55
|
+
function compareSemver(a, b) {
|
|
56
|
+
const pa = parseSemver(a);
|
|
57
|
+
const pb = parseSemver(b);
|
|
58
|
+
if (!pa && !pb) return 0;
|
|
59
|
+
if (!pa) return -1;
|
|
60
|
+
if (!pb) return 1;
|
|
61
|
+
for (const k of ['major', 'minor', 'patch']) {
|
|
62
|
+
if (pa[k] !== pb[k]) return pa[k] < pb[k] ? -1 : 1;
|
|
63
|
+
}
|
|
64
|
+
// Same x.y.z: a prerelease is LOWER than the stable release. (Two prereleases: not ordered
|
|
65
|
+
// precisely here — we never auto-target a prerelease, so their relative order is immaterial.)
|
|
66
|
+
if (pa.pre && !pb.pre) return -1;
|
|
67
|
+
if (!pa.pre && pb.pre) return 1;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeChannel(channel) {
|
|
72
|
+
const c = String(channel == null ? '' : channel).trim().toLowerCase();
|
|
73
|
+
return VALID_CHANNELS.includes(c) ? c : DEFAULT_CHANNEL;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Would `channel` permit an unattended jump from `installed` to `candidate`? True only when candidate
|
|
77
|
+
// is a STABLE release strictly newer than installed AND within the channel's blast radius. Any invalid
|
|
78
|
+
// input, an unknown installed version, a prerelease candidate, or a major bump ⇒ false (fail-closed).
|
|
79
|
+
function channelAllows(installed, candidate, channel) {
|
|
80
|
+
const ch = normalizeChannel(channel);
|
|
81
|
+
if (ch === 'pinned') return false;
|
|
82
|
+
const from = parseSemver(installed);
|
|
83
|
+
const to = parseSemver(candidate);
|
|
84
|
+
if (!from || !to) return false;
|
|
85
|
+
if (to.pre) return false; // never auto-target a prerelease
|
|
86
|
+
if (compareSemver(candidate, installed) <= 0) return false; // must be strictly newer
|
|
87
|
+
if (to.major !== from.major) return false; // a major bump is never automatic
|
|
88
|
+
if (ch === 'patch' && to.minor !== from.minor) return false; // patch stays on the same minor
|
|
89
|
+
return true; // 'minor' allows any higher minor within the major
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The highest version in `available` that `channel` permits from `installed`, or null if none
|
|
93
|
+
// (nothing newer, pinned, only prereleases/majors ahead, or an unknown installed version). This is
|
|
94
|
+
// the one function the routine asks: "given where this instance is and what it subscribes to, what —
|
|
95
|
+
// if anything — should I upgrade it to right now?"
|
|
96
|
+
function resolveChannelTarget({ installed, channel, available } = {}) {
|
|
97
|
+
const list = Array.isArray(available) ? available : [];
|
|
98
|
+
let best = null;
|
|
99
|
+
for (const v of list) {
|
|
100
|
+
if (!channelAllows(installed, v, channel)) continue;
|
|
101
|
+
if (best === null || compareSemver(v, best) > 0) best = v;
|
|
102
|
+
}
|
|
103
|
+
return best;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- registry: what versions of the core are published? ---------------------
|
|
107
|
+
|
|
108
|
+
// List published versions of @bongos/core from the registry via `npm view … versions --json`
|
|
109
|
+
// (ADR 0134 — the private scoped package resolves through the instance's env-fed .npmrc, so run this
|
|
110
|
+
// with cwd inside an instance that has one + NPM_TOKEN in env). Fail-SOFT: any error returns
|
|
111
|
+
// { ok:false } so the routine skips that instance rather than crashing the whole sweep. `run` +
|
|
112
|
+
// `pkg` are injectable for tests.
|
|
113
|
+
function listAvailableVersions({ cwd = process.cwd(), pkg = CORE_PKG, run = spawnSync, env = process.env } = {}) {
|
|
114
|
+
let res;
|
|
115
|
+
try {
|
|
116
|
+
res = run('npm', ['view', pkg, 'versions', '--json'], { cwd, env, encoding: 'utf8' });
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return { ok: false, error: e && e.message ? e.message : String(e), versions: [] };
|
|
119
|
+
}
|
|
120
|
+
if (!res || res.status !== 0) {
|
|
121
|
+
const reason = (res && (res.stderr || res.error && res.error.message)) || `exit ${res && res.status}`;
|
|
122
|
+
return { ok: false, error: String(reason).trim().split('\n')[0] || 'npm view failed', versions: [] };
|
|
123
|
+
}
|
|
124
|
+
return parseNpmViewVersions(res.stdout);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Parse `npm view … versions --json` stdout. npm prints a JSON ARRAY for a multi-version package, a
|
|
128
|
+
// bare JSON STRING for a single-version one, and nothing for an unpublished name. Normalizes all
|
|
129
|
+
// three to { ok:true, versions:[…] }. Exported + unit-tested (the shape has bitten callers before).
|
|
130
|
+
function parseNpmViewVersions(stdout) {
|
|
131
|
+
const s = String(stdout == null ? '' : stdout).trim();
|
|
132
|
+
if (!s) return { ok: true, versions: [] };
|
|
133
|
+
let parsed;
|
|
134
|
+
try { parsed = JSON.parse(s); } catch (e) { return { ok: false, error: `unparseable npm output: ${e.message}`, versions: [] }; }
|
|
135
|
+
if (Array.isArray(parsed)) return { ok: true, versions: parsed.map(String) };
|
|
136
|
+
if (typeof parsed === 'string') return { ok: true, versions: [parsed] };
|
|
137
|
+
return { ok: false, error: 'unexpected npm view shape', versions: [] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- subscription manifest --------------------------------------------------
|
|
141
|
+
|
|
142
|
+
// Read + normalize config/update-subscriptions.json — the operator-edited opt-in list of instances
|
|
143
|
+
// this control plane keeps up to date. Never throws: a missing/malformed file ⇒ an empty roster
|
|
144
|
+
// (subscribe nothing), because an unreadable manifest must not upgrade anything. Each entry is
|
|
145
|
+
// normalized to { slug, dir, service, healthUrl, channel } with the channel validated + defaulted.
|
|
146
|
+
function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SUBSCRIPTIONS_REL } = {}) {
|
|
147
|
+
const abs = path.isAbsolute(file) ? file : path.join(instanceDir, file);
|
|
148
|
+
let raw;
|
|
149
|
+
try { raw = fsImpl.readFileSync(abs, 'utf8'); } catch (_) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: false }; }
|
|
150
|
+
let json;
|
|
151
|
+
try { json = JSON.parse(raw); } catch (e) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: true, error: `malformed JSON: ${e.message}` }; }
|
|
152
|
+
const defaultChannel = normalizeChannel(json.defaultChannel);
|
|
153
|
+
const rows = Array.isArray(json.instances) ? json.instances : [];
|
|
154
|
+
const instances = rows
|
|
155
|
+
.filter((r) => r && typeof r === 'object' && r.dir) // an entry without a dir can't be upgraded — drop it
|
|
156
|
+
.map((r) => ({
|
|
157
|
+
slug: String(r.slug || r.dir),
|
|
158
|
+
dir: String(r.dir),
|
|
159
|
+
service: r.service ? String(r.service) : null,
|
|
160
|
+
healthUrl: r.healthUrl ? String(r.healthUrl) : null,
|
|
161
|
+
channel: normalizeChannel(r.channel == null ? defaultChannel : r.channel),
|
|
162
|
+
// Per-instance env merged into the `bongos upgrade` child (e.g. PGDATABASE so the migrate step
|
|
163
|
+
// hits THIS instance's DB, not the control plane's — see the live-op learnings for demo). Only
|
|
164
|
+
// string values are kept; anything else is ignored.
|
|
165
|
+
env: (r.env && typeof r.env === 'object' && !Array.isArray(r.env))
|
|
166
|
+
? Object.fromEntries(Object.entries(r.env).filter(([, v]) => typeof v === 'string'))
|
|
167
|
+
: {},
|
|
168
|
+
}));
|
|
169
|
+
return { defaultChannel, instances, source: abs, present: true };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
CORE_PKG,
|
|
174
|
+
VALID_CHANNELS,
|
|
175
|
+
DEFAULT_CHANNEL,
|
|
176
|
+
SUBSCRIPTIONS_REL,
|
|
177
|
+
parseSemver,
|
|
178
|
+
isStable,
|
|
179
|
+
compareSemver,
|
|
180
|
+
normalizeChannel,
|
|
181
|
+
channelAllows,
|
|
182
|
+
resolveChannelTarget,
|
|
183
|
+
listAvailableVersions,
|
|
184
|
+
parseNpmViewVersions,
|
|
185
|
+
loadSubscriptions,
|
|
186
|
+
};
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -100,6 +100,21 @@ function writeRegistryPin(instanceDir, version, fsImpl = fs) {
|
|
|
100
100
|
return prev;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// Restore the consumer package.json's @bongos/core pin to an EXACT previous dep string,
|
|
104
|
+
// VERBATIM — whether that was a `file:` tarball path (vendored) or a bare registry semver.
|
|
105
|
+
// The inverse of writePin/writeRegistryPin used by the auto-rollback path (task 2149), which
|
|
106
|
+
// snapshots the returned prev-dep before the bump. Returns the dep string it overwrote.
|
|
107
|
+
function restorePin(instanceDir, depString, fsImpl = fs) {
|
|
108
|
+
const pkgPath = path.join(instanceDir, 'package.json');
|
|
109
|
+
const pkg = readJson(pkgPath, fsImpl);
|
|
110
|
+
if (!pkg) throw new Error(`no readable package.json at ${pkgPath}`);
|
|
111
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
112
|
+
const prev = pkg.dependencies[CORE_PKG];
|
|
113
|
+
pkg.dependencies[CORE_PKG] = depString;
|
|
114
|
+
fsImpl.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
115
|
+
return prev;
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
// ---- steps (each injectable for tests) --------------------------------------
|
|
104
119
|
|
|
105
120
|
function gitTreeClean(instanceDir, run = spawnSync) {
|
|
@@ -154,10 +169,11 @@ function runMigrate(instanceDir, run = spawnSync) {
|
|
|
154
169
|
|
|
155
170
|
// Regenerate the per-instance API artifacts (OpenAPI spec + api-reference + typed
|
|
156
171
|
// client) from the NEW core's routes — the claude-materialize sibling (task 2051).
|
|
157
|
-
// WHY: the routes + generators ship WITH core,
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
172
|
+
// WHY: the routes + generators ship WITH core, and the generated spec/docs/client also
|
|
173
|
+
// ship as CORE STARTERS (task 2078 — a fresh hall/docs must render before first regen),
|
|
174
|
+
// but they are per-instance GENERATED, so a bump changes the served API while the
|
|
175
|
+
// instance's committed spec/client lag until regenerated (the classification settled in
|
|
176
|
+
// task 2141; init does the same at standup). We SPAWN the new core's generators (a fresh child
|
|
161
177
|
// captures its output root from the env below) with <FALLBACK_PREFIX>_INSTANCE_ROOT set
|
|
162
178
|
// so resolveDocsRoot() writes INTO the instance repo, not node_modules. docs first — the
|
|
163
179
|
// client reads the spec it produces. Best-effort: a hiccup warns, never fails the bump
|
|
@@ -279,6 +295,64 @@ async function pollHealth(healthUrl, deps = {}) {
|
|
|
279
295
|
}
|
|
280
296
|
}
|
|
281
297
|
|
|
298
|
+
// Auto-rollback (task 2149): a bump failed AFTER the pin was moved — an install or migrate
|
|
299
|
+
// error, or a failed post-restart health check. Revert the code pin to the pre-upgrade
|
|
300
|
+
// snapshot, reinstall the previous core, restart, re-verify health, and record the reversal
|
|
301
|
+
// in the ledger. The DB is deliberately NOT rolled back: core migrations are additive +
|
|
302
|
+
// forward-safe (the core migration rule), so any already-applied migration is left in place —
|
|
303
|
+
// only the code PIN moves back. Best-effort + fail-soft: every sub-step logs but never throws,
|
|
304
|
+
// so a rollback attempt can't itself crash the upgrader on top of the original failure.
|
|
305
|
+
async function rollback({ instanceDir, snapshot, fromVersion, toVersion, opts }, reason, deps = {}) {
|
|
306
|
+
const log = deps.log || console.log;
|
|
307
|
+
const err = deps.err || console.error;
|
|
308
|
+
const run = deps.run || spawnSync;
|
|
309
|
+
const fsImpl = deps.fs || fs;
|
|
310
|
+
const restore = deps.restorePin || restorePin;
|
|
311
|
+
|
|
312
|
+
err(` ↩ rolling back: ${reason} — reverting ${CORE_PKG} ${toVersion} → ${snapshot.prevVersion || snapshot.prevPin}`);
|
|
313
|
+
const result = { rolledBack: true, reason, restoredPin: snapshot.prevPin, restoredVersion: snapshot.prevVersion };
|
|
314
|
+
|
|
315
|
+
// 1. revert the pin to the exact previous dep string.
|
|
316
|
+
try { restore(instanceDir, snapshot.prevPin, fsImpl); result.restoreOk = true; log(` ✓ restored pin → ${snapshot.prevPin}`); }
|
|
317
|
+
catch (e) { result.restoreOk = false; err(` ! could not restore pin (${e.message}) — package.json may need manual repair`); }
|
|
318
|
+
|
|
319
|
+
// 2. reinstall the previous core.
|
|
320
|
+
const inst = npmInstall(instanceDir, run);
|
|
321
|
+
result.reinstallOk = !!inst.ok;
|
|
322
|
+
if (inst.ok) log(' ✓ reinstalled previous core');
|
|
323
|
+
else err(` ! reinstall of previous core failed (${inst.reason || 'exit ' + inst.code}) — the instance may need manual repair`);
|
|
324
|
+
|
|
325
|
+
// 3. restart back onto the previous core.
|
|
326
|
+
const rs = restartService({ service: opts.service, restartCmd: opts.restartCmd }, run);
|
|
327
|
+
if (rs.skipped) log(' • restart: no --service/--restart-cmd — restart the instance service manually to load the reverted core.');
|
|
328
|
+
else if (!rs.ok) err(` ! restart after rollback failed (exit ${rs.code}) — restart the service manually.`);
|
|
329
|
+
else log(` ✓ restarted ${opts.service || 'service'} on the previous core`);
|
|
330
|
+
result.restartOk = rs.ok;
|
|
331
|
+
|
|
332
|
+
// 4. re-verify: installed == previous, optional health recovery.
|
|
333
|
+
const installedNow = readInstalledCoreVersion(instanceDir, fsImpl);
|
|
334
|
+
result.installedAfter = installedNow;
|
|
335
|
+
log(` ${installedNow === snapshot.prevVersion ? '✓' : '!'} installed core after rollback: ${installedNow || '(unknown)'}`);
|
|
336
|
+
if (opts.healthUrl) {
|
|
337
|
+
const h = await pollHealth(opts.healthUrl, deps);
|
|
338
|
+
result.healthOk = h.ok;
|
|
339
|
+
if (h.ok) log(` ✓ health recovered ${opts.healthUrl} → ${h.status}${h.attempts > 1 ? ` (after ${h.attempts} tries)` : ''}`);
|
|
340
|
+
else err(` ! health STILL failing after rollback (${h.error || 'status ' + h.status}) — manual intervention needed`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 5. record the reversal in the ledger via the always-present `note` column (no schema
|
|
344
|
+
// dependency — so it records even when a FAILED migrate means a newer column wouldn't exist).
|
|
345
|
+
const note = `rolled_back: ${reason} (attempted ${fromVersion || '(none)'} → ${toVersion}, restored ${snapshot.prevVersion || 'previous'})`;
|
|
346
|
+
const ledger = await recordLedger({ fromVersion, toVersion, sourceCommit: opts.sourceCommit, note }, deps);
|
|
347
|
+
if (ledger.ok) log(' ✓ recorded rollback in core_upgrades ledger');
|
|
348
|
+
else if (ledger.skipped) log(` • rollback ledger skipped (${ledger.reason})`);
|
|
349
|
+
else err(` ! rollback ledger insert failed (${ledger.reason}) — record it manually`);
|
|
350
|
+
result.ledger = ledger.ok;
|
|
351
|
+
|
|
352
|
+
err(` ↩ rollback complete — instance restored to core ${snapshot.prevVersion || snapshot.prevPin}.`);
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
|
|
282
356
|
// ---- orchestration ----------------------------------------------------------
|
|
283
357
|
|
|
284
358
|
async function runUpgrade(opts, deps = {}) {
|
|
@@ -291,9 +365,16 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
291
365
|
const instanceDir = path.resolve(opts.instance || process.cwd());
|
|
292
366
|
const targetVersion = opts.to;
|
|
293
367
|
const dryRun = !!opts.dryRun;
|
|
368
|
+
// Auto-rollback is ON by default (task 2149); `--no-rollback-on-failure` opts out (e.g. an
|
|
369
|
+
// interactive run that wants to inspect a broken state). Programmatic callers pass false.
|
|
370
|
+
const rollbackOnFailure = opts.rollbackOnFailure !== false;
|
|
371
|
+
// Pre-upgrade snapshot { prevPin, prevVersion } — captured the instant the pin is moved, so a
|
|
372
|
+
// failure AFTER that point (install / migrate / health) can revert. null until then = nothing to undo.
|
|
373
|
+
let snapshot = null;
|
|
294
374
|
if (!targetVersion) return { ok: false, error: 'missing --to <version> (the target core version)' };
|
|
295
375
|
|
|
296
376
|
const fromVersion = readInstalledCoreVersion(instanceDir, fsImpl) || readPinnedCoreVersion(instanceDir, fsImpl);
|
|
377
|
+
const doRollback = (reason) => rollback({ instanceDir, snapshot, fromVersion, toVersion: targetVersion, opts }, reason, deps);
|
|
297
378
|
log(`bongos upgrade — ${CORE_PKG}: ${fromVersion || '(none)'} → ${targetVersion}`);
|
|
298
379
|
log(` instance: ${instanceDir}${dryRun ? ' [DRY RUN — no changes]' : ''}`);
|
|
299
380
|
if (fromVersion === targetVersion && !opts.force) {
|
|
@@ -317,7 +398,7 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
317
398
|
if (opts.from) return { ok: false, error: '--registry and --from are mutually exclusive (registry pull vs vendored tarball) — pick one core source' };
|
|
318
399
|
if (!hasNpmToken()) log(' ! no NPM_TOKEN in env — `npm install` will 404 on the PRIVATE @bongos/core until a read-only automation token is provided (ADR 0134).');
|
|
319
400
|
const prevPin = dryRun ? null : writeRegistryPin(instanceDir, targetVersion, fsImpl);
|
|
320
|
-
if (!dryRun) writeNpmrc(instanceDir, { fsImpl, overwrite: false, log });
|
|
401
|
+
if (!dryRun) { writeNpmrc(instanceDir, { fsImpl, overwrite: false, log }); snapshot = { prevPin, prevVersion: fromVersion }; }
|
|
321
402
|
log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${dryRun ? ' + write an env-fed .npmrc' : ''}${prevPin ? ` (was ${prevPin})` : ''}`);
|
|
322
403
|
} else {
|
|
323
404
|
let vendorRel = opts.pinPath || (opts.from ? null : path.join('vendor', `${ARTIFACT}-${targetVersion}.tgz`));
|
|
@@ -330,6 +411,7 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
330
411
|
return { ok: false, error: `no core tarball at ${vendorRel} — pass --from <path-to-.tgz> to vendor it (or --registry to pull from the npm registry)` };
|
|
331
412
|
}
|
|
332
413
|
const prevPin = dryRun ? null : writePin(instanceDir, vendorRel, fsImpl);
|
|
414
|
+
if (!dryRun) snapshot = { prevPin, prevVersion: fromVersion };
|
|
333
415
|
log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → file:${vendorRel}${prevPin ? ` (was ${prevPin})` : ''}`);
|
|
334
416
|
}
|
|
335
417
|
|
|
@@ -337,13 +419,27 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
337
419
|
|
|
338
420
|
// 3. npm install.
|
|
339
421
|
const inst = npmInstall(instanceDir, run);
|
|
340
|
-
if (!inst.ok)
|
|
422
|
+
if (!inst.ok) {
|
|
423
|
+
const error = `npm install failed (${inst.reason || 'exit ' + inst.code})`;
|
|
424
|
+
if (rollbackOnFailure && snapshot) {
|
|
425
|
+
const rb = await doRollback('npm install failed');
|
|
426
|
+
return { ok: false, error, fromVersion, toVersion: targetVersion, rolledBack: true, rollback: rb };
|
|
427
|
+
}
|
|
428
|
+
return { ok: false, error, fromVersion };
|
|
429
|
+
}
|
|
341
430
|
log(' ✓ npm install');
|
|
342
431
|
|
|
343
432
|
// 4. migrate (additive core_* migrations).
|
|
344
433
|
if (!opts.skipMigrate) {
|
|
345
434
|
const mig = runMigrate(instanceDir, run);
|
|
346
|
-
if (!mig.ok)
|
|
435
|
+
if (!mig.ok) {
|
|
436
|
+
const error = `migrate failed (${mig.reason || 'exit ' + mig.code})`;
|
|
437
|
+
if (rollbackOnFailure && snapshot) {
|
|
438
|
+
const rb = await doRollback('migrate failed');
|
|
439
|
+
return { ok: false, error, fromVersion, toVersion: targetVersion, rolledBack: true, rollback: rb };
|
|
440
|
+
}
|
|
441
|
+
return { ok: false, error, fromVersion };
|
|
442
|
+
}
|
|
347
443
|
log(' ✓ migrate');
|
|
348
444
|
} else log(' • migrate skipped (--skip-migrate)');
|
|
349
445
|
|
|
@@ -383,6 +479,14 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
383
479
|
else err(` ! health ${opts.healthUrl} not ready after ${h.attempts} tr${h.attempts === 1 ? 'y' : 'ies'}: ${h.error || 'status ' + h.status}`);
|
|
384
480
|
}
|
|
385
481
|
|
|
482
|
+
// A failed post-restart health check means the new core is serving broken — auto-revert (task
|
|
483
|
+
// 2149) instead of recording an "upgraded" row + leaving the instance down. The rollback writes
|
|
484
|
+
// its OWN rolled_back ledger row, so skip the normal insert below.
|
|
485
|
+
if (healthOk === false && rollbackOnFailure && snapshot) {
|
|
486
|
+
const rb = await doRollback('health check failed after restart');
|
|
487
|
+
return { ok: false, error: `health check failed after restart — rolled back to ${snapshot.prevVersion || 'the previous core'}`, fromVersion, toVersion: targetVersion, versionOk, healthOk: false, rolledBack: true, rollback: rb };
|
|
488
|
+
}
|
|
489
|
+
|
|
386
490
|
const ledger = await recordLedger({ fromVersion, toVersion: targetVersion, sourceCommit: opts.sourceCommit, note: opts.note }, deps);
|
|
387
491
|
if (ledger.ok) log(' ✓ recorded in core_upgrades ledger');
|
|
388
492
|
else if (ledger.skipped) log(` • ledger skipped (${ledger.reason})`);
|
|
@@ -410,6 +514,7 @@ function parseArgs(argv) {
|
|
|
410
514
|
skipMigrate: hasFlag('--skip-migrate', argv),
|
|
411
515
|
skipMaterialize: hasFlag('--skip-materialize', argv),
|
|
412
516
|
skipRegenDocs: hasFlag('--skip-regen-docs', argv),
|
|
517
|
+
rollbackOnFailure: !hasFlag('--no-rollback-on-failure', argv), // ADR 0100: auto-revert ON by default (task 2149)
|
|
413
518
|
};
|
|
414
519
|
}
|
|
415
520
|
|
|
@@ -432,10 +537,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
432
537
|
' --skip-migrate do not run `npm run migrate`',
|
|
433
538
|
' --skip-materialize do not refresh .claude/',
|
|
434
539
|
' --skip-regen-docs do not regenerate the OpenAPI spec + typed client from the new core',
|
|
540
|
+
' --no-rollback-on-failure do NOT auto-revert on a failed install/migrate/health check (default: auto-rollback ON)',
|
|
435
541
|
' --dry-run show the plan; change nothing',
|
|
436
542
|
' --force skip the clean-tree + same-version guards',
|
|
437
543
|
'',
|
|
438
544
|
'Full bump: pre-flight → bump pin → npm install → migrate → materialize .claude → regen API docs+client → restart → verify.',
|
|
545
|
+
'On a failure after the pin moves (install/migrate/health), auto-rollback reverts the pin, reinstalls the previous core, restarts, and records a rolled_back ledger row (the DB is not rolled back — core migrations are additive).',
|
|
439
546
|
].join('\n') + '\n');
|
|
440
547
|
return 0;
|
|
441
548
|
}
|
|
@@ -450,8 +557,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
450
557
|
}
|
|
451
558
|
|
|
452
559
|
module.exports = {
|
|
453
|
-
runUpgrade, parseArgs, main,
|
|
454
|
-
versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin,
|
|
560
|
+
runUpgrade, parseArgs, main, rollback,
|
|
561
|
+
versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
|
|
455
562
|
vendorTarball, gitTreeClean, preflightModules, npmInstall, runMigrate, regenerateApiArtifacts, restartService, recordLedger, pollHealth,
|
|
456
563
|
CORE_PKG, ARTIFACT,
|
|
457
564
|
};
|