@bongos/core 1.17.0 → 1.17.1

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 (90) hide show
  1. package/.bongos-core.json +154 -83
  2. package/CLAUDE.md +11 -238
  3. package/bin/bongos.js +1 -0
  4. package/clients/bongos-client/README.md +4 -4
  5. package/clients/bongos-client/bongos-client.global.js +14 -1
  6. package/clients/bongos-client/index.cjs +14 -1
  7. package/clients/bongos-client/index.d.ts +19 -2
  8. package/clients/bongos-client/index.mjs +11 -1
  9. package/clients/bongos-client/package.json +1 -1
  10. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
  11. package/docs/adr/0134-private-first-npm-distribution.md +20 -6
  12. package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
  13. package/docs/adr/README.md +5 -4
  14. package/docs/api/openapi.json +315 -6
  15. package/docs/api-reference.md +10 -5
  16. package/docs/architecture.md +1 -1
  17. package/docs/file-map.md +2 -2
  18. package/docs/modules-contract.md +2 -0
  19. package/docs/recipes/instance-deploy.md +4 -4
  20. package/docs/recipes/instance-first-run.md +51 -0
  21. package/docs/recipes/packaging-the-core.md +5 -5
  22. package/docs/recipes/private-npm-distribution.md +22 -12
  23. package/docs/recipes/self-host.md +1 -1
  24. package/docs/recipes/upgrading-the-core.md +8 -8
  25. package/migrations/core_191_module_submissions.sql +45 -0
  26. package/modules/autonomy/CLAUDE.md +11 -45
  27. package/modules/builder-settings/CLAUDE.md +11 -54
  28. package/modules/dev-box/CLAUDE.md +11 -34
  29. package/modules/discord/CLAUDE.md +11 -46
  30. package/modules/economy/CLAUDE.md +11 -44
  31. package/modules/grading/CLAUDE.md +11 -38
  32. package/modules/hall-ui/CLAUDE.md +11 -57
  33. package/modules/hall-ui/public/goals-page.js +227 -22
  34. package/modules/hall-ui/public/goals.html +16 -0
  35. package/modules/hall-ui/public/new-project-wizard.js +5 -4
  36. package/modules/ideas/CLAUDE.md +11 -33
  37. package/modules/lifecycle/CLAUDE.md +11 -59
  38. package/modules/lifecycle/routes/goals.js +80 -0
  39. package/modules/memory/CLAUDE.md +11 -36
  40. package/modules/onboarding/CLAUDE.md +11 -51
  41. package/modules/security/CLAUDE.md +11 -40
  42. package/modules/sessions/CLAUDE.md +11 -35
  43. package/modules/status-ui/CLAUDE.md +11 -43
  44. package/package-lock.json +2 -2
  45. package/package.json +1 -1
  46. package/public-landing/create.html +58 -12
  47. package/public-landing/index.html +40 -21
  48. package/scripts/gds/CLAUDE.md +11 -140
  49. package/scripts/gds/claude-materialize.js +151 -18
  50. package/scripts/gds/claude-md-template.js +84 -0
  51. package/scripts/gds/cli-lib.js +112 -2
  52. package/scripts/gds/gen-api-client.js +10 -10
  53. package/scripts/gds/init.js +53 -13
  54. package/scripts/gds/migration-namespace.js +1 -1
  55. package/scripts/gds/module.js +118 -4
  56. package/scripts/gds/npmrc.js +93 -0
  57. package/scripts/gds/onboard.js +13 -24
  58. package/scripts/gds/package-core.js +56 -14
  59. package/scripts/gds/provision.js +102 -17
  60. package/scripts/gds/seed-adopt-tasks.js +1 -1
  61. package/scripts/gds/status.js +6 -1
  62. package/scripts/gds/task.js +166 -0
  63. package/scripts/gds/upgrade.js +58 -18
  64. package/src/bongos/CLAUDE.md +11 -60
  65. package/src/bongos/auth.js +11 -0
  66. package/src/bongos/module-submissions.js +56 -0
  67. package/src/bongos/route-rank-check.js +6 -0
  68. package/src/bongos/routes/CLAUDE.md +11 -39
  69. package/src/bongos/routes/auth.js +6 -1
  70. package/src/bongos/routes/modules.js +123 -21
  71. package/src/instance-config.js +1 -1
  72. package/src/module-api.js +13 -1
  73. package/tests/claude_materialize.mjs +105 -0
  74. package/tests/device_flow_start.mjs +73 -0
  75. package/tests/first_run_engine_check.mjs +79 -0
  76. package/tests/goal_membership_requests.mjs +33 -3
  77. package/tests/hook_user_agents.mjs +62 -0
  78. package/tests/init.mjs +59 -6
  79. package/tests/instance_mismatch_guard.mjs +101 -0
  80. package/tests/module_cli.mjs +130 -2
  81. package/tests/module_submissions.mjs +135 -0
  82. package/tests/npmrc.mjs +105 -0
  83. package/tests/onboard.mjs +49 -0
  84. package/tests/package_core.mjs +68 -22
  85. package/tests/platform_boot.mjs +5 -2
  86. package/tests/provision.mjs +107 -8
  87. package/tests/provisioning_github_app_routes.mjs +29 -2
  88. package/tests/served_paths_shipped.mjs +2 -2
  89. package/tests/task_cli.mjs +99 -0
  90. package/tests/upgrade.mjs +89 -25
@@ -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 —
@@ -25,6 +25,8 @@
25
25
  const fs = require('node:fs');
26
26
  const path = require('node:path');
27
27
  const { VERBS } = require('../../bin/bongos.js'); // side-effect-free require (guarded main)
28
+ const ic = require('../../src/instance-config'); // neutral configDir/envPrefix fallbacks (never a founder literal)
29
+ const { renderInstanceClaudeMd } = require('./claude-md-template'); // task 2104 — the starter CLAUDE.md
28
30
 
29
31
  // script basename (e.g. "ship.js") → dedicated bongos verb, for verbs that map to a bare script with
30
32
  // NO prefixArgs. Verbs whose script is driven by prefixArgs (box → api.js) are EXCLUDED so a generic
@@ -59,6 +61,88 @@ function bongosify(text, verbs = VERBS) {
59
61
  .replace(OTHER_SCRIPT_RE, (_whole, rel) => `bongos exec scripts/${rel}`);
60
62
  }
61
63
 
64
+ // ---- instance rebrand (task 2103) -----------------------------------------
65
+ // The scaffolded .claude/ used to carry the SOURCE instance's identity verbatim:
66
+ // hooks hardcoded the founder origin (e.g. a `https://amazonprimea.com` fallback)
67
+ // and config dir (`~/.config/otb`), so a fresh clone's hooks phoned home to the
68
+ // WRONG instance — the 2026-07-08 demo-session failure. materialize now rewrites
69
+ // those founder literals to the TARGET instance's own values, derived from the two
70
+ // branding objects, so no source host survives. Focused + FULL-HOST (a subdomain
71
+ // like demo.cloudbongos.com is preserved), fail-open (a missing field drops that
72
+ // one rule), a no-op when source == target, and it touches ONLY the copied output
73
+ // (never the running source hooks). This does NOT rewrite hooks' internal
74
+ // require('scripts/gds/…') paths for a package-only install — that stays the W3
75
+ // follow-up; this lands the origin/config-dir neutralization.
76
+
77
+ function reEsc(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
78
+ function hostOf(origin) { try { return new URL(String(origin)).host || null; } catch { return null; } }
79
+
80
+ // Effective config dir / env prefix for a branding — the SAME neutral fallbacks
81
+ // instance-config uses, so nothing here is ever a founder literal (R76-safe).
82
+ function effConfigDir(b) { return (b && b.configDir) || ic.FALLBACK_DIR; }
83
+ function effEnvPrefix(b) { return (b && b.envPrefix) || ic.FALLBACK_ENV_PREFIX; }
84
+
85
+ // Ordered source→target rebrand rules from two branding objects. PURE + exported.
86
+ function buildRebrandRules(from = {}, to = {}) {
87
+ const rules = [];
88
+ const add = (a, b) => { if (a && b && a !== b) rules.push({ pattern: new RegExp(reEsc(a), 'g'), replace: b }); };
89
+ // Every configured origin HOST (public/builders/status/oauth) → the target's SAME-key
90
+ // host, or its publicOrigin host when that key is unset (so a source subdomain like
91
+ // builders.<founder> maps to the target's canonical origin, not builders.<target>).
92
+ // Full-host keeps real target subdomains; a bare founder host anywhere is caught too.
93
+ // Dedup by source host (first mapping wins) so one founder host can't spawn conflicting rules.
94
+ const fromD = from.domains || {}, toD = to.domains || {};
95
+ const toPublicHost = hostOf(toD.publicOrigin);
96
+ const hostMap = new Map();
97
+ for (const key of Object.keys(fromD)) {
98
+ const fh = hostOf(fromD[key]);
99
+ if (!fh || hostMap.has(fh)) continue;
100
+ const th = hostOf(toD[key]) || toPublicHost;
101
+ if (th) hostMap.set(fh, th);
102
+ }
103
+ for (const [fh, th] of hostMap) add(fh, th);
104
+ // Config dir in the shapes hooks reference it (~/.config/<dir>, /etc/<dir>, the [<dir>] log tag).
105
+ const fc = effConfigDir(from), tc = effConfigDir(to);
106
+ add(`.config/${fc}`, `.config/${tc}`);
107
+ add(`/etc/${fc}`, `/etc/${tc}`);
108
+ add(`[${fc}]`, `[${tc}]`);
109
+ // The instance env-var prefix (e.g. OTB_ → CLOUDBONGOS_), bounded by its underscore.
110
+ add(`${effEnvPrefix(from)}_`, `${effEnvPrefix(to)}_`);
111
+ // Longest literal first so a full host is replaced before any bare token inside it.
112
+ rules.sort((x, y) => y.pattern.source.length - x.pattern.source.length);
113
+ return rules;
114
+ }
115
+
116
+ function rebrandText(text, rules) {
117
+ if (typeof text !== 'string' || !text || !rules || !rules.length) return text;
118
+ let out = text;
119
+ for (const r of rules) out = out.replace(r.pattern, r.replace);
120
+ return out;
121
+ }
122
+
123
+ function readJsonSafe(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } }
124
+
125
+ // Skills owned by a module that is DISABLED in the TARGET instance (task 2103b).
126
+ // Each module.json declares `contributes.skills`; a module is enabled when the
127
+ // target's config/modules.json says so, else by its own `default`. A skill owned
128
+ // by NO module is core and is never skipped. PURE-ish (reads the fs) + exported.
129
+ function disabledModuleSkills({ coreRoot, instanceDir }) {
130
+ const skip = new Set();
131
+ const modulesDir = path.join(coreRoot, 'modules');
132
+ if (!fs.existsSync(modulesDir)) return skip;
133
+ const targetCfg = readJsonSafe(path.join(instanceDir, 'config', 'modules.json')) || {};
134
+ const enabledMap = (targetCfg.modules && typeof targetCfg.modules === 'object') ? targetCfg.modules : {};
135
+ for (const dir of fs.readdirSync(modulesDir)) {
136
+ const mj = readJsonSafe(path.join(modulesDir, dir, 'module.json'));
137
+ const skills = mj && mj.contributes && mj.contributes.skills;
138
+ if (!Array.isArray(skills)) continue;
139
+ const explicit = enabledMap[mj.key || dir];
140
+ const enabled = explicit === undefined ? mj.default === true : explicit === true;
141
+ if (!enabled) for (const s of skills) skip.add(s);
142
+ }
143
+ return skip;
144
+ }
145
+
62
146
  // ---- I/O: copy a tree, transforming text files through bongosify ----------
63
147
 
64
148
  const TEXT_EXT = new Set(['.md', '.txt', '.sh']); // command-bearing text we rewrite (skills are SKILL.md)
@@ -67,10 +151,16 @@ const TEXT_EXT = new Set(['.md', '.txt', '.sh']); // command-bearing text we rew
67
151
  // `overwrite: false` is the `bongos init --adopt` layering path (ADR 0121): a file
68
152
  // that already exists at the destination is NEVER clobbered — it is skipped and
69
153
  // recorded in the returned `skipped[]` so the caller can report it.
70
- function copyTreeTransformed(srcDir, destDir, { dryRun, onFile, overwrite = true } = {}) {
154
+ // `transform` (text→text) is applied to any file whose extension is in `textExt`
155
+ // (default: bongosify over .md/.txt/.sh — the historical skills behavior). `skip(rel)`
156
+ // excludes a whole subtree BEFORE it is walked (task 2103b — drop a disabled module's
157
+ // skill dir); excluded top-level entries are returned in `excluded[]`.
158
+ function copyTreeTransformed(srcDir, destDir, { dryRun, onFile, overwrite = true, transform = bongosify, textExt = TEXT_EXT, skip } = {}) {
71
159
  let files = 0;
72
160
  const skipped = [];
161
+ const excluded = [];
73
162
  const walk = (rel) => {
163
+ if (skip && skip(rel)) { excluded.push(rel); return; }
74
164
  const absSrc = path.join(srcDir, rel);
75
165
  const st = fs.statSync(absSrc);
76
166
  if (st.isDirectory()) {
@@ -86,16 +176,16 @@ function copyTreeTransformed(srcDir, destDir, { dryRun, onFile, overwrite = true
86
176
  files += 1;
87
177
  if (dryRun) { if (onFile) onFile(rel); return; }
88
178
  fs.mkdirSync(path.dirname(absDest), { recursive: true });
89
- if (TEXT_EXT.has(path.extname(rel))) {
90
- fs.writeFileSync(absDest, bongosify(fs.readFileSync(absSrc, 'utf8')));
179
+ if (textExt.has(path.extname(rel))) {
180
+ fs.writeFileSync(absDest, transform(fs.readFileSync(absSrc, 'utf8')));
91
181
  } else {
92
- fs.copyFileSync(absSrc, absDest); // .js / binary / json (settings handled separately) — verbatim
182
+ fs.copyFileSync(absSrc, absDest); // binary / other — verbatim
93
183
  }
94
184
  if (onFile) onFile(rel);
95
185
  };
96
- if (!fs.existsSync(srcDir)) return { files: 0, skipped, present: false };
186
+ if (!fs.existsSync(srcDir)) return { files: 0, skipped, excluded, present: false };
97
187
  for (const name of fs.readdirSync(srcDir)) walk(name);
98
- return { files, skipped, present: true };
188
+ return { files, skipped, excluded, present: true };
99
189
  }
100
190
 
101
191
  // Transform settings.json: parse, rewrite every hook command through bongosify, re-serialize. Keeps
@@ -121,8 +211,10 @@ function transformSettings(srcPath) {
121
211
  // `overwrite` defaults true (greenfield init / upgrade — unchanged). `overwrite: false`
122
212
  // is the `bongos init --adopt` layering path (ADR 0121): every pre-existing .claude/
123
213
  // file is LEFT untouched and recorded in `summary.skippedFiles` (never clobbered).
214
+ const HOOK_TEXT_EXT = new Set(['.js', '.mjs', '.cjs', '.json', '.sh', '.md', '.txt']); // hooks are .js — rebrand them too
215
+
124
216
  function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite = true, log = () => {} }) {
125
- const summary = { skipped: false, skills: 0, settings: false, hooks: 0, skippedFiles: [], notes: [] };
217
+ const summary = { skipped: false, skills: 0, settings: false, hooks: 0, skippedFiles: [], excludedSkills: [], rebrandRules: 0, claudeMd: false, notes: [] };
126
218
  if (path.resolve(instanceDir) === path.resolve(coreRoot)) {
127
219
  summary.skipped = true;
128
220
  summary.notes.push('instance dir is the core root — nothing to materialize (byte-identical no-op).');
@@ -133,13 +225,33 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
133
225
  const destClaude = path.join(instanceDir, '.claude');
134
226
  const left = (n) => (overwrite || n === 0 ? '' : ` (${n} left untouched)`);
135
227
 
136
- // 1. skills — ship in the core package; copy + rewrite invocations.
228
+ // Rebrand: rewrite the SOURCE instance's identity (origin host, config dir, env
229
+ // prefix) to the TARGET's, so a copied hook never phones home to the founder's
230
+ // instance (task 2103a). Derived from both config/branding.json files; empty — and
231
+ // thus a no-op — when they match or either is unreadable.
232
+ const fromBranding = readJsonSafe(path.join(coreRoot, 'config', 'branding.json')) || {};
233
+ const toBranding = readJsonSafe(path.join(instanceDir, 'config', 'branding.json')) || {};
234
+ const rebrandRules = buildRebrandRules(fromBranding, toBranding);
235
+ const rebrand = (t) => rebrandText(t, rebrandRules);
236
+ summary.rebrandRules = rebrandRules.length;
237
+ if (rebrandRules.length) log(` • rebranding ${rebrandRules.length} founder literal(s) in the copied .claude/ → this instance's identity.`);
238
+
239
+ // Drop skills owned by a module this instance hasn't enabled (task 2103b).
240
+ const skipSkills = disabledModuleSkills({ coreRoot, instanceDir });
241
+ const skillSkip = (rel) => skipSkills.has(String(rel).split(/[\\/]/)[0]);
242
+
243
+ // 1. skills — ship in the core package; copy, rewrite invocations→bongos, rebrand
244
+ // identity, and exclude any skill owned by a disabled module.
137
245
  const skillsSrc = path.join(srcClaude, 'skills');
138
- const skillsRes = copyTreeTransformed(skillsSrc, path.join(destClaude, 'skills'), { dryRun, overwrite });
246
+ const skillsRes = copyTreeTransformed(skillsSrc, path.join(destClaude, 'skills'), {
247
+ dryRun, overwrite, transform: (t) => rebrand(bongosify(t)), skip: skillSkip,
248
+ });
139
249
  summary.skills = skillsRes.files;
250
+ summary.excludedSkills = skillsRes.excluded || [];
140
251
  for (const rel of skillsRes.skipped || []) summary.skippedFiles.push(`skills/${rel}`);
141
252
  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)}`);
253
+ if (summary.excludedSkills.length) log(` • excluded ${summary.excludedSkills.length} skill(s) owned by disabled module(s): ${summary.excludedSkills.join(', ')}`);
254
+ log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${skillsRes.files} skill file(s) → .claude/skills/ (invocations→bongos, identity rebranded)${left((skillsRes.skipped || []).length)}`);
143
255
 
144
256
  // 2. settings.json — topology; present in the checkout/core repo, absent in a package-only install.
145
257
  const settingsSrc = path.join(srcClaude, 'settings.json');
@@ -151,26 +263,47 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
151
263
  } else {
152
264
  if (!dryRun) {
153
265
  fs.mkdirSync(destClaude, { recursive: true });
154
- fs.writeFileSync(settingsDest, transformSettings(settingsSrc));
266
+ fs.writeFileSync(settingsDest, rebrand(transformSettings(settingsSrc)));
155
267
  }
156
268
  summary.settings = true;
157
- log(` ${dryRun ? '[dry-run] would write' : '✓ wrote'} .claude/settings.json (hook commands rewritten to bongos)`);
269
+ log(` ${dryRun ? '[dry-run] would write' : '✓ wrote'} .claude/settings.json (hook commands→bongos, identity rebranded)`);
158
270
  }
159
271
  } else {
160
272
  summary.notes.push('.claude/settings.json not present at the core root (topology, excluded from the package) — provide it from a starter.');
161
273
  }
162
274
 
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 });
275
+ // 3. hooks — topology; copied WITH an identity rebrand (their .js internals carried
276
+ // the founder origin/config-dir). require('scripts/gds/…') path rewriting for a
277
+ // package-only install stays the W3 follow-up.
278
+ const hooksRes = copyTreeTransformed(path.join(srcClaude, 'hooks'), path.join(destClaude, 'hooks'), {
279
+ dryRun, overwrite, transform: rebrand, textExt: HOOK_TEXT_EXT,
280
+ });
165
281
  summary.hooks = hooksRes.files;
166
282
  for (const rel of hooksRes.skipped || []) summary.skippedFiles.push(`hooks/${rel}`);
167
283
  if (!hooksRes.present) {
168
284
  summary.notes.push('.claude/hooks not present at the core root (topology, excluded from the package) — provide them from a starter.');
169
285
  } 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).');
286
+ log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${hooksRes.files} hook file(s) → .claude/hooks/ (founder identity rebranded)${left((hooksRes.skipped || []).length)}`);
287
+ summary.notes.push('hooks: founder origin/config-dir rebranded to this instance; their internal `require(scripts/gds/…)` paths are NOT yet rewritten to resolve inside the package (W3 follow-up).');
288
+ }
289
+
290
+ // 4. CLAUDE.md — the project-root framing an agent reads every session (task 2104).
291
+ // Written from the neutral starter template, brand-substituted from THIS instance's
292
+ // branding. Written only when ABSENT — never clobber a CLAUDE.md the instance has
293
+ // grown (safe for init, adopt, and upgrade alike; the self-copy guard above already
294
+ // keeps this from ever touching the core/OTB checkout's own CLAUDE.md).
295
+ const claudeMdPath = path.join(instanceDir, 'CLAUDE.md');
296
+ if (fs.existsSync(claudeMdPath)) {
297
+ summary.notes.push('CLAUDE.md already present — left untouched (the instance owns it).');
298
+ } else {
299
+ if (!dryRun) {
300
+ fs.mkdirSync(instanceDir, { recursive: true });
301
+ fs.writeFileSync(claudeMdPath, renderInstanceClaudeMd(toBranding));
302
+ }
303
+ summary.claudeMd = true;
304
+ log(` ${dryRun ? '[dry-run] would write' : '✓ wrote'} CLAUDE.md (starter, brand-substituted for this instance)`);
172
305
  }
173
306
  return summary;
174
307
  }
175
308
 
176
- module.exports = { scriptToVerb, bongosify, transformSettings, copyTreeTransformed, materializeClaude, GDS_INVOCATION_RE };
309
+ 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,
@@ -10,7 +10,7 @@
10
10
  //
11
11
  // Design (mirrors gen-api-docs.js / ADR 0063): deterministic (stable sort, no
12
12
  // clock), zero runtime deps, --check gate. The client itself has ZERO deps — it
13
- // uses the platform `fetch` (Node 18+ / browsers), so `npm i @cloudbongos/client`
13
+ // uses the platform `fetch` (Node 18+ / browsers), so `npm i @bongos/client`
14
14
  // pulls nothing transitive.
15
15
  //
16
16
  // Usage:
@@ -92,7 +92,7 @@ function renderClientJs(spec, ops) {
92
92
  const groups = groupByTag(ops);
93
93
  const defaultBase = (spec.servers && spec.servers[0] && spec.servers[0].url) || '/api/bongos/v1';
94
94
  const L = [];
95
- L.push(`// @cloudbongos/client — GENERATED from the OpenAPI spec by`);
95
+ L.push(`// @bongos/client — GENERATED from the OpenAPI spec by`);
96
96
  L.push(`// scripts/gds/gen-api-client.js (task 1995 / ADR 0118). DO NOT EDIT BY HAND —`);
97
97
  L.push(`// regenerate on spec change. Zero deps; uses the platform fetch.`);
98
98
  L.push(`'use strict';`);
@@ -210,7 +210,7 @@ function stripEsmExports(esm) {
210
210
  // app, which is a zero-dep CJS subproject and vendors this file at package time).
211
211
  function renderClientCjs(esm) {
212
212
  const header =
213
- '// @cloudbongos/client (CommonJS build) — GENERATED from index.mjs by\n' +
213
+ '// @bongos/client (CommonJS build) — GENERATED from index.mjs by\n' +
214
214
  '// scripts/gds/gen-api-client.js. DO NOT EDIT BY HAND. Same behavior as the ESM\n' +
215
215
  '// build; only the module bindings differ. For require()-based consumers.\n';
216
216
  const body = stripEsmExports(esm)
@@ -232,7 +232,7 @@ function renderClientBrowserGlobal(esm) {
232
232
  .replace(/^'use strict';\n/m, '')
233
233
  .replace(/^\/\/ @cloudbongos\/client — GENERATED[^\n]*\n(?:\/\/[^\n]*\n)*/m, '');
234
234
  return (
235
- '// @cloudbongos/client (browser-global build) — GENERATED from index.mjs by\n' +
235
+ '// @bongos/client (browser-global build) — GENERATED from index.mjs by\n' +
236
236
  '// scripts/gds/gen-api-client.js. DO NOT EDIT BY HAND. Exposes window.BongosClient\n' +
237
237
  '// for classic <script> pages. Same behavior as the ESM build.\n' +
238
238
  '(function (global) {\n' +
@@ -316,7 +316,7 @@ function methodSignature(op) {
316
316
  function renderTypes(spec, ops) {
317
317
  const groups = groupByTag(ops);
318
318
  const L = [];
319
- L.push(`// GENERATED — TypeScript types for @cloudbongos/client (task 1995; field-level`);
319
+ L.push(`// GENERATED — TypeScript types for @bongos/client (task 1995; field-level`);
320
320
  L.push(`// request typing added in task 2050 / ADR 0118). DO NOT EDIT BY HAND.`);
321
321
  L.push(`export declare const API_VERSION: string;`);
322
322
  L.push(`export declare const DEFAULT_BASE_URL: string;`);
@@ -352,7 +352,7 @@ function renderTypes(spec, ops) {
352
352
 
353
353
  function renderPackageJson(spec) {
354
354
  return JSON.stringify({
355
- name: '@cloudbongos/client',
355
+ name: '@bongos/client',
356
356
  version: spec.info && spec.info.version ? spec.info.version : '0.0.0',
357
357
  description: 'Generated typed client for the Bongos API (ADR 0118). Do not edit by hand.',
358
358
  type: 'module',
@@ -374,7 +374,7 @@ function renderPackageJson(spec) {
374
374
  license: 'AGPL-3.0-or-later',
375
375
  // Pre-launch: publish PRIVATE (task 2089 / ADR 0134). Scoped + access:"restricted"
376
376
  // = only paid-org members can install. Flip to "public" (or `npm access public
377
- // @cloudbongos/client`) at launch, alongside the core.
377
+ // @bongos/client`) at launch, alongside the core.
378
378
  publishConfig: { access: 'restricted' },
379
379
  sideEffects: false,
380
380
  }, null, 2) + '\n';
@@ -418,7 +418,7 @@ function exampleLines(ops) {
418
418
  function renderReadme(spec, ops) {
419
419
  const base = (spec.servers && spec.servers[0] && spec.servers[0].url) || '/api/bongos/v1';
420
420
  return [
421
- `# @cloudbongos/client`,
421
+ `# @bongos/client`,
422
422
  ``,
423
423
  `A **generated**, zero-dependency typed client for the Bongos API — produced from`,
424
424
  `\`docs/api/openapi.json\` by \`scripts/gds/gen-api-client.js\` (ADR 0118). Do not edit`,
@@ -430,13 +430,13 @@ function renderReadme(spec, ops) {
430
430
  `## Install`,
431
431
  ``,
432
432
  '```bash',
433
- `npm install @cloudbongos/client # internal registry / workspace path`,
433
+ `npm install @bongos/client # internal registry / workspace path`,
434
434
  '```',
435
435
  ``,
436
436
  `## Use`,
437
437
  ``,
438
438
  '```js',
439
- `import { createClient, ApiError } from '@cloudbongos/client';`,
439
+ `import { createClient, ApiError } from '@bongos/client';`,
440
440
  ``,
441
441
  `const api = createClient({`,
442
442
  ` baseUrl: 'https://amazonprimea.com${base}', // or omit for same-origin (browser)`,