@bongos/core 1.17.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.bongos-core.json +154 -83
  2. package/CLAUDE.md +11 -238
  3. package/bin/bongos.js +1 -0
  4. package/clients/bongos-client/README.md +4 -4
  5. package/clients/bongos-client/bongos-client.global.js +14 -1
  6. package/clients/bongos-client/index.cjs +14 -1
  7. package/clients/bongos-client/index.d.ts +19 -2
  8. package/clients/bongos-client/index.mjs +11 -1
  9. package/clients/bongos-client/package.json +1 -1
  10. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
  11. package/docs/adr/0134-private-first-npm-distribution.md +20 -6
  12. package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
  13. package/docs/adr/README.md +5 -4
  14. package/docs/api/openapi.json +315 -6
  15. package/docs/api-reference.md +10 -5
  16. package/docs/architecture.md +1 -1
  17. package/docs/file-map.md +2 -2
  18. package/docs/modules-contract.md +2 -0
  19. package/docs/recipes/instance-deploy.md +4 -4
  20. package/docs/recipes/instance-first-run.md +51 -0
  21. package/docs/recipes/packaging-the-core.md +5 -5
  22. package/docs/recipes/private-npm-distribution.md +22 -12
  23. package/docs/recipes/self-host.md +1 -1
  24. package/docs/recipes/upgrading-the-core.md +8 -8
  25. package/migrations/core_191_module_submissions.sql +45 -0
  26. package/modules/autonomy/CLAUDE.md +11 -45
  27. package/modules/builder-settings/CLAUDE.md +11 -54
  28. package/modules/dev-box/CLAUDE.md +11 -34
  29. package/modules/discord/CLAUDE.md +11 -46
  30. package/modules/economy/CLAUDE.md +11 -44
  31. package/modules/grading/CLAUDE.md +11 -38
  32. package/modules/hall-ui/CLAUDE.md +11 -57
  33. package/modules/hall-ui/public/goals-page.js +227 -22
  34. package/modules/hall-ui/public/goals.html +16 -0
  35. package/modules/hall-ui/public/new-project-wizard.js +5 -4
  36. package/modules/ideas/CLAUDE.md +11 -33
  37. package/modules/lifecycle/CLAUDE.md +11 -59
  38. package/modules/lifecycle/routes/goals.js +80 -0
  39. package/modules/memory/CLAUDE.md +11 -36
  40. package/modules/onboarding/CLAUDE.md +11 -51
  41. package/modules/security/CLAUDE.md +11 -40
  42. package/modules/sessions/CLAUDE.md +11 -35
  43. package/modules/status-ui/CLAUDE.md +11 -43
  44. package/package-lock.json +2 -2
  45. package/package.json +1 -1
  46. package/public-landing/create.html +58 -12
  47. package/public-landing/index.html +40 -21
  48. package/scripts/gds/CLAUDE.md +11 -140
  49. package/scripts/gds/claude-materialize.js +151 -18
  50. package/scripts/gds/claude-md-template.js +84 -0
  51. package/scripts/gds/cli-lib.js +112 -2
  52. package/scripts/gds/gen-api-client.js +10 -10
  53. package/scripts/gds/init.js +53 -13
  54. package/scripts/gds/migration-namespace.js +1 -1
  55. package/scripts/gds/module.js +118 -4
  56. package/scripts/gds/npmrc.js +93 -0
  57. package/scripts/gds/onboard.js +13 -24
  58. package/scripts/gds/package-core.js +56 -14
  59. package/scripts/gds/provision.js +102 -17
  60. package/scripts/gds/seed-adopt-tasks.js +1 -1
  61. package/scripts/gds/status.js +6 -1
  62. package/scripts/gds/task.js +166 -0
  63. package/scripts/gds/upgrade.js +58 -18
  64. package/src/bongos/CLAUDE.md +11 -60
  65. package/src/bongos/auth.js +11 -0
  66. package/src/bongos/module-submissions.js +56 -0
  67. package/src/bongos/route-rank-check.js +6 -0
  68. package/src/bongos/routes/CLAUDE.md +11 -39
  69. package/src/bongos/routes/auth.js +6 -1
  70. package/src/bongos/routes/modules.js +123 -21
  71. package/src/instance-config.js +1 -1
  72. package/src/module-api.js +13 -1
  73. package/tests/claude_materialize.mjs +105 -0
  74. package/tests/device_flow_start.mjs +73 -0
  75. package/tests/first_run_engine_check.mjs +79 -0
  76. package/tests/goal_membership_requests.mjs +33 -3
  77. package/tests/hook_user_agents.mjs +62 -0
  78. package/tests/init.mjs +59 -6
  79. package/tests/instance_mismatch_guard.mjs +101 -0
  80. package/tests/module_cli.mjs +130 -2
  81. package/tests/module_submissions.mjs +135 -0
  82. package/tests/npmrc.mjs +105 -0
  83. package/tests/onboard.mjs +49 -0
  84. package/tests/package_core.mjs +68 -22
  85. package/tests/platform_boot.mjs +5 -2
  86. package/tests/provision.mjs +107 -8
  87. package/tests/provisioning_github_app_routes.mjs +29 -2
  88. package/tests/served_paths_shipped.mjs +2 -2
  89. package/tests/task_cli.mjs +99 -0
  90. package/tests/upgrade.mjs +89 -25
@@ -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,20 @@ 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
+
76
103
  // ---- steps (each injectable for tests) --------------------------------------
77
104
 
78
105
  function gitTreeClean(instanceDir, run = spawnSync) {
@@ -282,18 +309,29 @@ async function runUpgrade(opts, deps = {}) {
282
309
  log(` ✓ pre-flight: ${mods.note}`);
283
310
  if (!mods.ok) return { ok: false, error: `module pre-check failed: ${mods.blocking.map((b) => b.key).join(', ')} incompatible with core ${targetVersion}` };
284
311
 
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` };
312
+ // 2. bump the pin. Two mutually exclusive core sources (ADR 0134, task 2090):
313
+ // REGISTRY (--registry): pin @bongos/core@<version> + ensure the env-fed .npmrc, so
314
+ // the `npm install` below pulls the PRIVATE package from the registry (needs NPM_TOKEN).
315
+ // VENDORED (default, --from <tgz>): copy the .tgz into vendor/ + rewrite the file: dep.
316
+ if (opts.registry) {
317
+ if (opts.from) return { ok: false, error: '--registry and --from are mutually exclusive (registry pull vs vendored tarball) — pick one core source' };
318
+ 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).');
319
+ const prevPin = dryRun ? null : writeRegistryPin(instanceDir, targetVersion, fsImpl);
320
+ if (!dryRun) writeNpmrc(instanceDir, { fsImpl, overwrite: false, log });
321
+ log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${dryRun ? ' + write an env-fed .npmrc' : ''}${prevPin ? ` (was ${prevPin})` : ''}`);
322
+ } else {
323
+ let vendorRel = opts.pinPath || (opts.from ? null : path.join('vendor', `${ARTIFACT}-${targetVersion}.tgz`));
324
+ if (opts.from) {
325
+ if (!fsImpl.existsSync(opts.from)) return { ok: false, error: `--from tarball not found: ${opts.from}` };
326
+ if (dryRun) { log(` [dry-run] would vendor ${opts.from} → vendor/${ARTIFACT}-${targetVersion}.tgz`); vendorRel = path.join('vendor', `${ARTIFACT}-${targetVersion}.tgz`); }
327
+ else vendorRel = vendorTarball({ instanceDir, fromTgz: path.resolve(opts.from), targetVersion }, fsImpl);
328
+ }
329
+ if (!fsImpl.existsSync(path.join(instanceDir, vendorRel)) && !dryRun) {
330
+ 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)` };
331
+ }
332
+ const prevPin = dryRun ? null : writePin(instanceDir, vendorRel, fsImpl);
333
+ log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → file:${vendorRel}${prevPin ? ` (was ${prevPin})` : ''}`);
294
334
  }
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
335
 
298
336
  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
337
 
@@ -366,6 +404,7 @@ function parseArgs(argv) {
366
404
  sourceCommit: arg('--source-commit', argv),
367
405
  note: arg('--note', argv),
368
406
  pinPath: arg('--pin-path', argv),
407
+ registry: hasFlag('--registry', argv), // ADR 0134: pull @bongos/core@<--to> from the npm registry instead of vendoring
369
408
  dryRun: hasFlag('--dry-run', argv),
370
409
  force: hasFlag('--force', argv),
371
410
  skipMigrate: hasFlag('--skip-migrate', argv),
@@ -385,6 +424,7 @@ async function main(argv = process.argv.slice(2)) {
385
424
  '',
386
425
  ' --to <version> target core version — presence switches from pre-check to a real bump',
387
426
  ' --from <tgz> source tarball to vendor into vendor/ (else it must already be there)',
427
+ ' --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
428
  ' --instance <dir> consumer instance root (default: cwd)',
389
429
  ' --service <unit> systemd unit to restart after applying',
390
430
  ' --restart-cmd <cmd> explicit restart command (overrides --service)',
@@ -411,7 +451,7 @@ async function main(argv = process.argv.slice(2)) {
411
451
 
412
452
  module.exports = {
413
453
  runUpgrade, parseArgs, main,
414
- versionFromTgzPath, readInstalledCoreVersion, readPinnedCoreVersion, writePin,
454
+ versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin,
415
455
  vendorTarball, gitTreeClean, preflightModules, npmInstall, runMigrate, regenerateApiArtifacts, restartService, recordLedger, pollHealth,
416
456
  CORE_PKG, ARTIFACT,
417
457
  };
@@ -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,17 @@ 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
+ throw new Error(`GitHub rejected device-flow start: ${data.error}${data.error_description ? ` — ${data.error_description}` : ''}`);
234
+ }
235
+ if (!data.device_code) {
236
+ throw new Error('GitHub device/code returned no device_code (unexpected response shape).');
237
+ }
227
238
  // data: { device_code, user_code, verification_uri, expires_in, interval }
228
239
  deviceFlows.set(data.device_code, {
229
240
  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
 
@@ -1,5 +1,5 @@
1
1
  // src/gds/routes/modules.js — the hall "Modules" tab backend (ADR 0107 §9,
2
- // task 1776). Three jobs, matching the ADR:
2
+ // task 1776). Four jobs, matching the ADR:
3
3
  //
4
4
  // GET /modules list every on-disk module: enabled state,
5
5
  // attribution (author/origin/license/maintainer),
@@ -9,37 +9,72 @@
9
9
  // and reload the live process (§6: default-off
10
10
  // respected — this only ever flips an existing
11
11
  // module's flag, never invents a new key).
12
- // POST /modules/:key/submit the "submit for review" front door (§1). The
13
- // submit pipeline (task 1770) hasn't landed yet,
14
- // so this is a clear 501 stub, not a fake queue —
15
- // the button lights up for real once 1770 ships.
12
+ // POST /modules/:key/submit the "submit for review" front door (§1) — files
13
+ // a packaged submission into the upstreaming
14
+ // review queue (task 1770). Re-runs the §4
15
+ // pre-check + requires a DCO sign-off against
16
+ // THIS box's own on-disk copy of the module (the
17
+ // authoritative check — `bongos module submit`
18
+ // is a thin CLI client of this same route).
19
+ // GET /modules/submissions list queued submissions (task 1771 reads this).
16
20
  //
17
21
  // Read is open to any signed-in builder (same gate as the atlas/primer pages);
18
- // the write (enable/disable) is metic+archon, same rank as /backup/trigger —
19
- // it changes running behavior for the whole instance, not just the caller.
22
+ // every write (enable/disable, submit) is metic+archon, same rank as
23
+ // /backup/trigger — it changes running behavior for the whole instance (or
24
+ // files something into a shared review queue), not just the caller.
20
25
  //
21
- // Persistence note: this writes config/modules.json on disk and reloads the
22
- // in-process cache, so the effect is immediate on this box. Like any other
23
- // hand-edit of that file, surviving the next `git reset --hard` deploy needs
24
- // the change committed — this route does not auto-commit (an HTTP handler
25
- // auto-pushing to main bypasses the ship/grade/deploy pipeline, which the
26
- // project's trust boundary treats as a bigger risk than a toggle that needs a
27
- // human/agent to land it properly). Captured as a follow-up idea: durable
28
- // (DB-backed) module enablement that survives a deploy with no extra step.
26
+ // Persistence note: /modules/:key/enable writes config/modules.json on disk
27
+ // and reloads the in-process cache, so the effect is immediate on this box.
28
+ // Like any other hand-edit of that file, surviving the next `git reset --hard`
29
+ // deploy needs the change committed — this route does not auto-commit (an
30
+ // HTTP handler auto-pushing to main bypasses the ship/grade/deploy pipeline,
31
+ // which the project's trust boundary treats as a bigger risk than a toggle
32
+ // that needs a human/agent to land it properly). Captured as a follow-up
33
+ // idea: durable (DB-backed) module enablement that survives a deploy with no
34
+ // extra step.
35
+ //
36
+ // Why the review queue is a table in THIS instance's own DB (not a live
37
+ // cross-repo call to the extracted core repo): see
38
+ // docs/adr/0135-module-upstream-submission-interim-queue.md.
29
39
 
30
40
  const express = require('express');
31
41
  const fs = require('node:fs');
32
42
  const path = require('node:path');
33
43
  const auth = require('../auth');
34
- const { asyncHandler, validateOrRespond } = require('./_helpers');
44
+ const { asyncHandler, validateOrRespond, LIMITS } = require('./_helpers');
35
45
  const loader = require('../../module-loader/loader');
36
46
  const { deriveProvenance } = require('../../module-loader/provenance');
37
47
  const modulesLib = require('../../modules');
48
+ // scripts/gds/ is a sibling CORE-package dir (both resolve under
49
+ // resolveCoreRoot() — see scripts/gds/CLAUDE.md), so requiring its pure fs
50
+ // logic from here is in-boundary. module.js never statically requires the CLI
51
+ // session/fetch layer (cli-lib.js) — only cmdSubmit lazy-requires it, and only
52
+ // when actually invoked from the CLI — so this stays safe in the always-on
53
+ // server process.
54
+ const moduleCli = require('../../../scripts/gds/module');
55
+ const moduleSubmissions = require('../module-submissions');
56
+
57
+ // A packaged submission embeds every file's content (base64) — cap the total
58
+ // so a runaway/misbuilt module can't bloat the review-queue table. Generous
59
+ // headroom over what a real feature module carries (routes + migrations + UI
60
+ // + a CLAUDE.md are typically tens of KB); mirrors the security_docs content
61
+ // cap (SECURITY_DOC_CONTENT_MAX) in routes/security.js.
62
+ const SUBMISSION_MAX_BYTES = 5_000_000;
38
63
 
39
64
  function ownProjectName() {
40
65
  try { return require('../../branding').branding().identity.productName; } catch { return null; }
41
66
  }
42
67
 
68
+ // "owner/name", e.g. "lars589/amazonprimea" — the provenance a submission
69
+ // carries (ADR 0107 §2: attribution/provenance travels WITH the module as a
70
+ // portable record, not a cross-GDS link). null if the branding pack has none.
71
+ function sourceRepoLabel() {
72
+ try {
73
+ const repo = require('../../branding').branding().repo;
74
+ return repo && repo.name ? `${repo.owner || '?'}/${repo.name}` : null;
75
+ } catch { return null; }
76
+ }
77
+
43
78
  function readInstanceConfig() {
44
79
  try { return JSON.parse(fs.readFileSync(modulesLib.INSTANCE_PATH, 'utf8')); }
45
80
  catch (e) { if (e.code === 'ENOENT') return { modules: {} }; throw e; }
@@ -78,7 +113,7 @@ module.exports = function buildModulesRouter() {
78
113
  modules: all,
79
114
  own_built: all.filter((m) => m.provenance === 'own').length,
80
115
  core_loaded: all.filter((m) => m.provenance === 'core').length,
81
- submit_pipeline_live: false, // flips true once task 1770 ships
116
+ submit_pipeline_live: true, // task 1770 shipped — POST /modules/:key/submit is real
82
117
  });
83
118
  }));
84
119
 
@@ -108,7 +143,17 @@ module.exports = function buildModulesRouter() {
108
143
  res.json({ ok: true, key, enabled, note: 'Live on this box now. Persisting past the next deploy needs config/modules.json committed.' });
109
144
  }));
110
145
 
111
- // rank: metic+archon — the "submit this module for review" front door (501 until the submit pipeline, task 1770, ships).
146
+ // POST /api/gds/modules/:key/submit — ADR 0107 §1 upstreaming submission.
147
+ //
148
+ // task 1770. Body: { signed_by?: "Name <email>" }. Re-runs the §4
149
+ // AGPL/publish pre-check + requires a DCO-style sign-off against THIS box's
150
+ // OWN on-disk copy of modules/:key (the authoritative check — a local
151
+ // `bongos module check` pass on a builder's box is a courtesy, not proof, if
152
+ // the module's changes aren't committed + shipped + deployed here yet).
153
+ // signed_by records a NEW sign-off only if none is on file; an existing
154
+ // .upstream-signoff.json is reused as-is (not overwritten).
155
+ //
156
+ // rank: metic+archon — files into a shared review queue, not just a toggle for the caller.
112
157
  router.post('/modules/:key/submit', auth.requireBuilder, auth.requireRank('metic', 'archon'),
113
158
  asyncHandler('POST /modules/:key/submit', async (req, res) => {
114
159
  const { key } = req.params;
@@ -116,10 +161,67 @@ module.exports = function buildModulesRouter() {
116
161
  if (!known.find((m) => m.key === key)) {
117
162
  return res.fail('not_found', { status: 404, message: `No on-disk module "${key}".` });
118
163
  }
119
- res.fail('not_yet_available', {
120
- status: 501,
121
- message: 'The module-submit pipeline (bongos module submit → core review queue) is not live yet — tracked by task 1770 (ADR 0107 §8/§9). This front door will call straight through once it ships.',
164
+ if (validateOrRespond(req, res, { signed_by: { type: 'string', maxLength: LIMITS.TITLE } })) return;
165
+
166
+ const precheck = moduleCli.checkModulePublishability(key);
167
+ if (!precheck.ok) {
168
+ return res.fail('precheck_failed', {
169
+ status: 422,
170
+ message: `"${key}" is not clear to submit upstream (ADR 0107 §4 AGPL/publish pre-check failed).`,
171
+ findings: precheck.findings,
172
+ });
173
+ }
174
+
175
+ let signOff = moduleCli.readSignOff(key);
176
+ const signedBy = req.body && typeof req.body.signed_by === 'string' ? req.body.signed_by.trim() : '';
177
+ if (!signOff && signedBy) {
178
+ moduleCli.recordSignOff(key, signedBy);
179
+ signOff = moduleCli.readSignOff(key);
180
+ }
181
+ if (!signOff) {
182
+ return res.fail('sign_off_required', {
183
+ status: 422,
184
+ message: `No DCO-style sign-off on file for "${key}". Pass "signed_by": "Name <email>" to affirm + record it.`,
185
+ });
186
+ }
187
+
188
+ const pkg = moduleCli.packageModuleForSubmission(key);
189
+ if (pkg.totalBytes > SUBMISSION_MAX_BYTES) {
190
+ return res.fail('submission_too_large', {
191
+ status: 413,
192
+ message: `"${key}" is ${pkg.totalBytes} bytes, over the ${SUBMISSION_MAX_BYTES}-byte upstreaming cap.`,
193
+ });
194
+ }
195
+
196
+ const submission = await moduleSubmissions.createModuleSubmission({
197
+ moduleKey: key,
198
+ submittedBy: req.builder.id,
199
+ manifest: pkg.manifest,
200
+ files: pkg.files,
201
+ signOff,
202
+ precheckPassedAt: new Date(),
203
+ sourceRepo: sourceRepoLabel(),
122
204
  });
205
+
206
+ console.log(`[gds] module "${key}" submitted upstream by builder ${req.builder.id} — submission ${submission.id}`);
207
+ res.status(201).json({ ok: true, submission });
208
+ }));
209
+
210
+ // GET /api/gds/modules/submissions?status=&module_key= — the upstreaming
211
+ // review queue (task 1771 reads this). Metadata only — no file contents (a
212
+ // future GET .../:id can add a with-files fetch if the review UI needs it).
213
+ //
214
+ // rank: metic+archon — the review queue, same rank as filing into it.
215
+ router.get('/modules/submissions', auth.requireBuilder, auth.requireRank('metic', 'archon'),
216
+ asyncHandler('GET /modules/submissions', async (req, res) => {
217
+ const status = req.query.status ? String(req.query.status) : undefined;
218
+ if (status && !['pending', 'accepted', 'rejected'].includes(status)) {
219
+ return res.fail('bad_status', { status: 400, message: 'status must be one of pending|accepted|rejected.' });
220
+ }
221
+ const moduleKey = req.query.module_key ? String(req.query.module_key) : undefined;
222
+ const submissions = await moduleSubmissions.listModuleSubmissions({ status, moduleKey });
223
+ res.set('Cache-Control', 'no-store');
224
+ res.json({ submissions });
123
225
  }));
124
226
 
125
227
  return router;