@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.
- package/.bongos-core.json +363 -312
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/CLAUDE.md +11 -238
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +4 -4
- package/clients/bongos-client/bongos-client.global.js +14 -1
- package/clients/bongos-client/index.cjs +14 -1
- package/clients/bongos-client/index.d.ts +19 -2
- package/clients/bongos-client/index.mjs +11 -1
- package/clients/bongos-client/package.json +1 -1
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
- package/docs/adr/0134-private-first-npm-distribution.md +20 -6
- package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +14 -10
- package/docs/api/openapi.json +315 -6
- package/docs/api-reference.md +10 -5
- package/docs/architecture.md +3 -3
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +20 -19
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +8 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/instance-deploy.md +4 -4
- package/docs/recipes/instance-first-run.md +51 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/packaging-the-core.md +5 -5
- package/docs/recipes/private-npm-distribution.md +39 -12
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +63 -8
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/migrations/core_191_module_submissions.sql +45 -0
- package/modules/autonomy/CLAUDE.md +11 -45
- package/modules/builder-settings/CLAUDE.md +11 -54
- package/modules/dev-box/CLAUDE.md +11 -34
- package/modules/discord/CLAUDE.md +11 -46
- package/modules/discord/module.json +2 -1
- package/modules/economy/CLAUDE.md +11 -44
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/grading/CLAUDE.md +11 -38
- package/modules/hall-ui/CLAUDE.md +11 -57
- package/modules/hall-ui/public/builders.js +71 -4
- package/modules/hall-ui/public/goal-inbox.js +191 -0
- package/modules/hall-ui/public/goals-page.js +227 -22
- package/modules/hall-ui/public/goals.html +16 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -1
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/new-project-wizard.js +6 -4
- package/modules/hall-ui/public/style.css +43 -0
- package/modules/ideas/CLAUDE.md +11 -33
- package/modules/lifecycle/CLAUDE.md +11 -59
- package/modules/lifecycle/routes/goals.js +80 -0
- package/modules/memory/CLAUDE.md +11 -36
- package/modules/onboarding/CLAUDE.md +11 -51
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/CLAUDE.md +11 -40
- package/modules/security/secret-scrub.js +10 -0
- package/modules/sessions/CLAUDE.md +11 -35
- package/modules/status-ui/CLAUDE.md +11 -43
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/public-landing/create.html +58 -12
- package/public-landing/index.html +40 -21
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/CLAUDE.md +11 -140
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +174 -20
- package/scripts/gds/claude-md-template.js +84 -0
- package/scripts/gds/cli-lib.js +112 -2
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/gen-api-client.js +10 -10
- package/scripts/gds/init.js +112 -16
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/migration-namespace.js +1 -1
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/module.js +118 -4
- package/scripts/gds/npmrc.js +93 -0
- package/scripts/gds/onboard.js +17 -24
- package/scripts/gds/package-core.js +74 -17
- package/scripts/gds/provision.js +102 -17
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/seed-adopt-tasks.js +1 -1
- package/scripts/gds/status.js +6 -1
- package/scripts/gds/task.js +166 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +172 -25
- package/src/bongos/CLAUDE.md +11 -60
- package/src/bongos/auth.js +24 -0
- package/src/bongos/module-submissions.js +56 -0
- package/src/bongos/route-rank-check.js +6 -0
- package/src/bongos/routes/CLAUDE.md +11 -39
- package/src/bongos/routes/auth.js +6 -1
- package/src/bongos/routes/modules.js +123 -21
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/instance-config.js +1 -1
- package/src/module-api.js +24 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +125 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +96 -0
- package/tests/first_run_engine_check.mjs +79 -0
- package/tests/goal_membership_requests.mjs +33 -3
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/hook_user_agents.mjs +62 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +106 -6
- package/tests/instance_mismatch_guard.mjs +101 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_cli.mjs +130 -2
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/module_submissions.mjs +135 -0
- package/tests/npmrc.mjs +105 -0
- package/tests/onboard.mjs +62 -0
- package/tests/package_core.mjs +76 -22
- package/tests/platform_boot.mjs +5 -2
- package/tests/provision.mjs +107 -8
- package/tests/provisioning_github_app_routes.mjs +44 -2
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/served_paths_shipped.mjs +2 -2
- package/tests/task_cli.mjs +99 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +220 -25
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
|
@@ -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/@
|
|
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
|
-
//
|
|
19
|
-
//
|
|
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
|
-
|
|
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 (
|
|
90
|
-
fs.writeFileSync(absDest,
|
|
180
|
+
if (textExt.has(path.extname(rel))) {
|
|
181
|
+
fs.writeFileSync(absDest, transform(fs.readFileSync(absSrc, 'utf8')));
|
|
91
182
|
} else {
|
|
92
|
-
fs.copyFileSync(absSrc, absDest); //
|
|
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
|
-
//
|
|
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'), {
|
|
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(`
|
|
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
|
|
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
|
|
164
|
-
|
|
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
|
|
171
|
-
summary.notes.push('hooks
|
|
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 };
|
package/scripts/gds/cli-lib.js
CHANGED
|
@@ -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,
|