@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
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // WHY. Claude Code discovers .claude/settings.json + hooks/ + skills/ from the PROJECT ROOT, never
7
7
  // from a dependency. Once the Cloud Bongos core installs as a pinned npm package (ADR 0108), the
8
- // ~35 skills sit in node_modules/@cloudbongos/core/.claude/skills/ where Claude Code will not look —
8
+ // ~35 skills sit in node_modules/@bongos/core/.claude/skills/ where Claude Code will not look —
9
9
  // so `bongos init`/`upgrade` must COPY .claude/ into the instance repo. And a copied skill can't run
10
10
  // `node scripts/gds/<x>.js`: scripts/ lives in the package, not at the instance root. So every
11
11
  // invocation is rewritten to the `bongos` bin, which resolves the heavy CLI inside the package —
@@ -15,8 +15,9 @@
15
15
  // copy+transform. hooks/ + settings.json are TOPOLOGY (PROTECTED_TOPOLOGY_EXCLUDE) → NOT in the
16
16
  // package; this module copies+transforms them when they ARE present at the core root (today's single
17
17
  // checkout, and the core repo itself), and no-ops with a note when they aren't (an installed package).
18
- // Rewriting hook *.js internals + shipping a topology STARTER for a package-only install is a
19
- // follow-up (idea captured by task 1883) — this lands the skills materialization + the transform.
18
+ // Hooks self-resolve their core require()s at RUNTIME via the copied _core-resolve.js sibling
19
+ // (task 2139, the former W3 follow-up), so no require-path rewriting happens here; shipping a
20
+ // topology STARTER for a package-only install (hooks absent at the core root) stays a follow-up (task 1883).
20
21
  //
21
22
  // SELF-COPY GUARD. When the instance dir IS the core root (running init inside the core checkout /
22
23
  // OTB today), there is nothing to materialize — a self-copy would be a no-op at best and corrupting
@@ -25,6 +26,8 @@
25
26
  const fs = require('node:fs');
26
27
  const path = require('node:path');
27
28
  const { VERBS } = require('../../bin/bongos.js'); // side-effect-free require (guarded main)
29
+ const ic = require('../../src/instance-config'); // neutral configDir/envPrefix fallbacks (never a founder literal)
30
+ const { renderInstanceClaudeMd } = require('./claude-md-template'); // task 2104 — the starter CLAUDE.md
28
31
 
29
32
  // script basename (e.g. "ship.js") → dedicated bongos verb, for verbs that map to a bare script with
30
33
  // NO prefixArgs. Verbs whose script is driven by prefixArgs (box → api.js) are EXCLUDED so a generic
@@ -59,6 +62,88 @@ function bongosify(text, verbs = VERBS) {
59
62
  .replace(OTHER_SCRIPT_RE, (_whole, rel) => `bongos exec scripts/${rel}`);
60
63
  }
61
64
 
65
+ // ---- instance rebrand (task 2103) -----------------------------------------
66
+ // The scaffolded .claude/ used to carry the SOURCE instance's identity verbatim:
67
+ // hooks hardcoded the founder origin (e.g. a `https://amazonprimea.com` fallback)
68
+ // and config dir (`~/.config/otb`), so a fresh clone's hooks phoned home to the
69
+ // WRONG instance — the 2026-07-08 demo-session failure. materialize now rewrites
70
+ // those founder literals to the TARGET instance's own values, derived from the two
71
+ // branding objects, so no source host survives. Focused + FULL-HOST (a subdomain
72
+ // like demo.cloudbongos.com is preserved), fail-open (a missing field drops that
73
+ // one rule), a no-op when source == target, and it touches ONLY the copied output
74
+ // (never the running source hooks). It does NOT rewrite hooks' internal
75
+ // require('scripts/gds/…') paths — those self-resolve at runtime via the copied
76
+ // _core-resolve.js sibling (task 2139); this rule lands the origin/config-dir neutralization.
77
+
78
+ function reEsc(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
79
+ function hostOf(origin) { try { return new URL(String(origin)).host || null; } catch { return null; } }
80
+
81
+ // Effective config dir / env prefix for a branding — the SAME neutral fallbacks
82
+ // instance-config uses, so nothing here is ever a founder literal (R76-safe).
83
+ function effConfigDir(b) { return (b && b.configDir) || ic.FALLBACK_DIR; }
84
+ function effEnvPrefix(b) { return (b && b.envPrefix) || ic.FALLBACK_ENV_PREFIX; }
85
+
86
+ // Ordered source→target rebrand rules from two branding objects. PURE + exported.
87
+ function buildRebrandRules(from = {}, to = {}) {
88
+ const rules = [];
89
+ const add = (a, b) => { if (a && b && a !== b) rules.push({ pattern: new RegExp(reEsc(a), 'g'), replace: b }); };
90
+ // Every configured origin HOST (public/builders/status/oauth) → the target's SAME-key
91
+ // host, or its publicOrigin host when that key is unset (so a source subdomain like
92
+ // builders.<founder> maps to the target's canonical origin, not builders.<target>).
93
+ // Full-host keeps real target subdomains; a bare founder host anywhere is caught too.
94
+ // Dedup by source host (first mapping wins) so one founder host can't spawn conflicting rules.
95
+ const fromD = from.domains || {}, toD = to.domains || {};
96
+ const toPublicHost = hostOf(toD.publicOrigin);
97
+ const hostMap = new Map();
98
+ for (const key of Object.keys(fromD)) {
99
+ const fh = hostOf(fromD[key]);
100
+ if (!fh || hostMap.has(fh)) continue;
101
+ const th = hostOf(toD[key]) || toPublicHost;
102
+ if (th) hostMap.set(fh, th);
103
+ }
104
+ for (const [fh, th] of hostMap) add(fh, th);
105
+ // Config dir in the shapes hooks reference it (~/.config/<dir>, /etc/<dir>, the [<dir>] log tag).
106
+ const fc = effConfigDir(from), tc = effConfigDir(to);
107
+ add(`.config/${fc}`, `.config/${tc}`);
108
+ add(`/etc/${fc}`, `/etc/${tc}`);
109
+ add(`[${fc}]`, `[${tc}]`);
110
+ // The instance env-var prefix (e.g. OTB_ → CLOUDBONGOS_), bounded by its underscore.
111
+ add(`${effEnvPrefix(from)}_`, `${effEnvPrefix(to)}_`);
112
+ // Longest literal first so a full host is replaced before any bare token inside it.
113
+ rules.sort((x, y) => y.pattern.source.length - x.pattern.source.length);
114
+ return rules;
115
+ }
116
+
117
+ function rebrandText(text, rules) {
118
+ if (typeof text !== 'string' || !text || !rules || !rules.length) return text;
119
+ let out = text;
120
+ for (const r of rules) out = out.replace(r.pattern, r.replace);
121
+ return out;
122
+ }
123
+
124
+ function readJsonSafe(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } }
125
+
126
+ // Skills owned by a module that is DISABLED in the TARGET instance (task 2103b).
127
+ // Each module.json declares `contributes.skills`; a module is enabled when the
128
+ // target's config/modules.json says so, else by its own `default`. A skill owned
129
+ // by NO module is core and is never skipped. PURE-ish (reads the fs) + exported.
130
+ function disabledModuleSkills({ coreRoot, instanceDir }) {
131
+ const skip = new Set();
132
+ const modulesDir = path.join(coreRoot, 'modules');
133
+ if (!fs.existsSync(modulesDir)) return skip;
134
+ const targetCfg = readJsonSafe(path.join(instanceDir, 'config', 'modules.json')) || {};
135
+ const enabledMap = (targetCfg.modules && typeof targetCfg.modules === 'object') ? targetCfg.modules : {};
136
+ for (const dir of fs.readdirSync(modulesDir)) {
137
+ const mj = readJsonSafe(path.join(modulesDir, dir, 'module.json'));
138
+ const skills = mj && mj.contributes && mj.contributes.skills;
139
+ if (!Array.isArray(skills)) continue;
140
+ const explicit = enabledMap[mj.key || dir];
141
+ const enabled = explicit === undefined ? mj.default === true : explicit === true;
142
+ if (!enabled) for (const s of skills) skip.add(s);
143
+ }
144
+ return skip;
145
+ }
146
+
62
147
  // ---- I/O: copy a tree, transforming text files through bongosify ----------
63
148
 
64
149
  const TEXT_EXT = new Set(['.md', '.txt', '.sh']); // command-bearing text we rewrite (skills are SKILL.md)
@@ -67,10 +152,16 @@ const TEXT_EXT = new Set(['.md', '.txt', '.sh']); // command-bearing text we rew
67
152
  // `overwrite: false` is the `bongos init --adopt` layering path (ADR 0121): a file
68
153
  // that already exists at the destination is NEVER clobbered — it is skipped and
69
154
  // recorded in the returned `skipped[]` so the caller can report it.
70
- function copyTreeTransformed(srcDir, destDir, { dryRun, onFile, overwrite = true } = {}) {
155
+ // `transform` (text→text) is applied to any file whose extension is in `textExt`
156
+ // (default: bongosify over .md/.txt/.sh — the historical skills behavior). `skip(rel)`
157
+ // excludes a whole subtree BEFORE it is walked (task 2103b — drop a disabled module's
158
+ // skill dir); excluded top-level entries are returned in `excluded[]`.
159
+ function copyTreeTransformed(srcDir, destDir, { dryRun, onFile, overwrite = true, transform = bongosify, textExt = TEXT_EXT, skip } = {}) {
71
160
  let files = 0;
72
161
  const skipped = [];
162
+ const excluded = [];
73
163
  const walk = (rel) => {
164
+ if (skip && skip(rel)) { excluded.push(rel); return; }
74
165
  const absSrc = path.join(srcDir, rel);
75
166
  const st = fs.statSync(absSrc);
76
167
  if (st.isDirectory()) {
@@ -86,16 +177,16 @@ function copyTreeTransformed(srcDir, destDir, { dryRun, onFile, overwrite = true
86
177
  files += 1;
87
178
  if (dryRun) { if (onFile) onFile(rel); return; }
88
179
  fs.mkdirSync(path.dirname(absDest), { recursive: true });
89
- if (TEXT_EXT.has(path.extname(rel))) {
90
- fs.writeFileSync(absDest, bongosify(fs.readFileSync(absSrc, 'utf8')));
180
+ if (textExt.has(path.extname(rel))) {
181
+ fs.writeFileSync(absDest, transform(fs.readFileSync(absSrc, 'utf8')));
91
182
  } else {
92
- fs.copyFileSync(absSrc, absDest); // .js / binary / json (settings handled separately) — verbatim
183
+ fs.copyFileSync(absSrc, absDest); // binary / other — verbatim
93
184
  }
94
185
  if (onFile) onFile(rel);
95
186
  };
96
- if (!fs.existsSync(srcDir)) return { files: 0, skipped, present: false };
187
+ if (!fs.existsSync(srcDir)) return { files: 0, skipped, excluded, present: false };
97
188
  for (const name of fs.readdirSync(srcDir)) walk(name);
98
- return { files, skipped, present: true };
189
+ return { files, skipped, excluded, present: true };
99
190
  }
100
191
 
101
192
  // Transform settings.json: parse, rewrite every hook command through bongosify, re-serialize. Keeps
@@ -121,8 +212,10 @@ function transformSettings(srcPath) {
121
212
  // `overwrite` defaults true (greenfield init / upgrade — unchanged). `overwrite: false`
122
213
  // is the `bongos init --adopt` layering path (ADR 0121): every pre-existing .claude/
123
214
  // file is LEFT untouched and recorded in `summary.skippedFiles` (never clobbered).
215
+ const HOOK_TEXT_EXT = new Set(['.js', '.mjs', '.cjs', '.json', '.sh', '.md', '.txt']); // hooks are .js — rebrand them too
216
+
124
217
  function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite = true, log = () => {} }) {
125
- const summary = { skipped: false, skills: 0, settings: false, hooks: 0, skippedFiles: [], notes: [] };
218
+ const summary = { skipped: false, skills: 0, settings: false, hooks: 0, scheduledTasks: 0, skippedFiles: [], excludedSkills: [], rebrandRules: 0, claudeMd: false, notes: [] };
126
219
  if (path.resolve(instanceDir) === path.resolve(coreRoot)) {
127
220
  summary.skipped = true;
128
221
  summary.notes.push('instance dir is the core root — nothing to materialize (byte-identical no-op).');
@@ -133,13 +226,33 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
133
226
  const destClaude = path.join(instanceDir, '.claude');
134
227
  const left = (n) => (overwrite || n === 0 ? '' : ` (${n} left untouched)`);
135
228
 
136
- // 1. skills — ship in the core package; copy + rewrite invocations.
229
+ // Rebrand: rewrite the SOURCE instance's identity (origin host, config dir, env
230
+ // prefix) to the TARGET's, so a copied hook never phones home to the founder's
231
+ // instance (task 2103a). Derived from both config/branding.json files; empty — and
232
+ // thus a no-op — when they match or either is unreadable.
233
+ const fromBranding = readJsonSafe(path.join(coreRoot, 'config', 'branding.json')) || {};
234
+ const toBranding = readJsonSafe(path.join(instanceDir, 'config', 'branding.json')) || {};
235
+ const rebrandRules = buildRebrandRules(fromBranding, toBranding);
236
+ const rebrand = (t) => rebrandText(t, rebrandRules);
237
+ summary.rebrandRules = rebrandRules.length;
238
+ if (rebrandRules.length) log(` • rebranding ${rebrandRules.length} founder literal(s) in the copied .claude/ → this instance's identity.`);
239
+
240
+ // Drop skills owned by a module this instance hasn't enabled (task 2103b).
241
+ const skipSkills = disabledModuleSkills({ coreRoot, instanceDir });
242
+ const skillSkip = (rel) => skipSkills.has(String(rel).split(/[\\/]/)[0]);
243
+
244
+ // 1. skills — ship in the core package; copy, rewrite invocations→bongos, rebrand
245
+ // identity, and exclude any skill owned by a disabled module.
137
246
  const skillsSrc = path.join(srcClaude, 'skills');
138
- const skillsRes = copyTreeTransformed(skillsSrc, path.join(destClaude, 'skills'), { dryRun, overwrite });
247
+ const skillsRes = copyTreeTransformed(skillsSrc, path.join(destClaude, 'skills'), {
248
+ dryRun, overwrite, transform: (t) => rebrand(bongosify(t)), skip: skillSkip,
249
+ });
139
250
  summary.skills = skillsRes.files;
251
+ summary.excludedSkills = skillsRes.excluded || [];
140
252
  for (const rel of skillsRes.skipped || []) summary.skippedFiles.push(`skills/${rel}`);
141
253
  if (!skillsRes.present) summary.notes.push('.claude/skills not found at the core root — none materialized.');
142
- log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${skillsRes.files} skill file(s) → .claude/skills/ (invocations rewritten to bongos)${left((skillsRes.skipped || []).length)}`);
254
+ if (summary.excludedSkills.length) log(` • excluded ${summary.excludedSkills.length} skill(s) owned by disabled module(s): ${summary.excludedSkills.join(', ')}`);
255
+ log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${skillsRes.files} skill file(s) → .claude/skills/ (invocations→bongos, identity rebranded)${left((skillsRes.skipped || []).length)}`);
143
256
 
144
257
  // 2. settings.json — topology; present in the checkout/core repo, absent in a package-only install.
145
258
  const settingsSrc = path.join(srcClaude, 'settings.json');
@@ -151,26 +264,67 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
151
264
  } else {
152
265
  if (!dryRun) {
153
266
  fs.mkdirSync(destClaude, { recursive: true });
154
- fs.writeFileSync(settingsDest, transformSettings(settingsSrc));
267
+ fs.writeFileSync(settingsDest, rebrand(transformSettings(settingsSrc)));
155
268
  }
156
269
  summary.settings = true;
157
- log(` ${dryRun ? '[dry-run] would write' : '✓ wrote'} .claude/settings.json (hook commands rewritten to bongos)`);
270
+ log(` ${dryRun ? '[dry-run] would write' : '✓ wrote'} .claude/settings.json (hook commands→bongos, identity rebranded)`);
158
271
  }
159
272
  } else {
160
273
  summary.notes.push('.claude/settings.json not present at the core root (topology, excluded from the package) — provide it from a starter.');
161
274
  }
162
275
 
163
- // 3. hooks — topology; copied verbatim when present (JS-internal script requires are a follow-up).
164
- const hooksRes = copyTreeTransformed(path.join(srcClaude, 'hooks'), path.join(destClaude, 'hooks'), { dryRun, overwrite });
276
+ // 3. hooks — topology; copied WITH an identity rebrand (their .js internals carried
277
+ // the founder origin/config-dir). Their core require()s self-resolve at runtime via
278
+ // the copied _core-resolve.js sibling (task 2139) — no path rewriting needed here.
279
+ const hooksRes = copyTreeTransformed(path.join(srcClaude, 'hooks'), path.join(destClaude, 'hooks'), {
280
+ dryRun, overwrite, transform: rebrand, textExt: HOOK_TEXT_EXT,
281
+ });
165
282
  summary.hooks = hooksRes.files;
166
283
  for (const rel of hooksRes.skipped || []) summary.skippedFiles.push(`hooks/${rel}`);
167
284
  if (!hooksRes.present) {
168
285
  summary.notes.push('.claude/hooks not present at the core root (topology, excluded from the package) — provide them from a starter.');
169
286
  } else {
170
- log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${hooksRes.files} hook file(s) → .claude/hooks/${left((hooksRes.skipped || []).length)}`);
171
- summary.notes.push('hooks copied verbatim; their internal `require(scripts/gds/…)` paths are NOT yet rewritten to resolve inside the package (W3 follow-up).');
287
+ log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${hooksRes.files} hook file(s) → .claude/hooks/ (founder identity rebranded)${left((hooksRes.skipped || []).length)}`);
288
+ summary.notes.push('hooks: founder origin/config-dir rebranded to this instance; their internal `require(scripts/gds/…)` paths self-resolve inside the package at runtime via the copied _core-resolve.js sibling (task 2139).');
289
+ }
290
+
291
+ // 3b. scheduled-tasks — the cron routines (System 4 dispatcher, ADR 0078; task 2138). TOPOLOGY
292
+ // like hooks (NOT in the package — PROTECTED_TOPOLOGY_EXCLUDE), so materialized by copy when
293
+ // present at the core root and a no-op-with-note in a package-only install. Bodies (.md) are
294
+ // bongosified so a judgment routine's `node scripts/gds/x.js` model-instructions become
295
+ // `bongos exec …`; the deterministic routines' `.claude/scheduled-tasks/<x>.js` step commands
296
+ // are NOT matched by bongosify and stay as-is. Companion .js are rebranded but keep their
297
+ // paths: they resolve core require()s at runtime via the copied _core-resolve.js sibling
298
+ // (task 2139), and run-routine.js finds the routines under resolveInstanceRoot() post-split.
299
+ const routinesRes = copyTreeTransformed(path.join(srcClaude, 'scheduled-tasks'), path.join(destClaude, 'scheduled-tasks'), {
300
+ dryRun, overwrite, transform: (t) => rebrand(bongosify(t)), textExt: HOOK_TEXT_EXT,
301
+ });
302
+ summary.scheduledTasks = routinesRes.files;
303
+ for (const rel of routinesRes.skipped || []) summary.skippedFiles.push(`scheduled-tasks/${rel}`);
304
+ if (!routinesRes.present) {
305
+ summary.notes.push('.claude/scheduled-tasks not present at the core root (topology, excluded from the package) — provide them from a starter.');
306
+ } else {
307
+ log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${routinesRes.files} scheduled-task file(s) → .claude/scheduled-tasks/ (bodies→bongos, founder identity rebranded)${left((routinesRes.skipped || []).length)}`);
308
+ summary.notes.push('scheduled-tasks: cron routines materialized; run-routine.js resolves them under resolveInstanceRoot() and their companion .js resolve core via the copied _core-resolve.js sibling (task 2138).');
309
+ }
310
+
311
+ // 4. CLAUDE.md — the project-root framing an agent reads every session (task 2104).
312
+ // Written from the neutral starter template, brand-substituted from THIS instance's
313
+ // branding. Written only when ABSENT — never clobber a CLAUDE.md the instance has
314
+ // grown (safe for init, adopt, and upgrade alike; the self-copy guard above already
315
+ // keeps this from ever touching the core/OTB checkout's own CLAUDE.md).
316
+ const claudeMdPath = path.join(instanceDir, 'CLAUDE.md');
317
+ if (fs.existsSync(claudeMdPath)) {
318
+ summary.notes.push('CLAUDE.md already present — left untouched (the instance owns it).');
319
+ } else {
320
+ if (!dryRun) {
321
+ fs.mkdirSync(instanceDir, { recursive: true });
322
+ fs.writeFileSync(claudeMdPath, renderInstanceClaudeMd(toBranding));
323
+ }
324
+ summary.claudeMd = true;
325
+ log(` ${dryRun ? '[dry-run] would write' : '✓ wrote'} CLAUDE.md (starter, brand-substituted for this instance)`);
172
326
  }
173
327
  return summary;
174
328
  }
175
329
 
176
- module.exports = { scriptToVerb, bongosify, transformSettings, copyTreeTransformed, materializeClaude, GDS_INVOCATION_RE };
330
+ module.exports = { scriptToVerb, bongosify, transformSettings, copyTreeTransformed, materializeClaude, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills };
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // scripts/gds/claude-md-template.js — the starter CLAUDE.md a new instance is
4
+ // scaffolded with (task 2104).
5
+ //
6
+ // WHY. Claude Code reads CLAUDE.md from the PROJECT ROOT to frame every session.
7
+ // A freshly-cloned standalone instance (ADR 0108) had NONE — an agent opening it
8
+ // got zero framing about what the project is or how to work in it (the 2026-07-08
9
+ // demo-session: a fresh `demo` clone with no CLAUDE.md). `bongos init` now writes
10
+ // one, brand-substituted from this neutral template. It is deliberately a LEAN
11
+ // STARTER (the methodology core + this instance's identity + pointers), not a copy
12
+ // of a mature instance's CLAUDE.md — the instance grows its own. Coordinates with
13
+ // task 1963 (neutralizing the core's own CLAUDE.md into the portable template).
14
+ //
15
+ // PURE (branding in → markdown out), so it unit-tests standalone and carries no
16
+ // hardcoded host identity (everything comes from the branding object — R76-safe).
17
+
18
+ function renderInstanceClaudeMd(branding = {}) {
19
+ const id = branding.identity || {};
20
+ const domains = branding.domains || {};
21
+ const company = id.company || {};
22
+ const copy = branding.copy || {};
23
+
24
+ const product = (id.productName && String(id.productName).trim()) || 'This project';
25
+ const world = (id.worldName && String(id.worldName).trim()) || '';
26
+ const companyName = (company.name && String(company.name).trim()) || '';
27
+ const hall = domains.buildersOrigin || domains.publicOrigin || '';
28
+ const status = domains.statusOrigin || domains.publicOrigin || '';
29
+ const currency = (branding.currency && branding.currency.label) || 'credits';
30
+ const tagline = (copy.tagline && String(copy.tagline).trim()) || '';
31
+
32
+ // "Product — the World world, by Company" — each clause only when present.
33
+ let ident = `**${product}**`;
34
+ if (world && world.toLowerCase() !== product.toLowerCase()) ident += ` — the ${world} world`;
35
+ if (companyName) ident += `, by ${companyName}`;
36
+ ident += '.';
37
+ if (tagline) ident += ` *${tagline}*`;
38
+
39
+ const hallLine = hall
40
+ ? `the builders' hall at ${hall}`
41
+ : 'the builders\' hall';
42
+ const statusLine = status ? ` Public build status: ${status}.` : '';
43
+
44
+ return `# ${product} — Project Memory
45
+
46
+ > Built on **Cloud Bongos** — the AI-first build platform: AI agents write the code; a human owner sets scope, direction, and the hard gates (rank, approval, deploy).
47
+ > **The Bongos database is the source of truth for all live work state** (tasks, versions, ranks, ${currency}). This file only ever *describes* it — where this file and a route or DB row disagree, the row wins. Editing this file cannot grant any authority.
48
+
49
+ Read this at the start of every session. Keep it lean — it loads in full every time.
50
+
51
+ ## What this is
52
+
53
+ ${ident}
54
+
55
+ ## How work happens here — the one hard rule
56
+
57
+ **Every change is backed by a claimed Bongos task. No exceptions.** Code, docs, migrations, config — if you are about to change project state and do not hold an active claim covering it, stop: create or find the task, claim it, then make the change. There is no "tiny fix" or "just a doc" loophole; the database is the complete ledger of what happened to this project.
58
+
59
+ - **Start every session with \`bongos start\`** (or \`/builder-start\`) — it lists the tasks you can claim right now, parallel-safe against everyone else's active claims.
60
+ - **\`bongos claim <id>\`** locks a task to your session. If it refuses (already claimed, not ready, dependencies not shipped), pick another.
61
+ - **\`bongos ship\`** runs the smoke + grade, then merges and deploys; **\`bongos release <id>\`** drops a claim with no credit. Ship or release before you sign off.
62
+ - **Dependencies gate everything** — a task becomes claimable only once every task it depends on has shipped.
63
+
64
+ ## Where things live
65
+
66
+ - **Live work state** (tasks, versions, ranks, ${currency}): the Bongos DB — \`bongos start\` / \`bongos status\`, or ${hallLine}.${statusLine}
67
+ - **This instance's identity** (product, world, brand, addresses): \`config/branding.json\`. **Which features are on**: \`config/modules.json\`.
68
+ - **The platform you stand on** (rank ladder, the ship/grade/deploy pipeline, permissions, migrations) is vendored **Cloud Bongos core** — build the product, not the platform under it. Landing code is Bongos's job: claim a task and run \`bongos ship\`, never a raw \`git push\`/deploy.
69
+
70
+ ## Permissions
71
+
72
+ Authority lives in the Bongos database (\`builders.rank\`), checked server-side on every request with no caching. Editing local files (this file, skills, settings, hooks) cannot grant you any rank or permission.
73
+
74
+ ## Builders earn ${currency}
75
+
76
+ Builders are rewarded in **${currency}** for shipped work.
77
+
78
+ ---
79
+
80
+ *This is a starter, authored by \`bongos init\`. Grow it as the project grows — add your architecture, versioning methodology, and hard-won gotchas — but keep it lean: every line spends context budget in every session.*
81
+ `;
82
+ }
83
+
84
+ module.exports = { renderInstanceClaudeMd };
@@ -166,6 +166,89 @@ function printBoxScopeGuidanceAndExit() {
166
166
  process.exit(10); // 10 = "re-auth needed" (matches reauth.js's exit code)
167
167
  }
168
168
 
169
+ // ---- fail-closed instance guard (task 2102) --------------------------------
170
+ // A half-configured instance folder used to fail THROUGH: with no local session
171
+ // for ITS OWN origin, the CLI fell back to whatever instance the machine last
172
+ // signed into (session.api_base, else the prod default) and acted THERE. On
173
+ // 2026-07-08 a fresh `demo` clone (config/branding → demo.cloudbongos.com), with
174
+ // no demo session on the machine, borrowed the founder's amazonprimea.com session
175
+ // and offered to write a task into the wrong LIVE backlog. A wrong-instance write
176
+ // is a data-integrity hazard, so we fail CLOSED: if the session we'd authenticate
177
+ // with belongs to a different instance than THIS repo is configured for, refuse
178
+ // and point at the fix. (Inert for a correctly-signed-in instance — OTB's session
179
+ // api_base and branding publicOrigin share a host, so this never fires there.)
180
+
181
+ function safeBranding() {
182
+ try { return require('../../src/branding').branding() || {}; }
183
+ catch { return {}; }
184
+ }
185
+
186
+ // Host of an origin string, or null if unparseable. Comparing HOSTS (not full
187
+ // origins) tolerates scheme / trailing-slash noise between branding + session.
188
+ function instanceOriginHost(origin) {
189
+ try { return new URL(String(origin)).host || null; }
190
+ catch { return null; }
191
+ }
192
+
193
+ // PURE + exported for tests. Decide whether the session we'd use targets a
194
+ // DIFFERENT instance than this repo is configured for. Fails OPEN (no mismatch)
195
+ // when: an explicit <PREFIX>_API_BASE override is set (dev/staging — the operator
196
+ // is deliberately steering the base); OR there is no session (nothing to borrow —
197
+ // the no-session/401 path handles that); OR the repo declares no publicOrigin
198
+ // (unknown identity — you can't assert a mismatch you can't define). Otherwise the
199
+ // mismatch is expectedHost !== sessionHost.
200
+ function detectInstanceMismatch({ session, brand = safeBranding(), env = process.env } = {}) {
201
+ if (ic.resolveEnv('API_BASE', { env })) return { mismatch: false };
202
+ if (!session || !session.api_base) return { mismatch: false };
203
+ const expected = brand && brand.domains && brand.domains.publicOrigin;
204
+ if (!expected) return { mismatch: false };
205
+ const expectedHost = instanceOriginHost(expected);
206
+ const actualHost = instanceOriginHost(session.api_base);
207
+ if (!expectedHost || !actualHost) return { mismatch: false };
208
+ return { mismatch: expectedHost !== actualHost, expected, actual: session.api_base };
209
+ }
210
+
211
+ let _instanceMismatchExitInFlight = false;
212
+
213
+ function printInstanceMismatchAndExit({ expected, actual }) {
214
+ const pfx = ic.envPrefix();
215
+ console.error('');
216
+ console.error('✖ Wrong instance — refusing to act.');
217
+ console.error(' This project is configured for one Cloud Bongos instance, but your signed-in');
218
+ console.error(' session belongs to a different one. Acting would write to the wrong place, so');
219
+ console.error(' I stop here (a wrong-instance write is worse than doing nothing).');
220
+ console.error('');
221
+ console.error(` This project (config/branding.json): ${expected}`);
222
+ console.error(` Your session is signed in to: ${actual}`);
223
+ console.error('');
224
+ console.error(' Sign in to THIS instance, then re-run your command:');
225
+ console.error(` bongos login ${expected}`);
226
+ console.error(` (or, from a checkout without the bongos bin: node scripts/gds/login.js ${expected})`);
227
+ console.error('');
228
+ console.error(' Intentionally targeting another instance? Set the base explicitly, e.g.');
229
+ console.error(` ${pfx}_API_BASE=${actual} <your command>`);
230
+ console.error('');
231
+ process.exit(3); // 3 = instance mismatch (distinct from 2 = unauth, 10 = box-scope)
232
+ }
233
+
234
+ // Fail-closed instance guard for RAW-fetch READ commands (task 2124 / demo-audit C2).
235
+ // apiCall + cliClient guard themselves, but a command that reaches the API through its
236
+ // OWN fetch bypasses that — the 2026-07-08 demo hit exactly this: `bongos status` used a
237
+ // raw fetch(API_BASE/…) and happily printed the MAIN instance's progress while sitting in
238
+ // the demo repo, giving false confidence, while the write path correctly refused. Such a
239
+ // command must call this first. Inert when there is no session, when API_BASE is overridden,
240
+ // or when the session's instance matches this repo's branding — it only fires on a genuine
241
+ // wrong-instance session. Reuses the same detector + message as the write-side guard.
242
+ async function assertInstanceMatch({ allowInstanceMismatch = false } = {}) {
243
+ if (allowInstanceMismatch || _instanceMismatchExitInFlight) return;
244
+ const session = await loadSession();
245
+ const mm = detectInstanceMismatch({ session });
246
+ if (mm.mismatch) {
247
+ _instanceMismatchExitInFlight = true;
248
+ printInstanceMismatchAndExit(mm);
249
+ }
250
+ }
251
+
169
252
  /**
170
253
  * Make an authenticated request to the GDS API.
171
254
  *
@@ -175,8 +258,20 @@ function printBoxScopeGuidanceAndExit() {
175
258
  * decide whether to re-auth automatically).
176
259
  */
177
260
  async function apiCall(method, urlPath, body = null, opts = {}) {
178
- const { allowUnauthenticated = false, allowBoxScope = false } = opts;
261
+ const { allowUnauthenticated = false, allowBoxScope = false, allowInstanceMismatch = false } = opts;
179
262
  const session = await loadSession();
263
+ // Fail CLOSED (task 2102) if the session we'd authenticate with belongs to a
264
+ // different instance than this repo is configured for — never silently borrow
265
+ // another instance's credentials. Opt out with { allowInstanceMismatch: true }
266
+ // for a deliberate cross-instance caller (login.js talks to other instances via
267
+ // its own fetch, so it never reaches here).
268
+ if (!allowInstanceMismatch && !_instanceMismatchExitInFlight) {
269
+ const mm = detectInstanceMismatch({ session });
270
+ if (mm.mismatch) {
271
+ _instanceMismatchExitInFlight = true;
272
+ printInstanceMismatchAndExit(mm);
273
+ }
274
+ }
180
275
  // Instance-agnostic base (V4.R83 / task 1250): `bongos login <instance>` writes
181
276
  // that instance's origin to session.api_base, so every later command targets it.
182
277
  // Precedence mirrors readSessionToken: an explicit <PREFIX>_API_BASE env wins
@@ -240,8 +335,20 @@ async function apiCall(method, urlPath, body = null, opts = {}) {
240
335
  let _clientModPromise = null;
241
336
  const _clientCache = new Map();
242
337
  async function cliClient(opts = {}) {
243
- const { allowUnauthenticated = false, allowBoxScope = false } = opts;
338
+ const { allowUnauthenticated = false, allowBoxScope = false, allowInstanceMismatch = false } = opts;
244
339
  const session = await loadSession();
340
+ // Fail CLOSED (task 2102) — the same guard as apiCall, on the OTHER HTTP entry
341
+ // point. cliClient() is the typed client (api.js + newer scripts route through
342
+ // it), and it resolves the base identically, so it can silently target the wrong
343
+ // instance too. Guard both, or the hole just moves. Opt out with
344
+ // { allowInstanceMismatch: true } for a deliberate cross-instance caller.
345
+ if (!allowInstanceMismatch && !_instanceMismatchExitInFlight) {
346
+ const mm = detectInstanceMismatch({ session });
347
+ if (mm.mismatch) {
348
+ _instanceMismatchExitInFlight = true;
349
+ printInstanceMismatchAndExit(mm);
350
+ }
351
+ }
245
352
  const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || 'https://amazonprimea.com';
246
353
  const token = (session && session.token) || null;
247
354
  const key = `${base}|${token || ''}|${allowUnauthenticated}|${allowBoxScope}`;
@@ -506,6 +613,9 @@ module.exports = {
506
613
  printUnauthGuidanceAndExit,
507
614
  isBoxScope,
508
615
  printBoxScopeGuidanceAndExit,
616
+ detectInstanceMismatch,
617
+ assertInstanceMatch,
618
+ instanceOriginHost,
509
619
  fmtMinutes,
510
620
  fmtUsd,
511
621
  fmtTouches,