@bongos/core 1.17.0 → 1.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.bongos-core.json +363 -312
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/CLAUDE.md +11 -238
  15. package/GOVERNANCE.md +1 -1
  16. package/README.md +1 -1
  17. package/bin/bongos.js +1 -0
  18. package/clients/bongos-client/README.md +4 -4
  19. package/clients/bongos-client/bongos-client.global.js +14 -1
  20. package/clients/bongos-client/index.cjs +14 -1
  21. package/clients/bongos-client/index.d.ts +19 -2
  22. package/clients/bongos-client/index.mjs +11 -1
  23. package/clients/bongos-client/package.json +1 -1
  24. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  25. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  26. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  27. package/docs/adr/0020-security-model-public-stub.md +1 -1
  28. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  29. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  30. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  31. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  32. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  33. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  34. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  35. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  36. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  37. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  38. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  39. package/docs/adr/0057-container-cost-ledger.md +3 -3
  40. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  41. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  42. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  43. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  44. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  45. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  46. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  47. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  48. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  49. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  50. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  51. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
  52. package/docs/adr/0134-private-first-npm-distribution.md +20 -6
  53. package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
  54. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  55. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  56. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  57. package/docs/adr/README.md +14 -10
  58. package/docs/api/openapi.json +315 -6
  59. package/docs/api-reference.md +10 -5
  60. package/docs/architecture.md +3 -3
  61. package/docs/branding-contract.md +5 -4
  62. package/docs/canonical-permissions.md +1 -1
  63. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  64. package/docs/file-map.md +20 -19
  65. package/docs/handoff-template.md +2 -2
  66. package/docs/modules-contract.md +8 -1
  67. package/docs/onboarding/diagrams/README.md +7 -7
  68. package/docs/onboarding/drachmae-and-karma.md +18 -18
  69. package/docs/onboarding/primer.md +9 -9
  70. package/docs/recipes/audit-replay.md +2 -2
  71. package/docs/recipes/builder-box-lifecycle.md +2 -2
  72. package/docs/recipes/claude-design-loop.md +1 -1
  73. package/docs/recipes/instance-deploy.md +4 -4
  74. package/docs/recipes/instance-first-run.md +51 -0
  75. package/docs/recipes/ops-gotchas.md +1 -1
  76. package/docs/recipes/packaging-the-core.md +5 -5
  77. package/docs/recipes/private-npm-distribution.md +39 -12
  78. package/docs/recipes/self-host.md +1 -1
  79. package/docs/recipes/staging-environment.md +1 -1
  80. package/docs/recipes/upgrading-the-core.md +63 -8
  81. package/docs/research/cursor-competitive-analysis.md +1 -1
  82. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  83. package/migrations/core_191_module_submissions.sql +45 -0
  84. package/modules/autonomy/CLAUDE.md +11 -45
  85. package/modules/builder-settings/CLAUDE.md +11 -54
  86. package/modules/dev-box/CLAUDE.md +11 -34
  87. package/modules/discord/CLAUDE.md +11 -46
  88. package/modules/discord/module.json +2 -1
  89. package/modules/economy/CLAUDE.md +11 -44
  90. package/modules/economy/achievements.js +9 -1
  91. package/modules/economy/credits.js +10 -1
  92. package/modules/grading/CLAUDE.md +11 -38
  93. package/modules/hall-ui/CLAUDE.md +11 -57
  94. package/modules/hall-ui/public/builders.js +71 -4
  95. package/modules/hall-ui/public/goal-inbox.js +191 -0
  96. package/modules/hall-ui/public/goals-page.js +227 -22
  97. package/modules/hall-ui/public/goals.html +16 -0
  98. package/modules/hall-ui/public/hall-widgets.js +14 -1
  99. package/modules/hall-ui/public/index.html +3 -0
  100. package/modules/hall-ui/public/new-project-wizard.js +6 -4
  101. package/modules/hall-ui/public/style.css +43 -0
  102. package/modules/ideas/CLAUDE.md +11 -33
  103. package/modules/lifecycle/CLAUDE.md +11 -59
  104. package/modules/lifecycle/routes/goals.js +80 -0
  105. package/modules/memory/CLAUDE.md +11 -36
  106. package/modules/onboarding/CLAUDE.md +11 -51
  107. package/modules/provisioning/CLAUDE.md +15 -0
  108. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  109. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  110. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  111. package/modules/provisioning/module.json +17 -0
  112. package/modules/provisioning/onboard-plan.js +37 -0
  113. package/modules/provisioning/provisioning.js +539 -0
  114. package/modules/provisioning/rate-limit.js +35 -0
  115. package/modules/provisioning/routes/provisioning.js +607 -0
  116. package/modules/provisioning/tests/provisioning.mjs +232 -0
  117. package/modules/provisioning/ui/.gitkeep +0 -0
  118. package/modules/security/CLAUDE.md +11 -40
  119. package/modules/security/secret-scrub.js +10 -0
  120. package/modules/sessions/CLAUDE.md +11 -35
  121. package/modules/status-ui/CLAUDE.md +11 -43
  122. package/package-lock.json +2 -2
  123. package/package.json +7 -2
  124. package/public-landing/create.html +58 -12
  125. package/public-landing/index.html +40 -21
  126. package/scripts/discord/README.md +1 -1
  127. package/scripts/gds/CLAUDE.md +11 -140
  128. package/scripts/gds/claim.js +98 -48
  129. package/scripts/gds/claude-materialize.js +174 -20
  130. package/scripts/gds/claude-md-template.js +84 -0
  131. package/scripts/gds/cli-lib.js +112 -2
  132. package/scripts/gds/cutover-codemod.js +241 -0
  133. package/scripts/gds/fitness.js +54 -0
  134. package/scripts/gds/gen-api-client.js +10 -10
  135. package/scripts/gds/init.js +112 -16
  136. package/scripts/gds/lint-infra-exec.js +70 -0
  137. package/scripts/gds/lint-workflow-exec.js +63 -0
  138. package/scripts/gds/migration-namespace.js +1 -1
  139. package/scripts/gds/mirror-lineage-check.js +14 -5
  140. package/scripts/gds/mirror-redact.js +8 -0
  141. package/scripts/gds/module.js +118 -4
  142. package/scripts/gds/npmrc.js +93 -0
  143. package/scripts/gds/onboard.js +17 -24
  144. package/scripts/gds/package-core.js +74 -17
  145. package/scripts/gds/provision.js +102 -17
  146. package/scripts/gds/publish-manifest.js +60 -2
  147. package/scripts/gds/run-routine.js +51 -13
  148. package/scripts/gds/run-unit-tests.js +4 -0
  149. package/scripts/gds/seed-adopt-tasks.js +1 -1
  150. package/scripts/gds/status.js +6 -1
  151. package/scripts/gds/task.js +166 -0
  152. package/scripts/gds/update-channel.js +186 -0
  153. package/scripts/gds/upgrade.js +172 -25
  154. package/src/bongos/CLAUDE.md +11 -60
  155. package/src/bongos/auth.js +24 -0
  156. package/src/bongos/module-submissions.js +56 -0
  157. package/src/bongos/route-rank-check.js +6 -0
  158. package/src/bongos/routes/CLAUDE.md +11 -39
  159. package/src/bongos/routes/auth.js +6 -1
  160. package/src/bongos/routes/modules.js +123 -21
  161. package/src/bongos/serve-internal.js +10 -1
  162. package/src/branding.js +1 -0
  163. package/src/instance-config.js +1 -1
  164. package/src/module-api.js +24 -1
  165. package/src/module-loader/loader.js +36 -4
  166. package/src/module-loader/manifest-schema.js +16 -1
  167. package/tests/branding.mjs +11 -0
  168. package/tests/claim_error_surface.mjs +107 -0
  169. package/tests/claude_materialize.mjs +125 -0
  170. package/tests/consumer_layout_boot.mjs +302 -0
  171. package/tests/cutover_codemod.mjs +184 -0
  172. package/tests/device_flow_start.mjs +96 -0
  173. package/tests/first_run_engine_check.mjs +79 -0
  174. package/tests/goal_membership_requests.mjs +33 -3
  175. package/tests/hook_core_resolve.mjs +125 -0
  176. package/tests/hook_user_agents.mjs +62 -0
  177. package/tests/infra_exec_paths.mjs +104 -0
  178. package/tests/init.mjs +106 -6
  179. package/tests/instance_mismatch_guard.mjs +101 -0
  180. package/tests/mirror_lineage_proof.mjs +14 -0
  181. package/tests/module_cli.mjs +130 -2
  182. package/tests/module_loader.mjs +42 -0
  183. package/tests/module_manifest.mjs +19 -0
  184. package/tests/module_submissions.mjs +135 -0
  185. package/tests/npmrc.mjs +105 -0
  186. package/tests/onboard.mjs +62 -0
  187. package/tests/package_core.mjs +76 -22
  188. package/tests/platform_boot.mjs +5 -2
  189. package/tests/provision.mjs +107 -8
  190. package/tests/provisioning_github_app_routes.mjs +44 -2
  191. package/tests/publish_manifest.mjs +52 -1
  192. package/tests/run_routine.mjs +26 -1
  193. package/tests/run_routine_two_root.mjs +77 -0
  194. package/tests/scrubber_corpus.mjs +17 -0
  195. package/tests/served_paths_shipped.mjs +2 -2
  196. package/tests/task_cli.mjs +99 -0
  197. package/tests/update_channel.mjs +186 -0
  198. package/tests/upgrade.mjs +220 -25
  199. package/tests/workflow_exec_paths.mjs +98 -0
  200. package/docs/design/container-exterior-tiles.md +0 -49
  201. package/docs/design/container-exterior.md +0 -81
  202. package/docs/design/container-interior.md +0 -77
  203. package/docs/design/world-map-scroll.md +0 -41
  204. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  205. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  206. package/docs/recipes/cloudbongos-standup.md +0 -111
  207. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  208. package/docs/recipes/parallel-load-test.md +0 -32
  209. package/docs/recipes/pixel-art-pipeline.md +0 -411
  210. package/docs/recipes/staging-data.md +0 -50
  211. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  212. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  213. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  214. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  215. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  216. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  217. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  218. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  219. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  220. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  221. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  222. package/scripts/gds/art-stage.js +0 -152
  223. package/server.js +0 -212
  224. package/tests/art_key.mjs +0 -121
  225. package/tests/day_cycle.mjs +0 -197
  226. package/tests/snapshot_picker.mjs +0 -233
  227. package/tests/sprite_facing.mjs +0 -35
  228. package/tests/terrain_picker.mjs +0 -298
  229. package/tests/turn_in_place.mjs +0 -280
  230. package/tests/walkability.mjs +0 -188
package/tests/onboard.mjs CHANGED
@@ -37,12 +37,30 @@ test('onboardPlan(adopt): same sequence + human steps, but the scaffold-leg labe
37
37
  assert.doesNotMatch(label(green, 'init'), /--adopt/);
38
38
  });
39
39
 
40
+ test('onboardPlan lists seed-first-version as an explicit AUTO step right after provision (task 2119 anti-drift)', () => {
41
+ // The first-version seed used to be an unlisted afterthought bolted to the local
42
+ // `bongos init` leg (run --no-seed in every hosted flow, so it never fired). Making
43
+ // it a named step in the ONE canonical sequence is what stops it silently drifting.
44
+ const plan = ob.onboardPlan();
45
+ const keys = plan.map((s) => s.key);
46
+ const seed = plan.find((s) => s.key === 'seed-version');
47
+ assert.ok(seed, 'the plan carries a seed-version step');
48
+ assert.equal(seed.kind, 'auto', 'seeding is automated (the runner does it, no owner action)');
49
+ assert.equal(keys.indexOf('seed-version'), keys.indexOf('provision') + 1, 'seed immediately follows provision (the DB now exists)');
50
+ assert.ok(keys.indexOf('seed-version') < keys.indexOf('first-sign-in'), 'a version exists before the owner ever signs in');
51
+ // present in BOTH modes (not a greenfield/adopt branch).
52
+ assert.ok(ob.onboardPlan('adopt').some((s) => s.key === 'seed-version'), 'adopt gets it too');
53
+ });
54
+
40
55
  test('oauthAppInstructions pins the apex callback + homepage and points at ACCOUNT Developer settings', () => {
41
56
  const oa = ob.oauthAppInstructions('mercury.example.com');
42
57
  assert.equal(oa.homepageUrl, 'https://mercury.example.com');
43
58
  assert.equal(oa.callbackUrl, 'https://mercury.example.com/api/gds/auth/web/callback');
44
59
  assert.match(oa.location, /ACCOUNT/);
45
60
  assert.match(oa.location, /Developer settings/);
61
+ // task 2127 — must tell the owner to enable Device Flow (off by default; the CLI login
62
+ // uses it). Its absence was the 2026-07-08 demo login-500 root cause.
63
+ assert.match(oa.enableDeviceFlow, /Device Flow/i);
46
64
  });
47
65
 
48
66
  test('buildInitSpec maps answers to a STANDALONE init spec (domain → origin + provisioning shape)', () => {
@@ -68,3 +86,47 @@ test('buildInitSpec without a domain still defaults a standalone provisioning sh
68
86
  test('healthzUrl', () => {
69
87
  assert.equal(ob.healthzUrl('mercury.example.com'), 'https://mercury.example.com/healthz');
70
88
  });
89
+
90
+ // ── task 2085: ONE source for the onboarding sequence (anti-drift) ────────────
91
+
92
+ test('onboardPlan is the SAME function the provisioning module owns (single source, not a copy)', () => {
93
+ // The CLI must CONSUME modules/provisioning/onboard-plan.js, not carry its own copy —
94
+ // that is the whole point of task 2085. A re-declared duplicate would pass every other
95
+ // test yet silently drift; identity of the function reference proves there is one source.
96
+ const canonical = require('../modules/provisioning/onboard-plan.js').onboardPlan;
97
+ assert.equal(ob.onboardPlan, canonical, 'scripts/gds/onboard.js must re-export the module’s onboardPlan');
98
+ });
99
+
100
+ test('the web forms (/create + hall wizard) declare EXACTLY the canonical human steps — drift guard', () => {
101
+ // goal 35 / criterion 165: "the canonical step sequence is the source of truth;
102
+ // skill/CLI/hall are forms over it." Each web form tags its human-step <li>s with
103
+ // data-onboard-step="<key>"; this asserts that set equals the plan's human-step keys,
104
+ // so adding/removing/renaming a human step in the single source fails CI until every
105
+ // form is updated (and vice-versa) — the sequence can no longer drift between forms.
106
+ const { readFileSync } = require('node:fs');
107
+ const path = require('node:path');
108
+ const humanKeys = ob.onboardPlan().filter((s) => s.kind === 'human').map((s) => s.key).sort();
109
+ const surfaces = {
110
+ '/create landing page': 'public-landing/create.html',
111
+ 'hall new-project wizard': 'modules/hall-ui/public/new-project-wizard.js',
112
+ };
113
+ for (const [name, rel] of Object.entries(surfaces)) {
114
+ const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
115
+ const found = [...src.matchAll(/data-onboard-step="([^"]+)"/g)].map((m) => m[1]);
116
+ const uniq = [...new Set(found)].sort();
117
+ assert.deepEqual(uniq, humanKeys, `${name} (${rel}) must tag exactly the canonical human steps [${humanKeys}], found [${uniq}]`);
118
+ }
119
+ });
120
+
121
+ test('the one-click GitHub-App manifest path surfaces the CLI-only Enable-Device-Flow step (task 2129)', () => {
122
+ // GitHub defaults Device Flow OFF and the manifest can't enable it, but `bongos login` needs
123
+ // it — so BOTH manifest surfaces must tell the owner to enable it after the one-click approval.
124
+ // Assert on a phrase DISTINCT to the one-click caveat: the manual OAuth path already mentions
125
+ // Device Flow (task 2127), so a bare "Device Flow" match wouldn't prove the manifest path carries it.
126
+ const { readFileSync } = require('node:fs');
127
+ const path = require('node:path');
128
+ const onboardSrc = readFileSync(path.join(import.meta.dirname, '..', 'scripts/gds/onboard.js'), 'utf8');
129
+ const wizardSrc = readFileSync(path.join(import.meta.dirname, '..', 'modules/hall-ui/public/new-project-wizard.js'), 'utf8');
130
+ assert.match(onboardSrc, /cannot enable Device Flow via the manifest/i, 'bongos onboard one-click caveat missing');
131
+ assert.match(wizardSrc, /Terminal CLI only: after approving, enable/i, 'hall wizard one-click caveat missing');
132
+ });
@@ -25,6 +25,8 @@ const {
25
25
  ARTIFACT_NAME, MANIFEST_SCHEMA, TAR_ROOT,
26
26
  // task 1824 — the consumer-core file-classification layer
27
27
  isDocPath, functionalSecretLeaks, processCoreFile,
28
+ // task 2121 — agent-doc (CLAUDE.md/AGENTS.md) stub
29
+ isAgentDoc, CORE_AGENT_DOC_STUB,
28
30
  // task 1882 (ADR 0108 §2) — the real installable npm package
29
31
  CORE_DEP_NAMES, buildPackageJson, resolveLockDep, lockfileClosure, prunePackageLock,
30
32
  // task 2089 (ADR 0134) — private-by-default npm publish
@@ -141,27 +143,27 @@ t('withTarballInfo adds the exact-bytes coordinate for the side-car pin', () =>
141
143
  version: '1.13.0', coreContract: '1.13.0', sourceCommit: 'abc', sourceRef: 'HEAD',
142
144
  builtAtIso: '2026-07-01T00:00:00.000Z', files: [], treeHash: 'x', redaction: null,
143
145
  });
144
- const side = withTarballInfo(inner, { name: 'cloudbongos-core-1.13.0.tgz', sha256: 'cafe', bytes: 42 });
146
+ const side = withTarballInfo(inner, { name: 'bongos-core-1.13.0.tgz', sha256: 'cafe', bytes: 42 });
145
147
  assert.equal(side.tarball.sha256, 'cafe');
146
148
  assert.equal(side.tarball.bytes, 42);
147
149
  assert.equal(inner.tarball, undefined); // withTarballInfo does not mutate the inner
148
150
  });
149
151
 
150
152
  t('artifact filenames', () => {
151
- assert.equal(tarballName('1.13.0'), 'cloudbongos-core-1.13.0.tgz');
152
- assert.equal(manifestName('1.13.0'), 'cloudbongos-core-1.13.0.manifest.json');
153
- assert.equal(stageDirName('1.13.0'), 'cloudbongos-core-1.13.0');
153
+ assert.equal(tarballName('1.13.0'), 'bongos-core-1.13.0.tgz');
154
+ assert.equal(manifestName('1.13.0'), 'bongos-core-1.13.0.manifest.json');
155
+ assert.equal(stageDirName('1.13.0'), 'bongos-core-1.13.0');
154
156
  });
155
157
 
156
158
  // ── ustar name/prefix split ──────────────────────────────────────────────────────────
157
159
  t('splitUstarName: short path → name only', () => {
158
- const r = splitUstarName('cloudbongos-core/a.js');
160
+ const r = splitUstarName('bongos-core/a.js');
159
161
  assert.equal(r.prefix, '');
160
- assert.equal(r.name, 'cloudbongos-core/a.js');
162
+ assert.equal(r.name, 'bongos-core/a.js');
161
163
  });
162
164
 
163
165
  t('splitUstarName: long path splits at a / into name<=100 + prefix<=155', () => {
164
- const long = 'cloudbongos-core/' + 'dir/'.repeat(20) + 'file.js'; // > 100 bytes
166
+ const long = 'bongos-core/' + 'dir/'.repeat(24) + 'file.js'; // > 100 bytes → forces a name/prefix split
165
167
  const r = splitUstarName(long);
166
168
  assert.ok(r, 'expected a split');
167
169
  assert.ok(Buffer.byteLength(r.name) <= 100);
@@ -177,18 +179,18 @@ t('splitUstarName: unsplittable (>100-byte tail) → null (longlink fallback)',
177
179
  // ── buildTar — produces a real, readable, deterministic archive ──────────────────────
178
180
  t('buildTar round-trips a small set (content + mode preserved)', () => {
179
181
  const entries = [
180
- { name: 'cloudbongos-core/a.txt', mode: 0o644, buf: Buffer.from('hello\n') },
181
- { name: 'cloudbongos-core/bin/run.sh', mode: 0o755, buf: Buffer.from('#!/bin/sh\necho hi\n') },
182
+ { name: 'bongos-core/a.txt', mode: 0o644, buf: Buffer.from('hello\n') },
183
+ { name: 'bongos-core/bin/run.sh', mode: 0o755, buf: Buffer.from('#!/bin/sh\necho hi\n') },
182
184
  ];
183
185
  const files = readTar(buildTar(entries));
184
- assert.equal(files.get('cloudbongos-core/a.txt').content.toString(), 'hello\n');
185
- assert.equal(files.get('cloudbongos-core/bin/run.sh').content.toString(), '#!/bin/sh\necho hi\n');
186
- assert.equal(files.get('cloudbongos-core/bin/run.sh').mode & 0o777, 0o755);
187
- assert.equal(files.get('cloudbongos-core/a.txt').mode & 0o777, 0o644);
186
+ assert.equal(files.get('bongos-core/a.txt').content.toString(), 'hello\n');
187
+ assert.equal(files.get('bongos-core/bin/run.sh').content.toString(), '#!/bin/sh\necho hi\n');
188
+ assert.equal(files.get('bongos-core/bin/run.sh').mode & 0o777, 0o755);
189
+ assert.equal(files.get('bongos-core/a.txt').mode & 0o777, 0o644);
188
190
  });
189
191
 
190
192
  t('buildTar round-trips a >100-byte path via the GNU longlink path', () => {
191
- const longName = 'cloudbongos-core/' + 'deep/'.repeat(25) + 'leaf.js'; // ~150 bytes, but splittable
193
+ const longName = 'bongos-core/' + 'deep/'.repeat(25) + 'leaf.js'; // ~150 bytes, but splittable
192
194
  const unsplittable = 'z'.repeat(130); // forces the longlink branch (no valid split)
193
195
  const entries = [
194
196
  { name: longName, mode: 0o644, buf: Buffer.from('deep\n') },
@@ -201,15 +203,15 @@ t('buildTar round-trips a >100-byte path via the GNU longlink path', () => {
201
203
 
202
204
  t('buildTar is byte-deterministic (same input → identical bytes; entry order irrelevant)', () => {
203
205
  const e1 = [
204
- { name: 'cloudbongos-core/b.txt', mode: 0o644, buf: Buffer.from('B') },
205
- { name: 'cloudbongos-core/a.txt', mode: 0o644, buf: Buffer.from('A') },
206
+ { name: 'bongos-core/b.txt', mode: 0o644, buf: Buffer.from('B') },
207
+ { name: 'bongos-core/a.txt', mode: 0o644, buf: Buffer.from('A') },
206
208
  ];
207
209
  const e2 = [...e1].reverse();
208
210
  assert.equal(sha256hex(buildTar(e1)), sha256hex(buildTar(e2)));
209
211
  });
210
212
 
211
213
  t('buildTar output gunzips and ends with two zero blocks', () => {
212
- const gz = buildTar([{ name: 'cloudbongos-core/a', mode: 0o644, buf: Buffer.from('x') }]);
214
+ const gz = buildTar([{ name: 'bongos-core/a', mode: 0o644, buf: Buffer.from('x') }]);
213
215
  const tar = zlib.gunzipSync(gz);
214
216
  assert.equal(tar.length % 512, 0);
215
217
  const tail = tar.subarray(tar.length - 1024);
@@ -298,6 +300,50 @@ t('DOCS are genericized: instance brand/owner redacted, output is clean', () =>
298
300
  assert.equal(r.leaks.length, 0); // clean after redaction → no abort
299
301
  });
300
302
 
303
+ // ── task 2121 — CLAUDE.md/AGENTS.md ship as a brand-free stub (demo-audit fix) ──────
304
+ t('isAgentDoc: matches CLAUDE.md/AGENTS.md at root + any depth, nothing else', () => {
305
+ assert.equal(isAgentDoc('CLAUDE.md'), true);
306
+ assert.equal(isAgentDoc('scripts/gds/CLAUDE.md'), true);
307
+ assert.equal(isAgentDoc('src/bongos/routes/CLAUDE.md'), true);
308
+ assert.equal(isAgentDoc('modules/lifecycle/CLAUDE.md'), true);
309
+ assert.equal(isAgentDoc('AGENTS.md'), true);
310
+ assert.equal(isAgentDoc('docs/CLAUDE.md'), true);
311
+ // NOT an agent doc: other markdown, or a filename that merely contains the word
312
+ assert.equal(isAgentDoc('README.md'), false);
313
+ assert.equal(isAgentDoc('docs/adr/0100.md'), false);
314
+ assert.equal(isAgentDoc('docs/CLAUDE-notes.md'), false);
315
+ assert.equal(isAgentDoc('src/claude.js'), false);
316
+ });
317
+
318
+ t('processCoreFile STUBS a CLAUDE.md — the core memory never ships (no auto-load pollution)', () => {
319
+ // A realistic core CLAUDE.md: the wrong project title + instance brand + a long body.
320
+ const coreClaude = '# Off the Boats — Project Memory\n' +
321
+ 'Visit https://acme.example (world Xanadu), owner zoltan. ' + 'lorem ipsum '.repeat(200);
322
+ const r = proc('CLAUDE.md', coreClaude);
323
+ assert.equal(r.kind, 'stub'); // classified as a stub, not a doc
324
+ assert.equal(r.leaks.length, 0); // brand-free by construction → never a leak
325
+ const out = r.buf.toString();
326
+ assert.equal(out, CORE_AGENT_DOC_STUB); // exactly the stub, verbatim
327
+ assert.ok(!/Off the Boats/i.test(out), 'wrong project title gone');
328
+ assert.ok(!/acme/i.test(out) && !/zoltan/i.test(out) && !/Xanadu/i.test(out), 'no instance brand');
329
+ assert.ok(!/lorem ipsum/.test(out), 'original 200-line body gone');
330
+ assert.ok(out.length < coreClaude.length, 'stub is far shorter than the real memory');
331
+ });
332
+
333
+ t('processCoreFile STUBS nested + module CLAUDE.md too (all auto-load sites)', () => {
334
+ for (const p of ['scripts/gds/CLAUDE.md', 'src/bongos/CLAUDE.md', 'modules/economy/CLAUDE.md']) {
335
+ const r = proc(p, '# nested map\nsecret https://acme.example detail');
336
+ assert.equal(r.kind, 'stub', `${p} stubbed`);
337
+ assert.equal(r.buf.toString(), CORE_AGENT_DOC_STUB, `${p} → stub body`);
338
+ }
339
+ });
340
+
341
+ t('the stub itself carries no instance brand and states it does not govern', () => {
342
+ assert.ok(!/acme|zoltan|Xanadu|Off the Boats/i.test(CORE_AGENT_DOC_STUB));
343
+ assert.ok(/does not govern/i.test(CORE_AGENT_DOC_STUB));
344
+ assert.ok(/@bongos\/core/.test(CORE_AGENT_DOC_STUB));
345
+ });
346
+
301
347
  // ── task 1882 (ADR 0108 §2) — the real installable npm package ─────────────────────
302
348
  t('TAR_ROOT is the npm-required "package/" (not the ARTIFACT_NAME dir)', () => {
303
349
  assert.equal(TAR_ROOT, 'package/');
@@ -309,7 +355,7 @@ t('buildPackageJson: prunes to express+pg, keeps the source ranges + the undici
309
355
  sourceDeps: { express: '^4.21.2', pg: '^8.20.0', phaser: '^3.87.0', 'discord.js': '^14.26.4' },
310
356
  sourceOverrides: { undici: '^6.27.0' },
311
357
  });
312
- assert.equal(pkg.name, '@cloudbongos/core');
358
+ assert.equal(pkg.name, '@bongos/core');
313
359
  assert.equal(pkg.version, '1.14.0');
314
360
  assert.deepEqual(pkg.dependencies, { express: '^4.21.2', pg: '^8.20.0' });
315
361
  assert.equal(pkg.dependencies.phaser, undefined);
@@ -320,12 +366,20 @@ t('buildPackageJson: prunes to express+pg, keeps the source ranges + the undici
320
366
  assert.equal(pkg.exports['.'], './src/platform-server.js');
321
367
  assert.equal(pkg.exports['./serve-internal'], './src/bongos/serve-internal.js');
322
368
  assert.equal(pkg.exports['./module-api'], './src/module-api.js');
369
+ // rehearsal F3 / task 2161 — the game-host kit: the core internals a game-bearing
370
+ // instance's own entrypoint (server.js) wires. The cutover codemod rewrites server.js's
371
+ // relative ./src/* requires to exactly these subpaths, so they MUST be exported.
372
+ assert.equal(pkg.exports['./bongos/api-path-404'], './src/bongos/api-path-404.js');
373
+ assert.equal(pkg.exports['./bongos/uptime-poller'], './src/bongos/uptime-poller.js');
374
+ assert.equal(pkg.exports['./module-loader/loader'], './src/module-loader/loader.js');
375
+ assert.equal(pkg.exports['./modules'], './src/modules.js');
376
+ assert.equal(pkg.exports['./module-seams'], './src/module-seams.js');
323
377
  // task 2089 / ADR 0134 — the synthesized package publishes PRIVATE by default.
324
378
  assert.deepEqual(pkg.publishConfig, { access: 'restricted' });
325
379
  });
326
380
 
327
381
  t('publishArgs: private (restricted) publish, dry-run unless live (task 2089 / ADR 0134)', () => {
328
- const tgz = 'dist/cloudbongos-core-1.14.0.tgz';
382
+ const tgz = 'dist/bongos-core-1.14.0.tgz';
329
383
  // Default: dry-run, restricted (never public, never a real upload).
330
384
  assert.deepEqual(publishArgs(tgz, false), ['publish', tgz, '--access', 'restricted', '--dry-run']);
331
385
  // Live: still restricted (private) — the permanent publish is never public here.
@@ -408,8 +462,8 @@ t('lockfileClosure: matches the real repo lockfile — express+pg close over the
408
462
  });
409
463
 
410
464
  t('prunePackageLock: emits a self-consistent pruned lockfile (root ranges + only the kept packages)', () => {
411
- const pruned = prunePackageLock(FIXTURE_LOCK, { name: '@cloudbongos/core', version: '1.14.0', rootRanges: { a: '^1.0.0' }, rootDepNames: ['a'] });
412
- assert.equal(pruned.name, '@cloudbongos/core');
465
+ const pruned = prunePackageLock(FIXTURE_LOCK, { name: '@bongos/core', version: '1.14.0', rootRanges: { a: '^1.0.0' }, rootDepNames: ['a'] });
466
+ assert.equal(pruned.name, '@bongos/core');
413
467
  assert.equal(pruned.version, '1.14.0');
414
468
  assert.equal(pruned.lockfileVersion, 3);
415
469
  assert.equal(pruned.requires, true);
@@ -423,7 +477,7 @@ t('prunePackageLock: emits a self-consistent pruned lockfile (root ranges + only
423
477
  t('prunePackageLock: on the real repo lockfile, drops the vast majority of packages', () => {
424
478
  const realLock = require('../package-lock.json');
425
479
  const pruned = prunePackageLock(realLock, {
426
- name: '@cloudbongos/core', version: '1.14.0', rootRanges: { express: '^4.21.2', pg: '^8.20.0' },
480
+ name: '@bongos/core', version: '1.14.0', rootRanges: { express: '^4.21.2', pg: '^8.20.0' },
427
481
  });
428
482
  const keptCount = Object.keys(pruned.packages).length - 1; // exclude the root '' entry
429
483
  const sourceCount = Object.keys(realLock.packages).length - 1;
@@ -107,8 +107,11 @@ test('the apex host serves the landing page — not the game — on a game-less
107
107
  // it IS the landing, with the branding pack injected (look + identity from config)
108
108
  assert.match(res.body, /window\.__BRANDING__=/);
109
109
  assert.match(res.body, /Enter the Hall/);
110
- // the platform-level create door (goal 35, task 2084) — gated by sign-in client-side
111
- assert.match(res.body, /Create a project/);
110
+ // the per-project landing carries the top-right identity chip + hall entry
111
+ // (goal 35, task 2094). "Create a project" is an ORG-level action that lives
112
+ // only on the gate now — it must NOT appear on a project's own landing.
113
+ assert.match(res.body, /id="who"/);
114
+ assert.doesNotMatch(res.body, /Create a project/);
112
115
  });
113
116
 
114
117
  test('the apex host serves the branded /create page (goal 35, task 2081)', async () => {
@@ -46,6 +46,33 @@ t('dbCreateCmd / migrateCmd use the slug db + GDS-only migrate', () => {
46
46
  assert.match(P.migrateCmd(coTenant), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 \.\/scripts\/migrate\.sh/);
47
47
  });
48
48
 
49
+ t('seedFirstVersionCmd: idempotent (only-if-empty) V1 seed against the slug db, track explicit, one criterion — the fix for the empty-versions bug (task 2119)', () => {
50
+ const cmd = P.seedFirstVersionCmd(coTenant);
51
+ // targets the instance's OWN db, matching createdb/migrate
52
+ assert.match(cmd, /psql .*-d mercury /, 'seeds the instance db');
53
+ assert.match(cmd, /ON_ERROR_STOP=1/, 'aborts on any error (atomic, no half-seed)');
54
+ // the version the greenfield init spec defines — ONE source of truth
55
+ const v = P.instanceInitSpec(coTenant).version;
56
+ assert.match(cmd, /INSERT INTO versions \(id, name, track, status, done_when\)/);
57
+ assert.match(cmd, new RegExp(`\\$v\\$${reEsc(v.id)}\\$v\\$`), 'seeds the spec version id (V1)');
58
+ assert.match(cmd, /\$v\$product\$v\$/, 'track is set EXPLICITLY (versions.track is NOT NULL, no default since mig 112)');
59
+ assert.match(cmd, /\$v\$building\$v\$/, 'status building, mirroring seedViaApi');
60
+ assert.ok(cmd.includes(v.done_when), 'carries the spec done_when verbatim (incl. the → arrows)');
61
+ // the IDEMPOTENT guard — only seeds when there is NO version yet, so it never disturbs an existing project
62
+ assert.match(cmd, /WHERE NOT EXISTS \(SELECT 1 FROM versions\)/, 'only-if-empty guard');
63
+ // one done-when criterion so /status has a finish line (matches seedViaApi's 'first-goal')
64
+ assert.match(cmd, /INSERT INTO done_when_criteria/);
65
+ assert.match(cmd, /\$v\$first-goal\$v\$/);
66
+ // dollar-quoted literals ⇒ the whole -c arg is single-quoted (safe inline / SSH / cloud-init)
67
+ assert.match(cmd, /-c '.*'/, 'the SQL is a single-quoted -c argument');
68
+ assert.doesNotMatch(cmd, /-c ".*"/, 'never double-quoted (would break cloud-init bash -lc "…")');
69
+ });
70
+
71
+ t('seedFirstVersionCmd: privileged runs psql as the postgres role; default is bare (peer auth) — mirrors createdb/migrate', () => {
72
+ assert.match(P.seedFirstVersionCmd(coTenant, { privileged: false }), /^psql /);
73
+ assert.match(P.seedFirstVersionCmd(coTenant, { privileged: true }), /^sudo -u postgres psql /);
74
+ });
75
+
49
76
  t('serviceUnit binds the instance port + DB + per-instance env file, runs platform-server', () => {
50
77
  const u = P.serviceUnit(coTenant, { etcBase: '/etc' });
51
78
  assert.match(u, /Environment=PORT=3003/);
@@ -60,7 +87,7 @@ t('serviceUnit(standalone) runs the CORE PACKAGE server from the instance\'s OWN
60
87
  const u = P.serviceUnit(inst, { etcBase: '/etc' });
61
88
  const root = P.standaloneRoot(inst);
62
89
  assert.match(u, new RegExp('WorkingDirectory=' + reEsc(root)), 'runs from the instance\'s own repo dir');
63
- assert.match(u, /ExecStart=\/usr\/bin\/node node_modules\/@cloudbongos\/core\/src\/platform-server\.js/, 'runs the pinned core package server');
90
+ assert.match(u, /ExecStart=\/usr\/bin\/node node_modules\/@bongos\/core\/src\/platform-server\.js/, 'runs the pinned core package server');
64
91
  assert.match(u, /Environment=PORT=3003/);
65
92
  assert.match(u, /EnvironmentFile=\/etc\/mercury\/web\.env/);
66
93
  });
@@ -70,7 +97,7 @@ t('standalone step generators: root under standaloneBase + pull / npm-ci / insta
70
97
  assert.match(P.standaloneRoot(inst), /\/mercury$/);
71
98
  assert.equal(P.standalonePullCmd(), 'git pull --ff-only origin main');
72
99
  assert.equal(P.standaloneInstallCmd(), 'npm ci --omit=dev');
73
- assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@cloudbongos\/core\/scripts\/migrate\.sh/);
100
+ assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@bongos\/core\/scripts\/migrate\.sh/);
74
101
  });
75
102
 
76
103
  t('webEnvBody: OAuth creds use the LOADED pack\'s envPrefix (not the slug) + brand/port/db', () => {
@@ -98,6 +125,18 @@ t('webEnvBody: a domain writes the single-host origin overrides so the hall load
98
125
  assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_OAUTH_ORIGIN=${O}`));
99
126
  });
100
127
 
128
+ t('webEnvBody: the repo target_ref seats the founding owner via <prefix>_FIRST_ADMIN (goal 35 / criterion 165)', () => {
129
+ // Regression (caught live on demo.cloudbongos.com, 2026-07-07): a provisioned instance
130
+ // MUST convey its founding owner, or src/bongos/auth.js maybeSeatFirstAdmin can never
131
+ // seat an Archon on first sign-in and the owner can't claim their own instance. Derived
132
+ // from the repo owner in target_ref — the SAME source instanceInitSpec uses for the
133
+ // scaffold's branding.firstAdmin, so the env override and config/branding.json agree.
134
+ const b = P.webEnvBody({ ...coTenant, hosting_shape: 'standalone', domain: 'mercury.example.com', target_ref: 'octocat/mercury' });
135
+ assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_FIRST_ADMIN=octocat`));
136
+ // No target_ref ⇒ the line is omitted (org-owned or unknown owner sets it by hand).
137
+ assert.doesNotMatch(P.webEnvBody(coTenant), /_FIRST_ADMIN=/);
138
+ });
139
+
101
140
  t('caddyBlock: a domain → on-demand-TLS reverse_proxy to the local port; no domain → a comment', () => {
102
141
  const withDomain = P.caddyBlock({ ...coTenant, domain: 'mercury.example.com' });
103
142
  assert.match(withDomain, /mercury\.example\.com \{/);
@@ -142,11 +181,22 @@ t('healthzCmd hits the instance port /healthz', () => {
142
181
  assert.match(P.healthzCmd(coTenant), /127\.0\.0\.1:3003\/healthz/);
143
182
  });
144
183
 
145
- t('dedicatedUserData bakes createdb + migrate into cloud-init', () => {
184
+ t('dedicatedUserData bakes createdb + migrate + first-version seed into cloud-init (task 2119: dedicated must not diverge)', () => {
146
185
  const ud = P.dedicatedUserData(coTenant);
147
186
  assert.match(ud, /^#cloud-config/);
148
187
  assert.match(ud, /createdb mercury/);
149
188
  assert.match(ud, /MEDUSA_GDS_ONLY=1/);
189
+ // the seed rides cloud-init too, so a dedicated droplet also comes up able to take tasks
190
+ assert.match(ud, /INSERT INTO versions/, 'seed baked into cloud-init');
191
+ // it must be its OWN runcmd line AFTER migrate (the schema must exist first)
192
+ const lines = ud.split('\n');
193
+ const iMig = lines.findIndex((l) => /migrate\.sh/.test(l));
194
+ const iSeed = lines.findIndex((l) => /INSERT INTO versions/.test(l));
195
+ assert.ok(iMig >= 0 && iSeed > iMig, 'seed line comes after the migrate line');
196
+ // the seed line is a valid bash -lc "…" item: single-quoted -c arg, NO double quotes to break the YAML scalar
197
+ const seedLine = lines[iSeed];
198
+ assert.match(seedLine, /- \[ bash, -lc, "psql .*" \]/, 'well-formed cloud-init runcmd item');
199
+ assert.doesNotMatch(seedLine.replace(/^.*-lc, "/, '').replace(/" \]$/, ''), /"/, 'the seed command carries no inner double quotes');
150
200
  });
151
201
 
152
202
  console.log('\nport allocation:');
@@ -215,12 +265,15 @@ await ta('provisionInstance(co-tenant) composes db → migrate → service → h
215
265
  const joined = cmds.join('\n');
216
266
  assert.match(joined, /createdb mercury/);
217
267
  assert.match(joined, /MEDUSA_GDS_ONLY=1/);
268
+ assert.match(joined, /INSERT INTO versions/, 'seeds the first version (task 2119)');
218
269
  assert.match(joined, /systemctl daemon-reload/);
219
270
  assert.match(joined, /systemctl enable --now mercury/);
220
271
  assert.match(joined, /healthz/);
221
- // ordering: createdb before migrate before enable before healthz
272
+ // ordering: createdb before migrate before SEED before enable before healthz
222
273
  const idx = (re) => cmds.findIndex((c) => re.test(c));
223
274
  assert.ok(idx(/createdb/) < idx(/migrate\.sh/), 'createdb before migrate');
275
+ assert.ok(idx(/migrate\.sh/) < idx(/INSERT INTO versions/), 'migrate before the version seed (schema must exist)');
276
+ assert.ok(idx(/INSERT INTO versions/) < idx(/enable --now/), 'seed before service enable');
224
277
  assert.ok(idx(/migrate\.sh/) < idx(/enable --now/), 'migrate before service enable');
225
278
  assert.ok(idx(/enable --now/) < idx(/healthz/), 'service before healthz');
226
279
  // writes: the per-instance env file + the systemd unit
@@ -243,8 +296,9 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
243
296
  assert.match(joined, /git pull --ff-only origin main/, 'pulls the instance repo');
244
297
  assert.match(joined, /npm ci --omit=dev/, 'installs the pinned core');
245
298
  assert.match(joined, /createdb mercury/);
246
- assert.match(joined, /node_modules\/@cloudbongos\/core\/scripts\/migrate\.sh/, 'migrates from the INSTANCE root\'s core');
299
+ assert.match(joined, /node_modules\/@bongos\/core\/scripts\/migrate\.sh/, 'migrates from the INSTANCE root\'s core');
247
300
  assert.ok(!cmds.some((c) => /\.\/scripts\/migrate\.sh/.test(c)), 'standalone does NOT use the control-plane checkout migrate');
301
+ assert.match(joined, /INSERT INTO versions/, 'standalone seeds its first version too (task 2119)');
248
302
  // ordering: clone → package-core → init → push → pull → npm ci → migrate → service
249
303
  const idx = (re) => cmds.findIndex((c) => re.test(c));
250
304
  assert.ok(idx(/git clone/) < idx(/package-core/), 'clone before tarball build');
@@ -253,6 +307,8 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
253
307
  assert.ok(idx(/push -u origin main/) < idx(/git pull/), 'scaffold push before the deploy pull');
254
308
  assert.ok(idx(/git pull/) < idx(/npm ci/), 'pull before npm ci');
255
309
  assert.ok(idx(/npm ci/) < idx(/migrate\.sh/), 'npm ci before migrate');
310
+ assert.ok(idx(/migrate\.sh/) < idx(/INSERT INTO versions/), 'migrate before the version seed');
311
+ assert.ok(idx(/INSERT INTO versions/) < idx(/enable --now/), 'seed before service enable');
256
312
  assert.ok(idx(/migrate\.sh/) < idx(/enable --now/), 'migrate before service enable');
257
313
  assert.ok(writes.some((w) => /\/mercury\.service$/.test(w)), 'wrote the (standalone) systemd unit');
258
314
  });
@@ -316,6 +372,47 @@ await ta('scaffoldStandaloneRepo: idempotent — a repo that already carries pac
316
372
  }
317
373
  });
318
374
 
375
+ // ── ADR 0134 (task 2090): registry core-install path + fail-soft to vendored ──
376
+ // Runs the scaffold against a FRESH standaloneBase (no package.json → it proceeds) with
377
+ // an injected env, and asserts which core-resolution shape it chose from the commands.
378
+ async function scaffoldWithEnv(env) {
379
+ const tmp = mkdtempSync(join(tmpdir(), 'scaffold-'));
380
+ const origBase = P.CONFIG.standaloneBase;
381
+ P.CONFIG.standaloneBase = tmp;
382
+ try {
383
+ const { deps, cmds } = recorder({ apply: true });
384
+ await P.scaffoldStandaloneRepo(
385
+ { slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' },
386
+ { ...deps, env },
387
+ );
388
+ return cmds.join('\n');
389
+ } finally {
390
+ P.CONFIG.standaloneBase = origBase;
391
+ rmSync(tmp, { recursive: true, force: true });
392
+ }
393
+ }
394
+
395
+ await ta('scaffoldStandaloneRepo: PROVISION_CORE_FROM_REGISTRY=1 + NPM_TOKEN → registry path (init --registry-core, no tarball build)', async () => {
396
+ const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1', NPM_TOKEN: 'npm_readonly' });
397
+ assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --registry-core/, 'materializes via --registry-core');
398
+ assert.ok(!/package-core\.js/.test(joined), 'does NOT build a core tarball on the registry path');
399
+ assert.ok(!/--vendor-core/.test(joined), 'does NOT vendor a tarball on the registry path');
400
+ });
401
+
402
+ await ta('scaffoldStandaloneRepo: registry requested but NO NPM_TOKEN → FAILS SOFT to the vendored tarball', async () => {
403
+ const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1' });
404
+ assert.match(joined, /node scripts\/gds\/package-core\.js/, 'builds the core tarball (vendored fallback)');
405
+ assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --vendor-core /, 'file:-pins the vendored tarball');
406
+ assert.ok(!/--registry-core/.test(joined), 'does NOT take the registry path without a token');
407
+ });
408
+
409
+ await ta('scaffoldStandaloneRepo: default (no registry env) stays on the vendored tarball path', async () => {
410
+ const joined = await scaffoldWithEnv({});
411
+ assert.match(joined, /node scripts\/gds\/package-core\.js/, 'vendored is the default');
412
+ assert.match(joined, /--vendor-core /);
413
+ assert.ok(!/--registry-core/.test(joined));
414
+ });
415
+
319
416
  await ta('provisionInstance is idempotent: an already-active instance is a no-op', async () => {
320
417
  const { deps, cmds } = recorder();
321
418
  const r = await P.provisionInstance({ ...coTenant, status: 'active' }, deps);
@@ -323,10 +420,11 @@ await ta('provisionInstance is idempotent: an already-active instance is a no-op
323
420
  assert.equal(cmds.length, 0, 'no commands run for an active instance');
324
421
  });
325
422
 
326
- await ta('provisionInstance(dedicated) does NOT run local createdb/migrate (they ride cloud-init)', async () => {
423
+ await ta('provisionInstance(dedicated) does NOT run local createdb/migrate/seed (they ride cloud-init)', async () => {
327
424
  const { deps, cmds } = recorder();
328
425
  await P.provisionInstance({ ...coTenant, hosting_shape: 'dedicated', tier: 's-2vcpu-4gb' }, deps);
329
426
  assert.ok(!cmds.some((c) => /createdb/.test(c)), 'dedicated does not run createdb locally');
427
+ assert.ok(!cmds.some((c) => /INSERT INTO versions/.test(c)), 'dedicated does not run the version seed locally (it rides cloud-init)');
330
428
  });
331
429
 
332
430
  console.log('\nDNS UPSERT composition (P4, CONFIG override):');
@@ -552,8 +650,8 @@ t('migrateCmd: privileged runs migrate as postgres via env+bash (survives sudo e
552
650
 
553
651
  t('standaloneMigrateCmd: privileged runs the instance-root migrate as postgres; default unchanged', () => {
554
652
  const inst = { ...coTenant, hosting_shape: 'standalone' };
555
- assert.equal(P.standaloneMigrateCmd(inst), 'PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@cloudbongos/core/scripts/migrate.sh');
556
- assert.equal(P.standaloneMigrateCmd(inst, { privileged: true }), 'sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@cloudbongos/core/scripts/migrate.sh');
653
+ assert.equal(P.standaloneMigrateCmd(inst), 'PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@bongos/core/scripts/migrate.sh');
654
+ assert.equal(P.standaloneMigrateCmd(inst, { privileged: true }), 'sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@bongos/core/scripts/migrate.sh');
557
655
  });
558
656
 
559
657
  t('makeWriteFile(privileged): a { sudo:true } write stages a temp file + `sudo install -D -m <mode>` (never writes /etc directly)', () => {
@@ -615,6 +713,7 @@ await ta('provisionInstance(co-tenant, privileged): createdb/migrate run as post
615
713
  const joined = cmds.join('\n');
616
714
  assert.match(joined, /sudo -u postgres createdb mercury/, 'createdb as postgres');
617
715
  assert.match(joined, /sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash \.\/scripts\/migrate\.sh/, 'migrate as postgres');
716
+ assert.match(joined, /sudo -u postgres psql .*INSERT INTO versions/, 'first-version seed runs as postgres too (task 2119)');
618
717
  assert.match(joined, /sudo systemctl daemon-reload/, 'daemon-reload sudo-prefixed');
619
718
  assert.match(joined, /sudo systemctl enable --now mercury/, 'enable sudo-prefixed');
620
719
  assert.match(joined, /sudo systemctl reload caddy/, 'reload caddy sudo-prefixed');
@@ -30,7 +30,7 @@ provisioning.attachManifestCode = async (_db, id, code) => ({ id, status: 'code_
30
30
  // A branding with a known publicOrigin so mothershipOrigin() resolves deterministically.
31
31
  const branding = require(`${ROOT}/src/branding.js`);
32
32
  const realBranding = branding.branding;
33
- branding.branding = () => ({ ...realBranding(), domains: { publicOrigin: 'https://cloudbongos.com' } });
33
+ branding.branding = () => ({ ...realBranding(), identity: { ...(realBranding().identity || {}), productName: 'Cloud Bongos' }, domains: { publicOrigin: 'https://cloudbongos.com' } });
34
34
 
35
35
  // Inject the signed-in owner (the real requireBuilder needs a session).
36
36
  const api = require(`${ROOT}/src/module-api.js`);
@@ -66,15 +66,31 @@ console.log('POST /provisioning/instances/:id/github-app (mint the one-click lin
66
66
  t('post_url targets GitHub personal apps/new carrying the state', j.post_url === `https://github.com/settings/apps/new?state=${j.state}`, j.post_url);
67
67
  t('manifest.redirect_url is the MOTHERSHIP callback', j.manifest && j.manifest.redirect_url === 'https://cloudbongos.com/api/bongos/provisioning/github-app/callback', j.manifest && j.manifest.redirect_url);
68
68
  t('manifest.callback_urls are the INSTANCE sign-in (both prefixes)', j.manifest && j.manifest.callback_urls[0] === 'https://mercury.example.com/api/gds/auth/web/callback' && j.manifest.callback_urls[1] === 'https://mercury.example.com/api/bongos/auth/web/callback');
69
+ t('manifest.name is the instance domain (unique + matches the consent-screen redirect) within 34 chars', j.manifest && j.manifest.name === 'mercury.example.com' && j.manifest.name.length <= 34, j.manifest && j.manifest.name);
69
70
  t('org_url derived from the target_ref owner (acme)', j.org_url === `https://github.com/organizations/acme/settings/apps/new?state=${j.state}`, j.org_url);
70
71
  t('the attempt row was persisted with the minted state', !!(stored && stored.instanceId === 42 && stored.state === j.state && stored.requestedBy === 7));
71
72
  }
72
73
 
74
+ console.log('\nprovisioningOrigin override (task 2101 — customer App carries the PLATFORM brand, not the game domain):');
75
+ {
76
+ // A control plane that co-hosts a game (OTB = customer 0) has a GAME-branded publicOrigin.
77
+ // provisioningOrigin is the dedicated front door the customer's GitHub App must point at,
78
+ // so mothershipOrigin() must prefer it over publicOrigin. mothershipOrigin() calls
79
+ // api.branding() (module-api captured the fn ref at require-time), so override THAT.
80
+ const prevBranding = api.branding;
81
+ api.branding = () => ({ ...realBranding(), identity: { productName: 'Cloud Bongos' }, domains: { publicOrigin: 'https://amazonprimea.com', provisioningOrigin: 'https://get.cloudbongos.com' } });
82
+ const res = await fetch(`${base}/provisioning/instances/42/github-app`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
83
+ const j = await res.json();
84
+ t('redirect_url uses provisioningOrigin, not the game publicOrigin', j.manifest && j.manifest.redirect_url === 'https://get.cloudbongos.com/api/bongos/provisioning/github-app/callback', j.manifest && j.manifest.redirect_url);
85
+ t('webhook hook_attributes.url also uses provisioningOrigin', j.manifest && j.manifest.hook_attributes && j.manifest.hook_attributes.url === 'https://get.cloudbongos.com/api/bongos/provisioning/github-app/webhook', j.manifest && j.manifest.hook_attributes && j.manifest.hook_attributes.url);
86
+ api.branding = prevBranding;
87
+ }
88
+
73
89
  console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect):');
74
90
  {
75
91
  const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
76
92
  const html = await res.text();
77
- t('good state → 200 text/html finishing-setup page', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /GitHub App created/.test(html) && /no secret/i.test(html), `status=${res.status}`);
93
+ t('good state → 200 text/html progress page wired to auto-open', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /Setting up your instance/.test(html) && /Placing credentials/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
78
94
  t('the page names the instance host', /mercury\.example\.com/.test(html));
79
95
  }
80
96
  {
@@ -87,6 +103,32 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
87
103
  t('missing code/state → 400', res.status === 400, `status=${res.status}`);
88
104
  }
89
105
 
106
+ console.log('\nGET /provisioning/github-app/status (the page poll → auto-open signal):');
107
+ {
108
+ const res = await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`);
109
+ const j = await res.json();
110
+ t('pending → 200 ok, not ready, names the domain', res.status === 200 && j.ok === true && j.status === 'pending' && j.ready === false && j.domain === 'mercury.example.com', `status=${res.status} ${JSON.stringify(j)}`);
111
+ }
112
+ {
113
+ // Swap the stub to a placed-credentials ('done') attempt to prove ready flips true.
114
+ const prev = provisioning.getOAuthManifestByState;
115
+ provisioning.getOAuthManifestByState = async (_db, state) => (state === 'GOODSTATE'
116
+ ? { id: 99, instance_id: 42, requested_by: 7, status: 'done', expires_at: new Date(Date.now() + 600000).toISOString() }
117
+ : null);
118
+ const res = await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`);
119
+ const j = await res.json();
120
+ t("status 'done' → ready:true + terminal:true", res.status === 200 && j.ready === true && j.terminal === true, JSON.stringify(j));
121
+ provisioning.getOAuthManifestByState = prev;
122
+ }
123
+ {
124
+ const res = await fetch(`${base}/provisioning/github-app/status?state=NOPE`);
125
+ t('unknown state → 404 (no oracle)', res.status === 404, `status=${res.status}`);
126
+ }
127
+ {
128
+ const res = await fetch(`${base}/provisioning/github-app/status`);
129
+ t('missing state → 400', res.status === 400, `status=${res.status}`);
130
+ }
131
+
90
132
  console.log('\nparseId contract (the (req,res) signature — a bad id is a clean 400, not a 500):');
91
133
  {
92
134
  const res = await fetch(`${base}/provisioning/instances/not-a-number/github-app`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });