@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
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // pre-flight clean git tree + module-compat vs the TARGET core version
9
9
  // → bump pin vendor the new core .tgz, rewrite the package.json `file:` dependency
10
- // → npm install node_modules/@cloudbongos/core becomes the new version
10
+ // → npm install node_modules/@bongos/core becomes the new version
11
11
  // → migrate `npm run migrate` → the core's migrate.sh applies additive `core_*` migrations
12
12
  // → materialize refresh .claude/ skills+settings+hooks from the new core (claude-materialize / W3)
13
13
  // → restart restart the instance's service so the new core is live
@@ -16,7 +16,7 @@
16
16
  // SCOPE (ADR 0100 §2, reshaped by ADR 0103 §4): the SMALLEST channel the FIRST real consumer needs
17
17
  // on its first core bump — driven by that consumer, not designed up front. That consumer is
18
18
  // emersonian-circles (task 1917): a greenfield npm-dependency instance that pins the core as
19
- // "@cloudbongos/core": "file:vendor/cloudbongos-core-<ver>.tgz"
19
+ // "@bongos/core": "file:vendor/bongos-core-<ver>.tgz"
20
20
  // with `start`/`migrate` npm scripts and a materialized .claude/. Every step below maps to something
21
21
  // that consumer actually has. A published-registry source, rollback, and multi-consumer shapes are
22
22
  // deliberately OUT — they are follow-ups a second consumer's needs will define.
@@ -30,9 +30,10 @@ const fs = require('node:fs');
30
30
  const path = require('node:path');
31
31
  const { spawnSync } = require('node:child_process');
32
32
  const { arg, hasFlag } = require('./cli-lib');
33
+ const { writeNpmrc, hasNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the --registry channel's env-fed .npmrc
33
34
 
34
- const CORE_PKG = '@cloudbongos/core';
35
- const ARTIFACT = 'cloudbongos-core';
35
+ const CORE_PKG = '@bongos/core';
36
+ const ARTIFACT = 'bongos-core';
36
37
 
37
38
  // ---- pure helpers (no I/O side effects beyond the injected fs) --------------
38
39
 
@@ -44,6 +45,16 @@ function versionFromTgzPath(p) {
44
45
  return m ? m[1] : null;
45
46
  }
46
47
 
48
+ // The version from a REGISTRY dep spec — a bare semver, optionally range-prefixed
49
+ // (`1.17.0`, `^1.17.0`, `~1.17.0`). Returns null for a `file:` tarball spec (that's
50
+ // versionFromTgzPath's job) so the two never both match. ADR 0134 (task 2090).
51
+ function versionFromDep(dep) {
52
+ const s = String(dep || '').trim();
53
+ if (!s || /^file:/.test(s)) return null;
54
+ const m = s.match(/^[\^~>=<\s]*v?(\d+\.\d+\.\d+[A-Za-z0-9.+-]*)$/);
55
+ return m ? m[1] : null;
56
+ }
57
+
47
58
  function readJson(file, fsImpl = fs) {
48
59
  try { return JSON.parse(fsImpl.readFileSync(file, 'utf8')); } catch { return null; }
49
60
  }
@@ -54,11 +65,13 @@ function readInstalledCoreVersion(instanceDir, fsImpl = fs) {
54
65
  return pkg && pkg.version ? pkg.version : null;
55
66
  }
56
67
 
57
- // The version the consumer's package.json currently pins (from the `file:` tarball path).
68
+ // The version the consumer's package.json currently pins — from either the `file:`
69
+ // tarball path (vendored) or a bare registry semver (ADR 0134). A best-effort "from"
70
+ // hint; readInstalledCoreVersion (node_modules) is the authoritative source.
58
71
  function readPinnedCoreVersion(instanceDir, fsImpl = fs) {
59
72
  const pkg = readJson(path.join(instanceDir, 'package.json'), fsImpl);
60
73
  const dep = pkg && ((pkg.dependencies || {})[CORE_PKG] || (pkg.devDependencies || {})[CORE_PKG]);
61
- return versionFromTgzPath(dep);
74
+ return versionFromTgzPath(dep) || versionFromDep(dep);
62
75
  }
63
76
 
64
77
  // Rewrite the consumer package.json to pin `file:<vendorRel>`. Returns the previous dep string.
@@ -73,6 +86,35 @@ function writePin(instanceDir, vendorRel, fsImpl = fs) {
73
86
  return prev;
74
87
  }
75
88
 
89
+ // Rewrite the consumer package.json to pin an EXACT registry version (ADR 0134, task
90
+ // 2090): the registry sibling of writePin. `npm install` then resolves @bongos/core@version
91
+ // from the registry (auth via the env-fed .npmrc). Returns the previous dep string.
92
+ function writeRegistryPin(instanceDir, version, fsImpl = fs) {
93
+ const pkgPath = path.join(instanceDir, 'package.json');
94
+ const pkg = readJson(pkgPath, fsImpl);
95
+ if (!pkg) throw new Error(`no readable package.json at ${pkgPath}`);
96
+ pkg.dependencies = pkg.dependencies || {};
97
+ const prev = pkg.dependencies[CORE_PKG];
98
+ pkg.dependencies[CORE_PKG] = version;
99
+ fsImpl.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
100
+ return prev;
101
+ }
102
+
103
+ // Restore the consumer package.json's @bongos/core pin to an EXACT previous dep string,
104
+ // VERBATIM — whether that was a `file:` tarball path (vendored) or a bare registry semver.
105
+ // The inverse of writePin/writeRegistryPin used by the auto-rollback path (task 2149), which
106
+ // snapshots the returned prev-dep before the bump. Returns the dep string it overwrote.
107
+ function restorePin(instanceDir, depString, fsImpl = fs) {
108
+ const pkgPath = path.join(instanceDir, 'package.json');
109
+ const pkg = readJson(pkgPath, fsImpl);
110
+ if (!pkg) throw new Error(`no readable package.json at ${pkgPath}`);
111
+ pkg.dependencies = pkg.dependencies || {};
112
+ const prev = pkg.dependencies[CORE_PKG];
113
+ pkg.dependencies[CORE_PKG] = depString;
114
+ fsImpl.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
115
+ return prev;
116
+ }
117
+
76
118
  // ---- steps (each injectable for tests) --------------------------------------
77
119
 
78
120
  function gitTreeClean(instanceDir, run = spawnSync) {
@@ -127,10 +169,11 @@ function runMigrate(instanceDir, run = spawnSync) {
127
169
 
128
170
  // Regenerate the per-instance API artifacts (OpenAPI spec + api-reference + typed
129
171
  // client) from the NEW core's routes — the claude-materialize sibling (task 2051).
130
- // WHY: the routes + generators ship WITH core, but the generated spec/docs/client are
131
- // EXCLUDED from the package (per-instance artifacts), so a bump changes the served API
132
- // while the instance's committed spec/client lag until the next ship's fitness gate.
133
- // This closes that gap on upgrade. We SPAWN the new core's generators (a fresh child
172
+ // WHY: the routes + generators ship WITH core, and the generated spec/docs/client also
173
+ // ship as CORE STARTERS (task 2078 — a fresh hall/docs must render before first regen),
174
+ // but they are per-instance GENERATED, so a bump changes the served API while the
175
+ // instance's committed spec/client lag until regenerated (the classification settled in
176
+ // task 2141; init does the same at standup). We SPAWN the new core's generators (a fresh child
134
177
  // captures its output root from the env below) with <FALLBACK_PREFIX>_INSTANCE_ROOT set
135
178
  // so resolveDocsRoot() writes INTO the instance repo, not node_modules. docs first — the
136
179
  // client reads the spec it produces. Best-effort: a hiccup warns, never fails the bump
@@ -252,6 +295,64 @@ async function pollHealth(healthUrl, deps = {}) {
252
295
  }
253
296
  }
254
297
 
298
+ // Auto-rollback (task 2149): a bump failed AFTER the pin was moved — an install or migrate
299
+ // error, or a failed post-restart health check. Revert the code pin to the pre-upgrade
300
+ // snapshot, reinstall the previous core, restart, re-verify health, and record the reversal
301
+ // in the ledger. The DB is deliberately NOT rolled back: core migrations are additive +
302
+ // forward-safe (the core migration rule), so any already-applied migration is left in place —
303
+ // only the code PIN moves back. Best-effort + fail-soft: every sub-step logs but never throws,
304
+ // so a rollback attempt can't itself crash the upgrader on top of the original failure.
305
+ async function rollback({ instanceDir, snapshot, fromVersion, toVersion, opts }, reason, deps = {}) {
306
+ const log = deps.log || console.log;
307
+ const err = deps.err || console.error;
308
+ const run = deps.run || spawnSync;
309
+ const fsImpl = deps.fs || fs;
310
+ const restore = deps.restorePin || restorePin;
311
+
312
+ err(` ↩ rolling back: ${reason} — reverting ${CORE_PKG} ${toVersion} → ${snapshot.prevVersion || snapshot.prevPin}`);
313
+ const result = { rolledBack: true, reason, restoredPin: snapshot.prevPin, restoredVersion: snapshot.prevVersion };
314
+
315
+ // 1. revert the pin to the exact previous dep string.
316
+ try { restore(instanceDir, snapshot.prevPin, fsImpl); result.restoreOk = true; log(` ✓ restored pin → ${snapshot.prevPin}`); }
317
+ catch (e) { result.restoreOk = false; err(` ! could not restore pin (${e.message}) — package.json may need manual repair`); }
318
+
319
+ // 2. reinstall the previous core.
320
+ const inst = npmInstall(instanceDir, run);
321
+ result.reinstallOk = !!inst.ok;
322
+ if (inst.ok) log(' ✓ reinstalled previous core');
323
+ else err(` ! reinstall of previous core failed (${inst.reason || 'exit ' + inst.code}) — the instance may need manual repair`);
324
+
325
+ // 3. restart back onto the previous core.
326
+ const rs = restartService({ service: opts.service, restartCmd: opts.restartCmd }, run);
327
+ if (rs.skipped) log(' • restart: no --service/--restart-cmd — restart the instance service manually to load the reverted core.');
328
+ else if (!rs.ok) err(` ! restart after rollback failed (exit ${rs.code}) — restart the service manually.`);
329
+ else log(` ✓ restarted ${opts.service || 'service'} on the previous core`);
330
+ result.restartOk = rs.ok;
331
+
332
+ // 4. re-verify: installed == previous, optional health recovery.
333
+ const installedNow = readInstalledCoreVersion(instanceDir, fsImpl);
334
+ result.installedAfter = installedNow;
335
+ log(` ${installedNow === snapshot.prevVersion ? '✓' : '!'} installed core after rollback: ${installedNow || '(unknown)'}`);
336
+ if (opts.healthUrl) {
337
+ const h = await pollHealth(opts.healthUrl, deps);
338
+ result.healthOk = h.ok;
339
+ if (h.ok) log(` ✓ health recovered ${opts.healthUrl} → ${h.status}${h.attempts > 1 ? ` (after ${h.attempts} tries)` : ''}`);
340
+ else err(` ! health STILL failing after rollback (${h.error || 'status ' + h.status}) — manual intervention needed`);
341
+ }
342
+
343
+ // 5. record the reversal in the ledger via the always-present `note` column (no schema
344
+ // dependency — so it records even when a FAILED migrate means a newer column wouldn't exist).
345
+ const note = `rolled_back: ${reason} (attempted ${fromVersion || '(none)'} → ${toVersion}, restored ${snapshot.prevVersion || 'previous'})`;
346
+ const ledger = await recordLedger({ fromVersion, toVersion, sourceCommit: opts.sourceCommit, note }, deps);
347
+ if (ledger.ok) log(' ✓ recorded rollback in core_upgrades ledger');
348
+ else if (ledger.skipped) log(` • rollback ledger skipped (${ledger.reason})`);
349
+ else err(` ! rollback ledger insert failed (${ledger.reason}) — record it manually`);
350
+ result.ledger = ledger.ok;
351
+
352
+ err(` ↩ rollback complete — instance restored to core ${snapshot.prevVersion || snapshot.prevPin}.`);
353
+ return result;
354
+ }
355
+
255
356
  // ---- orchestration ----------------------------------------------------------
256
357
 
257
358
  async function runUpgrade(opts, deps = {}) {
@@ -264,9 +365,16 @@ async function runUpgrade(opts, deps = {}) {
264
365
  const instanceDir = path.resolve(opts.instance || process.cwd());
265
366
  const targetVersion = opts.to;
266
367
  const dryRun = !!opts.dryRun;
368
+ // Auto-rollback is ON by default (task 2149); `--no-rollback-on-failure` opts out (e.g. an
369
+ // interactive run that wants to inspect a broken state). Programmatic callers pass false.
370
+ const rollbackOnFailure = opts.rollbackOnFailure !== false;
371
+ // Pre-upgrade snapshot { prevPin, prevVersion } — captured the instant the pin is moved, so a
372
+ // failure AFTER that point (install / migrate / health) can revert. null until then = nothing to undo.
373
+ let snapshot = null;
267
374
  if (!targetVersion) return { ok: false, error: 'missing --to <version> (the target core version)' };
268
375
 
269
376
  const fromVersion = readInstalledCoreVersion(instanceDir, fsImpl) || readPinnedCoreVersion(instanceDir, fsImpl);
377
+ const doRollback = (reason) => rollback({ instanceDir, snapshot, fromVersion, toVersion: targetVersion, opts }, reason, deps);
270
378
  log(`bongos upgrade — ${CORE_PKG}: ${fromVersion || '(none)'} → ${targetVersion}`);
271
379
  log(` instance: ${instanceDir}${dryRun ? ' [DRY RUN — no changes]' : ''}`);
272
380
  if (fromVersion === targetVersion && !opts.force) {
@@ -282,30 +390,56 @@ async function runUpgrade(opts, deps = {}) {
282
390
  log(` ✓ pre-flight: ${mods.note}`);
283
391
  if (!mods.ok) return { ok: false, error: `module pre-check failed: ${mods.blocking.map((b) => b.key).join(', ')} incompatible with core ${targetVersion}` };
284
392
 
285
- // 2. bump the pin (vendor the tarball + rewrite the file: dep).
286
- let vendorRel = opts.pinPath || (opts.from ? null : path.join('vendor', `${ARTIFACT}-${targetVersion}.tgz`));
287
- if (opts.from) {
288
- if (!fsImpl.existsSync(opts.from)) return { ok: false, error: `--from tarball not found: ${opts.from}` };
289
- if (dryRun) { log(` [dry-run] would vendor ${opts.from} → vendor/${ARTIFACT}-${targetVersion}.tgz`); vendorRel = path.join('vendor', `${ARTIFACT}-${targetVersion}.tgz`); }
290
- else vendorRel = vendorTarball({ instanceDir, fromTgz: path.resolve(opts.from), targetVersion }, fsImpl);
291
- }
292
- if (!fsImpl.existsSync(path.join(instanceDir, vendorRel)) && !dryRun) {
293
- return { ok: false, error: `no core tarball at ${vendorRel} — pass --from <path-to-.tgz> to vendor it` };
393
+ // 2. bump the pin. Two mutually exclusive core sources (ADR 0134, task 2090):
394
+ // REGISTRY (--registry): pin @bongos/core@<version> + ensure the env-fed .npmrc, so
395
+ // the `npm install` below pulls the PRIVATE package from the registry (needs NPM_TOKEN).
396
+ // VENDORED (default, --from <tgz>): copy the .tgz into vendor/ + rewrite the file: dep.
397
+ if (opts.registry) {
398
+ if (opts.from) return { ok: false, error: '--registry and --from are mutually exclusive (registry pull vs vendored tarball) — pick one core source' };
399
+ if (!hasNpmToken()) log(' ! no NPM_TOKEN in env — `npm install` will 404 on the PRIVATE @bongos/core until a read-only automation token is provided (ADR 0134).');
400
+ const prevPin = dryRun ? null : writeRegistryPin(instanceDir, targetVersion, fsImpl);
401
+ if (!dryRun) { writeNpmrc(instanceDir, { fsImpl, overwrite: false, log }); snapshot = { prevPin, prevVersion: fromVersion }; }
402
+ log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${dryRun ? ' + write an env-fed .npmrc' : ''}${prevPin ? ` (was ${prevPin})` : ''}`);
403
+ } else {
404
+ let vendorRel = opts.pinPath || (opts.from ? null : path.join('vendor', `${ARTIFACT}-${targetVersion}.tgz`));
405
+ if (opts.from) {
406
+ if (!fsImpl.existsSync(opts.from)) return { ok: false, error: `--from tarball not found: ${opts.from}` };
407
+ if (dryRun) { log(` [dry-run] would vendor ${opts.from} → vendor/${ARTIFACT}-${targetVersion}.tgz`); vendorRel = path.join('vendor', `${ARTIFACT}-${targetVersion}.tgz`); }
408
+ else vendorRel = vendorTarball({ instanceDir, fromTgz: path.resolve(opts.from), targetVersion }, fsImpl);
409
+ }
410
+ if (!fsImpl.existsSync(path.join(instanceDir, vendorRel)) && !dryRun) {
411
+ return { ok: false, error: `no core tarball at ${vendorRel} — pass --from <path-to-.tgz> to vendor it (or --registry to pull from the npm registry)` };
412
+ }
413
+ const prevPin = dryRun ? null : writePin(instanceDir, vendorRel, fsImpl);
414
+ if (!dryRun) snapshot = { prevPin, prevVersion: fromVersion };
415
+ log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → file:${vendorRel}${prevPin ? ` (was ${prevPin})` : ''}`);
294
416
  }
295
- const prevPin = dryRun ? null : writePin(instanceDir, vendorRel, fsImpl);
296
- log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → file:${vendorRel}${prevPin ? ` (was ${prevPin})` : ''}`);
297
417
 
298
418
  if (dryRun) { log(' [dry-run] would: npm install → npm run migrate → materialize .claude → regen API docs+client → restart → verify'); return { ok: true, dryRun: true, fromVersion, toVersion: targetVersion }; }
299
419
 
300
420
  // 3. npm install.
301
421
  const inst = npmInstall(instanceDir, run);
302
- if (!inst.ok) return { ok: false, error: `npm install failed (${inst.reason || 'exit ' + inst.code})`, fromVersion };
422
+ if (!inst.ok) {
423
+ const error = `npm install failed (${inst.reason || 'exit ' + inst.code})`;
424
+ if (rollbackOnFailure && snapshot) {
425
+ const rb = await doRollback('npm install failed');
426
+ return { ok: false, error, fromVersion, toVersion: targetVersion, rolledBack: true, rollback: rb };
427
+ }
428
+ return { ok: false, error, fromVersion };
429
+ }
303
430
  log(' ✓ npm install');
304
431
 
305
432
  // 4. migrate (additive core_* migrations).
306
433
  if (!opts.skipMigrate) {
307
434
  const mig = runMigrate(instanceDir, run);
308
- if (!mig.ok) return { ok: false, error: `migrate failed (${mig.reason || 'exit ' + mig.code})`, fromVersion };
435
+ if (!mig.ok) {
436
+ const error = `migrate failed (${mig.reason || 'exit ' + mig.code})`;
437
+ if (rollbackOnFailure && snapshot) {
438
+ const rb = await doRollback('migrate failed');
439
+ return { ok: false, error, fromVersion, toVersion: targetVersion, rolledBack: true, rollback: rb };
440
+ }
441
+ return { ok: false, error, fromVersion };
442
+ }
309
443
  log(' ✓ migrate');
310
444
  } else log(' • migrate skipped (--skip-migrate)');
311
445
 
@@ -345,6 +479,14 @@ async function runUpgrade(opts, deps = {}) {
345
479
  else err(` ! health ${opts.healthUrl} not ready after ${h.attempts} tr${h.attempts === 1 ? 'y' : 'ies'}: ${h.error || 'status ' + h.status}`);
346
480
  }
347
481
 
482
+ // A failed post-restart health check means the new core is serving broken — auto-revert (task
483
+ // 2149) instead of recording an "upgraded" row + leaving the instance down. The rollback writes
484
+ // its OWN rolled_back ledger row, so skip the normal insert below.
485
+ if (healthOk === false && rollbackOnFailure && snapshot) {
486
+ const rb = await doRollback('health check failed after restart');
487
+ return { ok: false, error: `health check failed after restart — rolled back to ${snapshot.prevVersion || 'the previous core'}`, fromVersion, toVersion: targetVersion, versionOk, healthOk: false, rolledBack: true, rollback: rb };
488
+ }
489
+
348
490
  const ledger = await recordLedger({ fromVersion, toVersion: targetVersion, sourceCommit: opts.sourceCommit, note: opts.note }, deps);
349
491
  if (ledger.ok) log(' ✓ recorded in core_upgrades ledger');
350
492
  else if (ledger.skipped) log(` • ledger skipped (${ledger.reason})`);
@@ -366,11 +508,13 @@ function parseArgs(argv) {
366
508
  sourceCommit: arg('--source-commit', argv),
367
509
  note: arg('--note', argv),
368
510
  pinPath: arg('--pin-path', argv),
511
+ registry: hasFlag('--registry', argv), // ADR 0134: pull @bongos/core@<--to> from the npm registry instead of vendoring
369
512
  dryRun: hasFlag('--dry-run', argv),
370
513
  force: hasFlag('--force', argv),
371
514
  skipMigrate: hasFlag('--skip-migrate', argv),
372
515
  skipMaterialize: hasFlag('--skip-materialize', argv),
373
516
  skipRegenDocs: hasFlag('--skip-regen-docs', argv),
517
+ rollbackOnFailure: !hasFlag('--no-rollback-on-failure', argv), // ADR 0100: auto-revert ON by default (task 2149)
374
518
  };
375
519
  }
376
520
 
@@ -385,6 +529,7 @@ async function main(argv = process.argv.slice(2)) {
385
529
  '',
386
530
  ' --to <version> target core version — presence switches from pre-check to a real bump',
387
531
  ' --from <tgz> source tarball to vendor into vendor/ (else it must already be there)',
532
+ ' --registry pull @bongos/core@<--to> from the npm registry (env-fed .npmrc + NPM_TOKEN) instead of vendoring a tarball (ADR 0134); mutually exclusive with --from',
388
533
  ' --instance <dir> consumer instance root (default: cwd)',
389
534
  ' --service <unit> systemd unit to restart after applying',
390
535
  ' --restart-cmd <cmd> explicit restart command (overrides --service)',
@@ -392,10 +537,12 @@ async function main(argv = process.argv.slice(2)) {
392
537
  ' --skip-migrate do not run `npm run migrate`',
393
538
  ' --skip-materialize do not refresh .claude/',
394
539
  ' --skip-regen-docs do not regenerate the OpenAPI spec + typed client from the new core',
540
+ ' --no-rollback-on-failure do NOT auto-revert on a failed install/migrate/health check (default: auto-rollback ON)',
395
541
  ' --dry-run show the plan; change nothing',
396
542
  ' --force skip the clean-tree + same-version guards',
397
543
  '',
398
544
  'Full bump: pre-flight → bump pin → npm install → migrate → materialize .claude → regen API docs+client → restart → verify.',
545
+ 'On a failure after the pin moves (install/migrate/health), auto-rollback reverts the pin, reinstalls the previous core, restarts, and records a rolled_back ledger row (the DB is not rolled back — core migrations are additive).',
399
546
  ].join('\n') + '\n');
400
547
  return 0;
401
548
  }
@@ -410,8 +557,8 @@ async function main(argv = process.argv.slice(2)) {
410
557
  }
411
558
 
412
559
  module.exports = {
413
- runUpgrade, parseArgs, main,
414
- versionFromTgzPath, readInstalledCoreVersion, readPinnedCoreVersion, writePin,
560
+ runUpgrade, parseArgs, main, rollback,
561
+ versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
415
562
  vendorTarball, gitTreeClean, preflightModules, npmInstall, runMigrate, regenerateApiArtifacts, restartService, recordLedger, pollHealth,
416
563
  CORE_PKG, ARTIFACT,
417
564
  };
@@ -1,64 +1,15 @@
1
- # src/bongos/ — GDS core (agent notes)
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > Nested CLAUDE.md — loads on demand when you work here. Root [CLAUDE.md](../../CLAUDE.md) is the index; this is the altitude map for this directory. See [ADR 0061](../../docs/adr/<redacted>.md) for why these exist.
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
4
6
 
5
- **Purpose.** `src/bongos/` is the Game Development System — the build-methodology backend (builders, tasks, claims, credits, grading, memory, dev boxes), mounted at `/api/gds` in the same Node process as the game. This is where **server-side enforcement lives**: rank/permission gates, the ship/grade pipeline, and credit accounting all run here, never on the client. The DB is the source of truth; this code is its only authoritative writer.
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
6
10
 
7
- **Key files / public surface.**
8
- - `db.js` — the **RESIDUE** after the lifecycle carve (BV1.R86): builder IDENTITY + admission, RANK/status management (`setBuilderRank`/`_applyRankChangeTx`), AUTH `builder_sessions`, the social TWINS (Discord link, karma feed, `listAuditLog`, the grade-trend `builderRollingAvgGrade`), the `security_docs` store, and the Gemini-key leak. **The build state machine (tasks·claims·versions·done-when·ship) graduated to [`modules/lifecycle/db.js`](../../modules/lifecycle/CLAUDE.md)** — so the residue here is exactly the kernel identity/auth/twin slice + `db-kernel.js` (ADR 0093 §3). (The **credits/costs** slice graduated to economy; the **session_records** slice graduated to sessions.) Don't read end-to-end — `grep` for the helper name. `withTx(fn)` wraps BEGIN/COMMIT. `sealSessionLog` + `_applyRankChangeTx`(+`RANKS`/`singleRungPromotionError`) + the `gradePassCount` fragment are deliberately TWINNED with `modules/lifecycle/db.js` (ADR 0091 §2 — keep in sync).
9
- - **Lifecycle graduated out** → [`modules/lifecycle/`](../../modules/lifecycle/CLAUDE.md) (the build state machine — tasks · claims · versions · done-when · goals · dependencies · the claim→ship→grade→credit lifecycle · github-push + merge-lock + conflict-resolve + publish-reconciler + ship-card/preflight + version-close; the LAST + most-coupled domain, BV1.R86). Core reaches it only through the `lifecycle` kernel **port** the module registers — `routes/{public,me,builders}.js` + `cascade-dispatch.js` resolve `seams.resolveOptional('lifecycle')` for the version/ship feed, per-builder claims/grades, the Archon roster, and the task-kind classifier; the moved routes (`routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js`) mount via the loader. It **retired the last three doorway leaks** (`createTask`/`VOTABLE_TASK_STATUSES`/`tallyPeerVotes` → behind the port). As a transaction-participant it RESOLVES the `reward`+`onboarding` ports inside its claim/ship/confirm txns ([ADR 0093](../../docs/adr/<redacted>.md) §1,§3).
10
- - **Grading graduated out** → [`modules/grading/`](../../modules/grading/CLAUDE.md) (the quality gate: v4 adversarial worker panel Narc/Quality/Hacker/Efficiency + deterministic pre-passes + the rubric; BV1.R73). Core reaches it only through the `grade` kernel **port** — the lifecycle module's `routes/tasks.js` + core `routes/public.js` resolve `seams.resolveOptional('grade')`, never a direct require ([ADR 0091](../../docs/adr/<redacted>.md) §4, [ADR 0024](../../docs/adr/<redacted>.md)). `cascade-dispatch.js` (dormant System 3) stayed core.
11
- - **Economy graduated out** → [`modules/economy/`](../../modules/economy/CLAUDE.md) (credit allocation + the cost-plus/kind-multiplier model, the cost ledger + dashboard rollups, streaks, achievements, the leaderboard; BV1.R77). Core reaches it only through the `reward` kernel **port** — a **transaction-participant** port: the lifecycle (`applyGrade`/`confirmTask`/`decideOverrideRequest`/`claimTask`/`upsertSessionRecordWithReward`) keeps its state-machine SQL and the `credit_log`/`cost_log` *guards*, but resolves `seams.resolveOptional('reward')` and passes its own tx `client` for every credit/achievement WRITE; the read routes (`me.js`/`public.js`/`builders.js`/`sessions.js`) + `publish-reconciler.js` resolve the same port ([ADR 0093](../../docs/adr/<redacted>.md) §1-2). `llm-pricing.js` stays core shared LLM-cost infra (doorway leak `api.llmPricing`, like `llm-cache.js`).
12
- - **Ideas graduated out** → [`modules/ideas/`](../../modules/ideas/CLAUDE.md) (the `idea_inbox` capture/triage/criterion-proposal flow + the `blockers` queue; BV1.R78). The discord `#ideas` inbound reaches it only through the `ideas.capture` kernel **port** the module registers — which **retired** the `captureIdea` doorway leak ([ADR 0093](../../docs/adr/<redacted>.md) §5). `routes/public.js` keeps its **own** inline `idea_inbox`/`blockers` SELECTs (core querying a shared table with its own SQL is fine — no import, no twin).
13
- - **Security FEATURE graduated out** → [`modules/security/`](../../modules/security/CLAUDE.md) (red-team / vulnerability **reports** + the **bounty** payout table, rank-gated Metic+; `security.js` + `routes/reports.js`; BV1.R79). Core reaches the bounty schedule only through the `security` kernel **port** the module registers — `routes/public.js`'s `/public/bounty-table` resolves `seams.resolveOptional('security').bountyTableDetailed()`, never a direct require ([ADR 0093](../../docs/adr/<redacted>.md) §1; [02-module-map §5](../../docs/design/modular-architecture/02-module-map.md)). **<redacted> stays kernel:** `permission-path-check.js`/`route-rank-check.js` (authz) AND `routes/security.js` (the `/security/adr` threat-model reader + the Archon-only `/security/docs` exploit-detail store, backed by core `db.js` `security_docs` queries) all stayed in core.
14
- - **Builder-settings graduated out** → [`modules/builder-settings/`](../../modules/builder-settings/CLAUDE.md) (the per-builder PREFS domain — wandering · skill · sound · render — + builder-needs + their `db.js` slice; the five `/me/*-prefs` routes; BV1.R80). The `GET /me` aggregator (`routes/me.js`) + `cascade-dispatch.js` reach it only through the `builder-settings` kernel **port** the module registers (`seams.resolveOptional('builder-settings')` for the wandering/render/needs profile slices + the skill ceiling), never a direct require ([ADR 0093](../../docs/adr/<redacted>.md) §1). The five `skill_model_prefs`/`sound_prefs`/`behavior_prefs`/`event_sound_prefs` getter/setter pairs left `db.js`; the `builders` table + the Gemini-key columns (art-pipeline credential infra) stay core.
15
- - `auth.js` — GitHub Device Flow (CLI) + web OAuth → `builder_sessions`. Exports the request guards: `requireBuilder`, `requireRank`, `allowBoxScope` (box tokens are deny-by-default, [ADR 0053](../../docs/adr/0053-scoped-dev-box-session.md)), plus Discord OAuth helpers.
16
- - `github-push.js` — server-mediated branch publish ([ADR 0055](../../docs/adr/<redacted>.md)): box uploads a git bundle, server pushes + opens PR + auto-merges via a short-lived **GitHub App** installation token (no standing PAT). Also `mergeIfGreen`, gate commit-status helpers.
17
- - `routes.js` + `routes/` — **thin mounter**: each feature is a sub-router in `routes/`; adding a feature = one new file + one `.use()` line. Audit + rate-limit middleware mount first. See `routes/`'s own nested CLAUDE.md.
18
- - `permission-path-check.js` / `route-rank-check.js` — the pure protected-path + route-rank matchers (SEC [#863](https://example.com/builders#/task/863) / [ADR 0043](../../docs/adr/<redacted>.md)), reused by the grading module (via the doorway — `api.routeRankCheck`/`api.permissionPathCheck`), pre-push hook, and `--fit` runner.
19
- - **Onboarding graduated out** → [`modules/onboarding/`](../../modules/onboarding/CLAUDE.md) (admission · the server-ticked newcomer onboarding state machine · the friction analyzer · the evergreen newcomer-chore restock; `onboarding-state.js` + `onboarding.js` + `newcomer-restock.js` + `routes/access-requests.js` + `approval-broadcast.js` + its `db.js` slice; BV1.R81). The novel shape: it is the FIRST core→module port the LIFECYCLE drives — core reaches the state machine + restock only through the `onboarding` kernel **port** the module registers (`seams.resolveOptional('onboarding')` in `db.js` claim/ship, `auth.js`, `routes/me.js`/`builders.js`/`tasks.js`/`claims.js`), never a direct require ([ADR 0093](../../docs/adr/<redacted>.md) §1). The admission GATE READS (`hasInvitedAccessRequest`/`getBuilderByGithubId`) stay core `db.js` (kernel identity/admission).
20
- - **Sessions/BFG graduated out** → [`modules/sessions/`](../../modules/sessions/CLAUDE.md) (the `session_records` corpus + its `/sessions` data plane, the BFG cross-builder scorer, the session-pulse/stale-claim helpers; `bfg.js` + `pulse.js` + `routes/sessions.js` + its `db.js` slice; BV1.R82). Like `memory`, it registers **NO port** — core never calls back in: the ship-time session-digest write is an **HTTP POST** to the module's own `/sessions` route. Its one outward edge is the economy `reward` port it *resolves* (`seams.resolveOptional('reward')` in `db.js` `upsertSessionRecordWithReward` + `routes/sessions.js`) for the cost-plus token reward + `sumCreditsForTasks` ([ADR 0093](../../docs/adr/<redacted>.md) §1). **Stays core:** `session_logs` (the per-claim ship LOG, `sealSessionLog` — lifecycle) and `builder_sessions` (auth tokens) are NOT this domain.
21
- - **Autonomy/MAS graduated out** → [`modules/autonomy/`](../../modules/autonomy/CLAUDE.md) (the Multi-Agent System — the autonomy gate, the conductor/architect/run-routine CLIs, and the in-process routine timers; `routes/autonomy.js` + `pollers/routine-timers.js` + the `autonomy_runs` `db.js` slice; BV1.R83). Like `sessions`/`memory`, it registers **NO port** — core never calls back in. The cross-cutting bit: the **routine timer** (was `routineTimers.start()` in `server.js`) is now a loader-started **poller** (`contributes.pollers`), and the **precheck route** (was mounted in `routes.js`) a module route; both severed their `src/`/`server.js` import. The timer's one outward edge is the lifecycle **`tallyPeerVotes`** capability it resolves through the **doorway** (`api.tallyPeerVotes` — the SAME tx `POST /tasks/peer-votes/tally` runs; a tracked lifecycle leak until R86), never a core require ([ADR 0093](../../docs/adr/<redacted>.md) §1). **Stays core:** `tallyPeerVotes` itself (lifecycle — writes `karma_log`/`peer_votes`/`tasks`); the MAS-domain CLIs travel with the domain but stay under `scripts/gds/`.
22
- - `boxes.js` · `box-access.js` · `do-api.js` · `cf-tunnel.js` — per-builder cloud dev-box lifecycle, rank-gated source clawback, DigitalOcean client, and the Cloudflare named-tunnel control plane ([ADR 0031](../../docs/adr/<redacted>.md)).
23
- - **Discord graduated out** → [`modules/discord/`](../../modules/discord/CLAUDE.md) (bot, broadcast, role-sync, channels, bug-attachments, link/role; BV1.R47). Core reaches it only through the doorway seams — ports `discord.isLinked`/`discord.guildJoinSpec`, events `task.shipped`/`builder.role-sync-needed` — never a direct require. Discord is a mirror only, never authority ([ADR 0033](../../docs/adr/<redacted>.md)).
24
- - `llm-pricing.js` · `task-classifier.js` · `search-chunker.js` · `search.js` · `publish-reconciler.js` · `conflict-resolve.js` — token→USD pricing (cost-plus reward), `tasks.kind` heuristic, retrieval-layer chunker + search ([ADR 0060](../../docs/adr/0060-gds-retrieval-layer.md)), the confirmed→shipped auto-heal sweep, and (`conflict-resolve.js`, [ADR 0082](../../docs/adr/<redacted>.md)) the SERVER-side generated-file/append-index merge-conflict resolver the reconciler calls on `merge_conflict` (temp-clone merge with the `otb-regen` driver + `union`, then push; never bypasses a check).
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
25
13
 
26
- **Gotchas / invariants.**
27
- - **Permissions enforced server-side, per-request, no cache** ([ADR 0016](../../docs/adr/<redacted>.md)) — `requireRank`/`requireBuilder` read live `builders.rank`; editing local files grants nothing.
28
- - `db.js` is huge — **skim by section/grep, never read whole** (see efficiency note above). (`grader.js` graduated to `modules/grading/` — BV1.R73.)
29
- - Protected-path + route-rank logic is defined **once** in `permission-path-check.js` / `route-rank-check.js`; reuse the matcher, don't re-implement.
30
- - `routes.js` is deliberately conflict-free — feature logic goes in a `routes/` sub-router, not here.
31
- - `preview-server.js` is **not** here — it lives at `../preview-server.js` (game-only sandbox).
32
-
33
- <!-- BEGIN GENERATED REPO-MAP (scripts/gds/gen-repo-map.js — do not hand-edit) -->
34
- **Symbol skeleton (generated from the code — do not hand-edit; refresh: `node scripts/gds/gen-repo-map.js`).** Exported signatures, most-referenced first. The interpretive notes above are hand-written; this factual list is regenerated on deploy so it can't drift ([ADR 0063](../../docs/adr/<redacted>.md)).
35
-
36
- - `api-errors.js` — `envelope(codeOrError, message, details)`, `classify(code)`, `class ApiError`, `CATEGORIES`, `OVERRIDES`, `isApiError(x)`, `DEFAULT_CATEGORY`, `statusForCode(code, override)`, `categoryForCode(code)`, `isRetriable(code)`
37
- - `api-path-404.js` — `apiNotFoundBody(p)`, `isApiShapedPath(p)`, `apiPathHint(p)`, `GDS_ROOTS`
38
- - `api-prefix.js` — `API_PREFIX`, `API_VERSION`, `LEGACY_API_PREFIXES`, `VERSIONED_API_PREFIX`, `ALL_API_PREFIXES`
39
- - `app-pair.js` — `pairStore`, `createPairStore(opts = {})`, `normalizeCode(raw)`
40
- - `auth.js` — `requireBuilder(req, res, next)`, `requireRank(...allowed)`, `allowBoxScope(req, _res, next)`, `authConfigured()`, `requireBfgPrincipal(req, res, next)`, `loadDiscordBotToken()`, `requireArchonPage(req, res, next)`, `isValidDiscordUserId(id)`, `requireBuilderPage(req, res, next)`, `completeWebFlow({ code, redirectUri, userAgent })` _(+20 more)_
41
- - `cascade-dispatch.js` — `dispatch({ prompt, taskKind, title, description, discipline, ceiling = 'opus', expectSchema = null,…)`, `LADDER`, `startRungForTask({ taskKind, title, description } = {})`, `startRungForKind(taskKind)`, `ceilingForBuilder(builderPrefs, skillName)`, `MAX_ESCALATIONS`, `rungIndex(model)`, `shouldEscalate(result, expectSchema)`
42
- - `db-kernel.js` — `getBuilderById(id)`, `insertAuditLog({)`, `withTx(fn, { pool: p = pool } = {})`, `RANKS`, `LIVE_RANK_LADDER`, `createSession({ builderId, token, source, userAgent, expiresAt })`, `lookupSession(token)`, `upsertBuilder({ githubId, githubLogin, displayName, avatarUrl })`, `getBuilderByGithubId(githubId)`, `hasInvitedAccessRequest(login)` _(+3 more)_
43
- - `db.js` — `setBuilderRank({ builderId, newRank, actorBuilderId })`, `getDiscordLink(builderId)`, `hasCliSession(builderId)`, `recordApplause({ targetTaskId, discordUserId })`, `singleRungPromotionError(oldRank, newRank)`, `deactivateBuilder({ builderId, reason, actorBuilderId })`, `<redacted>(builderId)`, `listAuditLog({ limit = 100, offset = 0, builderId, method, surface } = {})`, `revokeAllSessionsForBuilder(builderId)`, `unlinkDiscord(builderId)` _(+11 more)_
44
- - `goal-scope-check.js` — `check(goalScopeModules, touches, builderRank)`, `isCovered(touch, goalGlobs)`, `protectedPathBlock(touches, builderRank)`, `scopeSubset(goalScopeModules, touches)`
45
- - `hierarchy-config.js` — `tier`, `hierarchy()`, `tiers`, `INSTANCE_PATH`, `DEFAULT_HIERARCHY`, `ROLE_KEYS`, `_resetForTests()`, `STANDARD_KEYS`, `loadHierarchy({ instancePath, env = process.env } = {})`, `membershipTier` _(+9 more)_
46
- - `live-channel.js` — `subscribe(fn)`, `CHANNEL`, `_state()`
47
- - `llm-cache.js` — `store(spec, { resultStdout, resultCostUsd = 0 } = {})`, `normalize(taskType, prompt)`, `makeCacheSpec({ prompt, model, task_type, scope_sha, owner_builder_id = null, ttl_ms = null } = {})`, `NORMALIZERS`, `poolReachable()`, `withCache(spec, producer)`, `computeCacheKey({ prompt, model, task_type, scope_sha } = {})`, `recordSaving(spec, { resultCostUsd = 0, hitCount = 0 } = {})`, `scopeShaFor(paths, { runner, cwd } = {})`, `stripVolatile(text)` _(+2 more)_
48
- - `llm-pricing.js` — `priceUsage(model, usage)`, `PRICES`, `priceModelUsage(modelUsage)`, `componentsFromModelUsage(modelUsage)`, `familyFor(model)`, `canonicalizeModelId(model)`
49
- - `metrics.js` — `render(p = pool)`, `HIST_BUCKETS`, `authorizeScrape(req)`, `httpMiddleware()`, `incCounter(name, help, labels = {}, by = 1)`, `collectDbGauges(p = pool)`, `metricsHandler()`, `normalizeRoute(p)`, `observeHistogram(name, help, labels, value)`, `__resetForTests()`
50
- - `module-scope-map.js` — `MODULE_SCOPE_MAP`, `moduleKeys()`, `scopeIncludesProtected(keys)`, `isModuleKey(key)`, `isProtected(globs)`, `MODULE_GLOBS`, `globsForModules(keys)`, `scopeForModule(key)`, `isProtectedModule(key)`, `protectedModules()`
51
- - `optimizer.js` — `VALID_MODES`, `DEFAULT_MAX_PARALLEL`, `optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })`
52
- - `path-match.js` — `touchesOverlap(a, b)`, `matchOne(file, touches)`
53
- - `permission-path-check.js` — `PROTECTED_GLOBS`, `SPECIALTY`, `matchProtected(files)`, `checkPermissionPaths({ changedFiles, builderRank } = {})`, `globToRegExp(glob)`, `RANK_TIER`, `tierOf(rank)`, `toWorkerResult(result)`, `MIN_TIER`, `isBelowFloor(rank)`
54
- - `pool.js` — `instanceDbName()`
55
- - `repo-info.js` — `loadRepoInfo()`, `parseRemote(remote)`, `_resetCacheForTests()`
56
- - `route-rank-check.js` — `EXPECTED_RANKS`, `SPECIALTY`, `MODULES_DIR`, `MODULE_EXPECTED_RANKS`, `checkRouteRanks({ routesDir = ROUTES_DIR, modulesDir } = {})`, `toWorkerResult(result)`, `KNOWN_RANKS`, `WRITE_METHODS`, `classifyRank(lines, i)`, `classifyText(text)` _(+2 more)_
57
- - `routes.js` — `buildGdsRouter()`
58
- - `secret-box.js` — `VERSION`, `isConfigured()`, `decrypt(blob)`, `encrypt(plaintext)`, `provisionMasterKey(opts = {})`, `masterKeyFile()`
59
- - `sensitive-surfaces.js` — `isSensitive(file)`, `SENSITIVE_SURFACES`
60
- - `serve-internal.js` — `isBuildersHost(req)`, `mountInternalSurfaces(app)`, `isStatusHost(req)`, `injectBrandingGlobal(html, brand = clientBranding()`, `applyBrandTokens(html, brand = clientBranding()`, `injectBrandingInto(stamped, brand = clientBranding()`, `injectModuleHallWidgets(html, scripts = hallWidgetScripts({ isEnabled: isModuleEnabled })`, `themeMarkup(theme)`
61
- - `uptime-poller.js` — `start()`
62
- <!-- END GENERATED REPO-MAP -->
63
-
64
- **See also.** [ADR 0010](../../docs/adr/0010-pms-architecture.md) (GDS architecture) · [ADR 0016](../../docs/adr/<redacted>.md) (trust boundary) · [docs/architecture.md](../../docs/architecture.md) · `routes/CLAUDE.md` (HTTP surface).
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._
@@ -224,6 +224,30 @@ async function startDeviceFlow() {
224
224
  throw new Error(`device/code failed (${res.status}): ${text}`);
225
225
  }
226
226
  const data = await res.json();
227
+ // GitHub returns HTTP 200 with an { error, error_description } body for OAuth-App
228
+ // misconfig — most often device_flow_disabled (Device Flow not enabled on the App)
229
+ // or incorrect_client_credentials. res.ok is TRUE there, so without this the missing
230
+ // device_code silently produced a broken flow / an opaque 500 (the 2026-07-08 demo
231
+ // login failure). Mirror the token-exchange guard (data.error, above); task 2126.
232
+ if (data.error) {
233
+ // device_flow_disabled is self-inflicted + common: GitHub defaults Device Flow OFF on new
234
+ // Apps and the one-click manifest can't enable it (task 2129). Make the CLI error self-
235
+ // service by naming the right settings surface. We hold only client_id here (not the app
236
+ // slug — that lives on the control plane), so we can't build /settings/apps/<slug>; but the
237
+ // id PREFIX distinguishes a GitHub App (Iv*) from an OAuth App (Ov*), which picks the right
238
+ // listing to open.
239
+ let hint = '';
240
+ if (data.error === 'device_flow_disabled') {
241
+ const where = /^Iv/i.test(creds.client_id || '')
242
+ ? 'https://github.com/settings/apps (org-owned apps: https://github.com/organizations/<org>/settings/apps)'
243
+ : 'https://github.com/settings/developers';
244
+ hint = ` — terminal sign-in needs Device Flow enabled on the app: open ${where}, open your app, check "Enable Device Flow", and save. Web sign-in is unaffected.`;
245
+ }
246
+ throw new Error(`GitHub rejected device-flow start: ${data.error}${data.error_description ? ` — ${data.error_description}` : ''}${hint}`);
247
+ }
248
+ if (!data.device_code) {
249
+ throw new Error('GitHub device/code returned no device_code (unexpected response shape).');
250
+ }
227
251
  // data: { device_code, user_code, verification_uri, expires_in, interval }
228
252
  deviceFlows.set(data.device_code, {
229
253
  interval: data.interval || 5,
@@ -0,0 +1,56 @@
1
+ // src/bongos/module-submissions.js — the ADR 0107 §1 upstreaming review queue
2
+ // (task 1770, migration core_191). A rank-gated (metic+archon) queue a builder
3
+ // files a packaged module into (manifest + attribution + file snapshot + DCO
4
+ // sign-off) before it can be reviewed/accepted upstream. See
5
+ // docs/adr/0135-module-upstream-submission-interim-queue.md for why this queue
6
+ // lives in THIS instance's own DB rather than a live cross-repo call.
7
+ //
8
+ // Pure DB access — the packaging (pre-check, file read, sign-off) is
9
+ // scripts/gds/module.js's job (build tooling, fs-only); the route wires the
10
+ // two together.
11
+
12
+ const { pool } = require('./pool');
13
+
14
+ async function createModuleSubmission({
15
+ moduleKey, submittedBy, manifest, files, signOff, precheckPassedAt, sourceRepo,
16
+ }) {
17
+ const { rows } = await pool.query(
18
+ `INSERT INTO module_submissions
19
+ (module_key, submitted_by, manifest, files, sign_off, precheck_passed_at, source_repo)
20
+ VALUES ($1, $2, $3::jsonb, $4::jsonb, $5::jsonb, $6, $7)
21
+ RETURNING id, module_key, submitted_by, manifest, sign_off, precheck_passed_at,
22
+ source_repo, status, reviewed_by, reviewed_at, review_notes, submitted_at`,
23
+ [
24
+ moduleKey,
25
+ submittedBy,
26
+ JSON.stringify(manifest),
27
+ JSON.stringify(files),
28
+ JSON.stringify(signOff),
29
+ precheckPassedAt,
30
+ sourceRepo || null,
31
+ ]
32
+ );
33
+ return rows[0];
34
+ }
35
+
36
+ // Metadata-only list (no files jsonb — the file snapshot can run to hundreds of
37
+ // KB per submission, and no list consumer needs it; a future GET .../:id can add
38
+ // a with-files fetch if task 1771's review UI needs the code inline).
39
+ async function listModuleSubmissions({ status, moduleKey } = {}) {
40
+ const clauses = [];
41
+ const params = [];
42
+ if (status) { params.push(status); clauses.push(`status = $${params.length}`); }
43
+ if (moduleKey) { params.push(moduleKey); clauses.push(`module_key = $${params.length}`); }
44
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
45
+ const { rows } = await pool.query(
46
+ `SELECT id, module_key, submitted_by, manifest, sign_off, precheck_passed_at,
47
+ source_repo, status, reviewed_by, reviewed_at, review_notes, submitted_at
48
+ FROM module_submissions
49
+ ${where}
50
+ ORDER BY submitted_at DESC`,
51
+ params
52
+ );
53
+ return rows;
54
+ }
55
+
56
+ module.exports = { createModuleSubmission, listModuleSubmissions };
@@ -177,6 +177,12 @@ const MODULE_EXPECTED_RANKS = {
177
177
  'POST /goals/:id/requests/:reqId/respond': 'any-builder',
178
178
  'POST /goals/:id/invitations': 'any-builder',
179
179
  'POST /goals/:id/invitations/:reqId/respond': 'any-builder',
180
+ // the goal's own pending-queue view + requester self-withdraw (task 1953) —
181
+ // same in-handler walls: GET /requests is owner|manager|Archon
182
+ // (authorizeGoalManagement); withdraw is the requester only
183
+ // (authorizeWithdrawJoinRequest).
184
+ 'GET /goals/:id/requests': 'any-builder',
185
+ 'POST /goals/:id/requests/:reqId/withdraw': 'any-builder',
180
186
  // criterion confirmation + goal auto-achieve (ADR 0086 §6) — the planner's close loop.
181
187
  'POST /done-when/:criterionId/satisfy': 'metic+archon',
182
188
  // runtime criterion create (ADR 0105 gap fix, task 1874) — scope-truth write, Archon-only.
@@ -1,43 +1,15 @@
1
- # src/bongos/routes/ — GDS HTTP surface (agent notes)
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
2
 
3
- > Nested CLAUDE.md — loads on demand when you work here. Root [CLAUDE.md](../../../CLAUDE.md) is the index. See [ADR 0061](../../../docs/adr/<redacted>.md) for why these exist.
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
4
6
 
5
- **Purpose.** Express route handlers for the GDS API, all mounted under `/api/gds/*` in the same Node process as the game server. One file per resource group; each exports a `buildXRouter()` factory that returns its own `express.Router()`. The thin mounter is [`../routes.js`](../routes.js) — adding a feature = new file here + one `router.use(buildXRouter())` line there (kept conflict-free across parallel branches). `routes.js` also wires the global 64 KB JSON parser (skipped for `/memory/sync`, `/sessions`, `/tasks/:id/publish-branch` — they mount their own larger parser), the audit-log finish hook, and the rate limiter — all BEFORE the feature routers.
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
6
10
 
7
- **Conventions (read before adding a route).**
8
- - Auth/rank: middleware from [`../auth.js`](../auth.js) — `auth.requireBuilder` (valid session) then `auth.requireRank('metic','archon')` / `auth.requireRank('archon')`. **The server enforces rank per-request, no caching** ([ADR 0016](../../../docs/adr/<redacted>.md)). A `// rank:` comment sits above most handlers — it feeds the **generated** OpenAPI spec + [`docs/api-reference.md`](../../../docs/api-reference.md) (via `scripts/gds/gen-api-docs.js`; do NOT hand-maintain a mirror — it regenerates on ship, task 1918/[ADR 0109](../../../docs/adr/<redacted>.md)). `auth.allowBoxScope` opens a route to the deny-by-default box token (box.js bootstrap only).
9
- - Public surface: `public.js` + `analytics.js` are unauthenticated read-only (status page reads cross-origin) — wrap with `corsPublicGet(scope)` from `_helpers.js`; no secrets.
10
- - Validation: [`_helpers.js`](./_helpers.js) — `validateOrRespond(req,res,schema)` (opt-in, never rejects extras, structured 400), `LIMITS` (TITLE/DESCRIPTION/NOTES caps), `parseId`, `asyncHandler(tag, fn)`.
11
- - GitHub-rendered output only: the PR title/body built in `tasks.js` de-hashes bare `#NNN` (so it doesn't 404-autolink to GitHub issues) via the local `deHashRefs` helper — which mirrors ship.js's `sanitizeGitHubRefs` (not requirable here). Mirror `deHashRefs`; don't hand-roll. DB-row prose (tasks/inbox/memory) is stored as-is — sanitization happens only at the GitHub boundary.
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
12
13
 
13
- **Route groups.**
14
- - Work-tracking: `tasks.js`, `claims.js`, `versions.js`, `done-when.js` — the task lifecycle, claims, versions, done-when criteria.
15
- - Auth/identity: `auth.js`, `me.js`, `sessions.js` — Device/web/app-pair flows, self profile/prefs, session digests. (Admission gating — `access-requests.js` — carved to [`modules/onboarding/`](../../../modules/onboarding/CLAUDE.md) at R81; the loader mounts it.)
16
- - Builder ops: `builders.js`, `cost.js`, `leaderboard.js`, `memory.js`, `learnings.js` — builder records/budget, cost log, rankings, BFG-scoped memory, learnings.
17
- - Dev-box: `box.js` — provision/wake/close, SSH keys, terminal/sandbox tunnels, server-mediated branch publish.
18
- - Social/data: `discord.js`, `inbox.js`, `blockers.js`, `analytics.js`, `audit-log.js` — idea inbox, blockers, Discord link, analytics, Archon audit trail.
19
- - GDS-V4 / security: `search.js` (recall), `gate-approvals.js` (Archon gate PRs), `security.js` (red-team reports + Archon-served docs).
20
- - Util: `healthz.js`, `public.js`, `optimizer.js`, `bug-attachments.js`.
21
-
22
- **Gotchas / invariants.**
23
- - `tasks.js` (~79 KB), `box.js`, `auth.js`, `memory.js` are large — grep `router.(get|post|...)(` for the route, don't read end-to-end.
24
- - Rank gates are load-bearing (trust boundary). Default a write to `requireRank('archon')` unless it's deliberately broader; never loosen one without the permissions doc + an ADR.
25
- - Mount order: `public.js` is mounted LAST in routes.js; keep new files' prefixes from shadowing it.
26
-
27
- <!-- BEGIN GENERATED REPO-MAP (scripts/gds/gen-repo-map.js — do not hand-edit) -->
28
- **Symbol skeleton (generated from the code — do not hand-edit; refresh: `node scripts/gds/gen-repo-map.js`).** Exported signatures, most-referenced first. The interpretive notes above are hand-written; this factual list is regenerated on deploy so it can't drift ([ADR 0063](../../../docs/adr/<redacted>.md)).
29
-
30
- - `_helpers.js` — `validateOrRespond(req, res, schema, opts = {})`, `parseId(req, res, { param = 'id', code = 'bad_id', positiveInt = false } = {})`, `LIMITS`, `validate(body, schema, { strict = false, query = false } = {})`, `asyncHandler(tag, handler, { message = true, errorCode = 'server_error' } = {})`, `publicOrigin(req)`, `pageMeta(limit, offset, total)`, `parsePagination(req, { defaultLimit = PAGINATION.DEFAULT_LIMIT, maxLimit = PAGINATION.MAX_LIMIT } = {})`, `corsPublicGet(scope)`, `parseCookie(req, name)` _(+2 more)_
31
- - `auth.js` — `renderAuthErrorPage(heading, detail, opts = {})`, `sessionCookieDomainOpts(req)`, `safeReturnTo(raw)`, `reqHost(req)`
32
- - `backup.js` — `latestDump()`
33
- - `builders.js` — `boxHasEverConnected(builderId)`, `discordIsLinked(builderId)`, `displayRank(rank)`, `<redacted>({ displayName, direction, newRank })`, `fireDiscordBroadcast(text)`
34
- - `instance.js` — `MANIFEST_SCHEMA`, `buildInstanceManifest({)`, `AUTH_METHODS`
35
- - `live.js` — `buildLiveRouter()`, `frameEvent(event, data)`, `MAX_CONN_PER_IP`, `streamLive(req, res, deps = {})`
36
- - `llm-cache.js` — `badKey(res)`, `knownTaskType(t)`, `clampedCost(raw)`
37
- - `me.js` — `boxHasEverConnected(builderId)`, `artSharedKeyConfigured()`, `discordIsLinked(builderId)`
38
- - `modules.js` — `listModules()`
39
- - `public.js` — `projectPublicTask(task)`, `projectPublicBlocker(blocker)`, `projectPublicIdea(idea)`, `recentShippedWindow(rawDays)`
40
- - `security.js` — `adrPath()`, `readFullAdr()`
41
- <!-- END GENERATED REPO-MAP -->
42
-
43
- **See also.** [ADR 0016](../../../docs/adr/<redacted>.md) (trust boundary), [`docs/api-reference.md`](../../../docs/api-reference.md) (generated per-endpoint reference; `/docs` renders it), [`docs/canonical-permissions.md`](../../../docs/canonical-permissions.md) (rank ladder), [`../routes.js`](../routes.js) (mounter), [`../auth.js`](../auth.js).
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._
@@ -209,7 +209,12 @@ module.exports = function buildAuthRouter() {
209
209
  res.json(out);
210
210
  } catch (err) {
211
211
  console.error('[gds] device/start', err);
212
- res.fail('device_start_failed', { status: 500, message: 'internal error' });
212
+ // Surface the underlying reason (task 2126). A thrown device-flow start is almost
213
+ // always a GitHub OAuth-App misconfig (device_flow_disabled / bad client) — safe,
214
+ // non-secret, and actionable — so return it instead of an opaque "internal error".
215
+ // 502: the failure is upstream (GitHub), not a bug in this server.
216
+ const reason = String(err && err.message ? err.message : 'internal error').slice(0, 300);
217
+ res.fail('device_start_failed', { status: 502, message: reason });
213
218
  }
214
219
  });
215
220