@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
@@ -37,7 +37,7 @@
37
37
  // it refuses where config/branding.json exists, and --force OVERWRITES. `--adopt` is
38
38
  // the opposite: it LAYERS Cloud Bongos onto an EXISTING repo (app code, git history,
39
39
  // a package.json). It writes the lean config only where absent, adds the pinned
40
- // @cloudbongos/core dependency to the owner's package.json without touching any other
40
+ // @bongos/core dependency to the owner's package.json without touching any other
41
41
  // field, and materializes .claude/ ADDITIVELY — for any pre-existing file it
42
42
  // MERGES-or-LEAVES-AND-REPORTS, it NEVER overwrites, and it does not reuse the --force
43
43
  // path. It deliberately does NOT seed a synthetic "V1 — Prove the loop": capturing the
@@ -56,11 +56,17 @@
56
56
  // bongos init --dir <path> target dir for the config files (default: cwd)
57
57
  // bongos init --no-seed write config only; skip the version/task seeding
58
58
  // bongos init --force overwrite existing config files (greenfield only; ignored by --adopt)
59
- // bongos init --vendor-core <tgz> greenfield only: vendor the @cloudbongos/core .tgz + pin
59
+ // bongos init --vendor-core <tgz> greenfield only: vendor the @bongos/core .tgz + pin
60
60
  // package.json to file:vendor/… + generate package-lock.json,
61
61
  // so the scaffold is `npm ci`-installable (ADR 0108 gap 1/4).
62
- // Without it the scaffold pins the caret range (unresolvable
63
- // on npm today) — a local/dev scaffold you resolve another way.
62
+ // Without it the scaffold pins the caret range (which now
63
+ // resolves from the PRIVATE registry — see --registry-core).
64
+ // bongos init --registry-core greenfield only: keep the caret pin + write an env-fed
65
+ // .npmrc (`_authToken=${NPM_TOKEN}`) so `npm ci` pulls the
66
+ // PRIVATE @bongos/core from the npm registry (ADR 0134).
67
+ // With NPM_TOKEN set it also generates package-lock.json;
68
+ // without a token it 404s (the expected tokenless outcome).
69
+ // Mutually exclusive with --vendor-core (registry vs tarball).
64
70
 
65
71
  const fs = require('node:fs');
66
72
  const path = require('node:path');
@@ -72,6 +78,7 @@ const { materializeClaude } = require('./claude-materialize'); // ADR 0108 §3 (
72
78
  const { detectStack, formatStackSummary } = require('./adopt-detect'); // ADR 0121 §Decision 4 (task 2043): inspect an existing repo before layering
73
79
  const { resolveBacklogTasks, DEFAULT_MAX: BACKLOG_MAX } = require('./adopt-import'); // ADR 0121 §Decision 3 (task 2044): import an existing backlog
74
80
  const { preflightAdopt, formatPreflight } = require('./adopt-preflight'); // ADR 0121 §Decision 5 (task 2045): the accept-gated conflict pre-flight
81
+ const { writeNpmrc, hasNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the registry-install path's env-fed .npmrc
75
82
 
76
83
  const KNOWN_MODULES = ['game', 'art-pipeline', 'discord', 'dev-box'];
77
84
 
@@ -87,7 +94,7 @@ const HOSTING_SHAPES = ['co-tenant', 'dedicated', 'standalone'];
87
94
  // `bongos init --adopt` LAYERS this onto an existing repo's package.json; `bongos
88
95
  // upgrade` later re-pins it to a vendored `file:` tarball. Named so the adopt
89
96
  // layering and the upgrade re-pin agree on the dependency name.
90
- const CORE_PKG = '@cloudbongos/core';
97
+ const CORE_PKG = '@bongos/core';
91
98
 
92
99
  const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
93
100
 
@@ -114,13 +121,13 @@ function coreVersionSafe() {
114
121
  // (`bash <core>/scripts/migrate.sh`). Keeping them identical means `npm start` /
115
122
  // `npm run migrate` on the instance behave like production, and `bongos upgrade`'s
116
123
  // `npm run migrate` step resolves to the core's migrate.sh (ADR 0108).
117
- const INSTANCE_CORE_PKG_DIR = 'node_modules/@cloudbongos/core';
124
+ const INSTANCE_CORE_PKG_DIR = 'node_modules/@bongos/core';
118
125
  const INSTANCE_START_SCRIPT = `node ${INSTANCE_CORE_PKG_DIR}/src/platform-server.js`;
119
126
  const INSTANCE_MIGRATE_SCRIPT = `bash ${INSTANCE_CORE_PKG_DIR}/scripts/migrate.sh`;
120
127
 
121
128
  // PURE (exported for tests) — build/augment the greenfield instance package.json so a
122
129
  // fresh instance is `npm ci`-installable and runnable (ADR 0108 greenfield gap 1/4,
123
- // task 2053): the @cloudbongos/core dependency (the `file:` tarball pin when a tarball is
130
+ // task 2053): the @bongos/core dependency (the `file:` tarball pin when a tarball is
124
131
  // vendored, else the declared caret range — the CARET RESOLVES NOWHERE on npm, which is
125
132
  // exactly why the vendored path pins `file:`) plus the start/migrate npm scripts a
126
133
  // standalone instance runs. Preserves any pre-existing fields + scripts (fills only what
@@ -258,7 +265,7 @@ function buildHierarchyConfig(spec) {
258
265
  }
259
266
 
260
267
  // PURE — layer the pinned core dependency into a parsed package.json (ADR 0121
261
- // §Decision 2 / task 2042). NEVER clobbers: if `@cloudbongos/core` is already a dep
268
+ // §Decision 2 / task 2042). NEVER clobbers: if `@bongos/core` is already a dep
262
269
  // (in dependencies OR devDependencies) it returns the object untouched with
263
270
  // action:'present'; if there is no package.json at all it MINTS a minimal one
264
271
  // (action:'created'); otherwise it ADDS the dep, preserving every existing field and
@@ -466,7 +473,7 @@ async function interview({ adopt = false } = {}) {
466
473
  let provisioning;
467
474
  const wantProv = (await ask(rl, ' Provision production hosting now? (y/N)', 'N')).toLowerCase();
468
475
  if (wantProv === 'y' || wantProv === 'yes') {
469
- const shape = (await ask(rl, ' Hosting shape? (co-tenant = share an existing box, $0 / dedicated = its own droplet / standalone = its own repo + pinned @cloudbongos/core, ADR 0108)', 'co-tenant')).toLowerCase();
476
+ const shape = (await ask(rl, ' Hosting shape? (co-tenant = share an existing box, $0 / dedicated = its own droplet / standalone = its own repo + pinned @bongos/core, ADR 0108)', 'co-tenant')).toLowerCase();
470
477
  const domain = await ask(rl, ' Domain you own — bring-your-own (blank = none yet)?');
471
478
  const targetRef = await ask(rl, ' Target credential ref? (which control-plane creds the runner should use; optional)');
472
479
  let tier = '';
@@ -579,7 +586,7 @@ function layerConfigs(spec, { dir, dryRun, log = console.log }) {
579
586
  }
580
587
 
581
588
  // `--adopt` package.json layering (ADR 0121 §Decision 2 / task 2042): add the
582
- // pinned @cloudbongos/core dependency to the owner's EXISTING package.json without
589
+ // pinned @bongos/core dependency to the owner's EXISTING package.json without
583
590
  // touching any other field, or MINT a minimal one when the repo has none. When the
584
591
  // dep is already present the file is left byte-untouched (no reserialize). An
585
592
  // unparseable package.json is left alone with a report — adopt never risks
@@ -785,10 +792,15 @@ async function main(argv) {
785
792
  const adopt = hasFlag('--adopt', args); // brownfield: LAYER onto an existing repo (ADR 0121)
786
793
  const accept = hasFlag('--accept', args) || hasFlag('--yes', args) || hasFlag('-y', args); // adopt pre-flight accept-gate (§Decision 5)
787
794
  const vendorCore = arg('--vendor-core', args); // greenfield: vendor + file:-pin a core .tgz, then generate the lockfile (ADR 0108 gap 1/4, task 2053)
795
+ const registryCore = hasFlag('--registry-core', args); // greenfield: caret pin + env-fed .npmrc → `npm ci` pulls the PRIVATE core from the registry (ADR 0134, task 2090)
788
796
 
789
797
  if (adopt && force) {
790
798
  console.error('init: --force is a greenfield-only overwrite; --adopt never overwrites, so --force is ignored.');
791
799
  }
800
+ if (vendorCore && registryCore) {
801
+ console.error('init: --vendor-core and --registry-core are mutually exclusive — the vendored tarball and the registry pull are two ways to resolve the same core dependency. Pick one.');
802
+ return 1;
803
+ }
792
804
 
793
805
  let raw;
794
806
  if (fromPath) {
@@ -841,7 +853,7 @@ async function main(argv) {
841
853
  console.log(`\nScaffolding "${spec.identity.productName}"${dryRun ? ' (dry run — nothing will be written or created)' : ''}:`);
842
854
  writeConfigs(spec, { dir, force, dryRun, coreDep });
843
855
 
844
- // Vendor the pinned @cloudbongos/core tarball + generate the lockfile so the scaffold is
856
+ // Vendor the pinned @bongos/core tarball + generate the lockfile so the scaffold is
845
857
  // `npm ci --omit=dev`-installable by the provisioner (ADR 0108 gap 1/4, task 2053). The
846
858
  // `npm install` here is the ONLY heavy step — it CREATES package-lock.json from the file:
847
859
  // tarball; the provisioner later runs `npm ci` against that committed lockfile.
@@ -849,14 +861,42 @@ async function main(argv) {
849
861
  if (dryRun) {
850
862
  console.log(`\n[dry-run] would vendor ${vendorPlan.tgz} → ${vendorPlan.vendorRel} and run \`npm install\` to generate package-lock.json`);
851
863
  } else {
852
- console.log('\nVendoring the pinned @cloudbongos/core + generating the lockfile:');
864
+ console.log('\nVendoring the pinned @bongos/core + generating the lockfile:');
853
865
  upgrade.vendorTarball({ instanceDir: dir, fromTgz: vendorPlan.tgz, targetVersion: vendorPlan.version });
854
866
  console.log(` ✓ vendored ${vendorPlan.vendorRel}`);
855
867
  const inst = upgrade.npmInstall(dir);
856
868
  if (!inst.ok) {
857
869
  console.error(` ! npm install failed (${inst.reason || 'exit ' + inst.code}) — package-lock.json not generated; run \`npm install\` in ${dir} by hand before provisioning.`);
858
870
  } else {
859
- console.log(' ✓ npm install → package-lock.json generated (node_modules/@cloudbongos/core installed)');
871
+ console.log(' ✓ npm install → package-lock.json generated (node_modules/@bongos/core installed)');
872
+ }
873
+ }
874
+ }
875
+
876
+ // Registry-install path (ADR 0134, task 2090): keep the caret pin writeConfigs wrote
877
+ // and add an env-fed .npmrc so `npm ci` pulls the PRIVATE @bongos/core from the
878
+ // registry. With NPM_TOKEN present we also generate package-lock.json (a reproducible
879
+ // `npm ci` later); without a token we still write the caret pin + .npmrc so the
880
+ // scaffold is registry-shaped, and say plainly that the private package 404s until a
881
+ // token is provided — the expected tokenless outcome the ADR calls to verify.
882
+ if (registryCore) {
883
+ if (dryRun) {
884
+ console.log(`\n[dry-run] would write .npmrc (env-fed @bongos auth) and ${hasNpmToken()
885
+ ? 'run `npm install` to generate package-lock.json from the registry'
886
+ : 'SKIP lockfile generation (no NPM_TOKEN — the private core 404s until one is provided)'}`);
887
+ } else {
888
+ console.log('\nWiring the registry-install path (ADR 0134):');
889
+ writeNpmrc(dir, { log: console.log });
890
+ if (hasNpmToken()) {
891
+ const inst = upgrade.npmInstall(dir);
892
+ if (!inst.ok) {
893
+ console.error(` ! npm install failed (${inst.reason || 'exit ' + inst.code}) — confirm NPM_TOKEN reaches the private @bongos org; package-lock.json not generated.`);
894
+ } else {
895
+ console.log(' ✓ npm install → package-lock.json generated (pulled @bongos/core from the registry)');
896
+ }
897
+ } else {
898
+ console.log(' • no NPM_TOKEN in the environment — wrote the caret pin + .npmrc but skipped lockfile generation.');
899
+ console.log(' Provide a read-only automation token (paid `bongos` org) as NPM_TOKEN before `npm ci`; without it the private package 404s cleanly.');
860
900
  }
861
901
  }
862
902
  }
@@ -13,7 +13,7 @@
13
13
  //
14
14
  // DISCIPLINE. Every migration SOURCE owns a distinct filename namespace, so stems can
15
15
  // never collide across sources and each source's allocator counts only its own numbers:
16
- // • CORE (the @cloudbongos/core package's migrations/*.sql) → core_NNN_slug.sql
16
+ // • CORE (the @bongos/core package's migrations/*.sql) → core_NNN_slug.sql
17
17
  // • MODULE (modules/<key>/migrations/*.sql) → <key>_NNN_slug.sql (key: dev-box → dev_box_)
18
18
  // • INSTANCE (the host repo's migrations/instance/*.sql) → NNN_slug.sql (BARE — the instance owns the bare namespace)
19
19
  //
@@ -351,6 +351,40 @@ function recordSignOff(key, signedBy, { modulesDir = MODULES_DIR } = {}) {
351
351
  return { ...record, path: path.relative(REPO_ROOT, file) };
352
352
  }
353
353
 
354
+ // Reads a previously-recorded sign-off (recordSignOff above), or null if the
355
+ // module has never been signed off. Never throws on a missing/garbled file —
356
+ // "no sign-off yet" is the expected common case, not an error.
357
+ function readSignOff(key, { modulesDir = MODULES_DIR } = {}) {
358
+ return readJson(path.join(modulesDir, key, '.upstream-signoff.json'));
359
+ }
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // bongos module submit <key> — ADR 0107 §1 upstreaming submission
363
+ // ---------------------------------------------------------------------------
364
+
365
+ // Packages a module for the upstreaming queue: its manifest + a full file
366
+ // snapshot (base64, so binary assets round-trip identically) + whatever
367
+ // sign-off is on disk. Pure fs read — no pre-check, no network; the caller
368
+ // (cmdSubmit locally, the POST /modules/:key/submit route server-side) is
369
+ // responsible for having already run checkModulePublishability() and for
370
+ // deciding whether a missing sign-off is fatal.
371
+ function packageModuleForSubmission(key, { modulesDir = MODULES_DIR } = {}) {
372
+ const dir = path.join(modulesDir, key);
373
+ const manifest = readJson(path.join(dir, 'module.json'));
374
+ if (!manifest) throw new Error(`modules/${key}/module.json missing or unreadable`);
375
+ let totalBytes = 0;
376
+ const files = listModuleFiles(dir)
377
+ .map((abs) => {
378
+ const rel = path.relative(dir, abs).split(path.sep).join('/');
379
+ const buf = fs.readFileSync(abs);
380
+ totalBytes += buf.length;
381
+ return { path: rel, contentBase64: buf.toString('base64'), bytes: buf.length };
382
+ })
383
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
384
+ const signOff = readSignOff(key, { modulesDir });
385
+ return { key, manifest, files, totalBytes, signOff };
386
+ }
387
+
354
388
  // ---------------------------------------------------------------------------
355
389
  // Commands
356
390
  // ---------------------------------------------------------------------------
@@ -445,6 +479,79 @@ function cmdCheck(args, { log = console.log, errlog = console.error, check = che
445
479
  return 0;
446
480
  }
447
481
 
482
+ // `bongos module submit <key>` — ADR 0107 §1. Runs the §4 pre-check + requires
483
+ // a sign-off (locally, for fast refusal without a network round trip), then
484
+ // POSTs to POST /modules/:key/submit — the SAME route the hall "Modules" tab's
485
+ // submit button calls (src/bongos/routes/modules.js). That route re-runs the
486
+ // pre-check against ITS OWN on-disk copy of the module and is the authoritative
487
+ // gate: a local pass here is a courtesy, not a guarantee, if the module's
488
+ // changes aren't committed + shipped + deployed yet.
489
+ async function cmdSubmit(args, {
490
+ log = console.log, errlog = console.error,
491
+ check = checkModulePublishability, sign = recordSignOff, readSign = readSignOff,
492
+ submit,
493
+ } = {}) {
494
+ const key = args.find((a) => !a.startsWith('-'));
495
+ if (!key) {
496
+ errlog('usage: bongos module submit <key> [--sign-off "Name <email>"]');
497
+ return 2;
498
+ }
499
+ const signIdx = args.indexOf('--sign-off');
500
+ const signedBy = signIdx !== -1 ? args[signIdx + 1] : null;
501
+
502
+ log(`bongos module submit — upstreaming "${key}" (ADR 0107 §1)`);
503
+
504
+ let result;
505
+ try { result = check(key); }
506
+ catch (e) { errlog(`bongos module submit: ${e.message}`); return 1; }
507
+
508
+ if (!result.ok) {
509
+ for (const f of result.findings) errlog(` ✗ ${f.kind}: ${f.detail}`);
510
+ errlog(`\nREFUSING: "${key}" is not clear to submit upstream. Fix the finding(s) above and re-run.`);
511
+ return 1;
512
+ }
513
+ log(' ✓ AGPL/publish pre-check passed.');
514
+
515
+ let signOff = readSign(key);
516
+ if (!signOff && signedBy) {
517
+ const record = sign(key, signedBy);
518
+ log(` ✓ Sign-off recorded — ${record.path}`);
519
+ signOff = readSign(key);
520
+ }
521
+ if (!signOff) {
522
+ errlog(`\nREFUSING: no DCO-style sign-off on file for "${key}". Pass --sign-off "Name <email>" (or run \`bongos module check ${key} --sign-off "Name <email>"\` first).`);
523
+ return 1;
524
+ }
525
+ log(` ✓ Sign-off on file — "${signOff.signOff.signedBy}", ${signOff.signOff.date}.`);
526
+
527
+ const doSubmit = submit || (async (k, body) => {
528
+ // Lazy require: cli-lib.js carries session/fetch plumbing that must NEVER
529
+ // load into the always-on server process — this file is also required by
530
+ // src/bongos/routes/modules.js for the pre-check it reuses. A real network
531
+ // call only happens when a human/agent actually invokes this CLI command.
532
+ const { cliClient, requireSession } = require('./cli-lib');
533
+ await requireSession();
534
+ const api = await cliClient();
535
+ return api.modules.postModulesKeySubmit({ key: k, body });
536
+ });
537
+
538
+ let res;
539
+ try { res = await doSubmit(key, { signed_by: signedBy || undefined }); }
540
+ catch (e) { errlog(`bongos module submit: request failed — ${e.message}`); return 1; }
541
+
542
+ if (!res.ok) {
543
+ const detail = (res.data && (res.data.message || res.data.error)) || JSON.stringify(res.data);
544
+ errlog(`\nREFUSING: the review queue rejected the submission (HTTP ${res.status}): ${detail}`);
545
+ if (res.status === 422) errlog('(the server re-runs the pre-check/sign-off check against its OWN deployed copy — commit + ship first if this module has local-only changes.)');
546
+ return 1;
547
+ }
548
+
549
+ const submission = res.data && res.data.submission;
550
+ log(`\n✓ Filed — submission id ${submission ? submission.id : '?'} for "${key}", status ${submission ? submission.status : 'pending'}.`);
551
+ log('Task 1771 (core-side review & accept flow) picks it up from here.');
552
+ return 0;
553
+ }
554
+
448
555
  function printHelp(out = console.log) {
449
556
  out('bongos module — scaffold + manage Cloud Bongos modules\n');
450
557
  out('Usage:');
@@ -452,14 +559,17 @@ function printHelp(out = console.log) {
452
559
  out(' bongos upgrade Pre-check enabled modules against the core version (fail-closed)');
453
560
  out(' bongos module check <key> [--sign-off "Name <email>"]');
454
561
  out(' AGPL/publish pre-check before upstreaming + record a DCO-style sign-off');
562
+ out(' bongos module submit <key> [--sign-off "Name <email>"]');
563
+ out(' Pre-check + sign-off, then file into the rank-gated upstreaming review queue');
455
564
  }
456
565
 
457
- function main(argv) {
566
+ async function main(argv) {
458
567
  const args = argv.slice(2);
459
568
  const sub = args[0];
460
569
  if (sub === 'new') return cmdNew(args.slice(1));
461
570
  if (sub === 'upgrade') return cmdUpgrade(args.slice(1));
462
571
  if (sub === 'check') return cmdCheck(args.slice(1));
572
+ if (sub === 'submit') return cmdSubmit(args.slice(1));
463
573
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
464
574
  printHelp();
465
575
  return sub ? 0 : 2;
@@ -470,13 +580,17 @@ function main(argv) {
470
580
  }
471
581
 
472
582
  module.exports = {
473
- main, cmdNew, cmdUpgrade, cmdCheck, printHelp,
583
+ main, cmdNew, cmdUpgrade, cmdCheck, cmdSubmit, printHelp,
474
584
  scaffoldModule, gatherUpgrade, evaluateUpgrade, buildManifest,
475
- checkModulePublishability, recordSignOff, DCO_STATEMENT,
585
+ checkModulePublishability, recordSignOff, readSignOff, DCO_STATEMENT,
586
+ listModuleFiles, packageModuleForSubmission,
476
587
  routeTemplate, claudeMdTemplate, titleize, camel, coreVersion,
477
588
  MODULES_DIR,
478
589
  };
479
590
 
480
591
  if (require.main === module) {
481
- process.exit(main(process.argv));
592
+ Promise.resolve(main(process.argv)).then(
593
+ (code) => process.exit(code),
594
+ (e) => { console.error(e); process.exit(1); }
595
+ );
482
596
  }
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ //
4
+ // scripts/gds/npmrc.js — env-fed `.npmrc` for installing the PRIVATE @bongos/* core
5
+ // from the npm registry (ADR 0134, task 2090). The single place that knows the
6
+ // registry host + scope + the `.npmrc` shape + the opt-in/token gates, shared by:
7
+ // • init.js — the greenfield `--registry-core` scaffold
8
+ // • provision.js — the standalone scaffold's registry path (opt-in, fail-soft)
9
+ // • upgrade.js — the `--registry <version>` update channel
10
+ //
11
+ // WHY private + env-fed. `@bongos/core` publishes as a SCOPED, PRIVATE package (ADR
12
+ // 0134): a private scoped package can only be installed with auth. Consumers
13
+ // authenticate with an `.npmrc` whose token EXPANDS FROM THE ENVIRONMENT at install
14
+ // time (`_authToken=${NPM_TOKEN}`) — so the committed file carries NO secret, and a
15
+ // tokenless install 404s cleanly (the expected pre-launch outcome).
16
+ //
17
+ // The vendored-tarball path (task 2053) stays the DEFAULT/fallback until this registry
18
+ // path is proven end-to-end on a real greenfield standup (ADR 0134 §Consequences).
19
+
20
+ const fs = require('node:fs');
21
+ const path = require('node:path');
22
+
23
+ const REGISTRY_HOST = 'registry.npmjs.org';
24
+ const CORE_SCOPE = '@bongos'; // the paid, canonical npm org (ADR 0134 scope correction, task 2092)
25
+ const TOKEN_ENV = 'NPM_TOKEN';
26
+ const REQUEST_ENV = 'PROVISION_CORE_FROM_REGISTRY';
27
+
28
+ // The `.npmrc` a scaffolded instance commits. The token is the LITERAL `${NPM_TOKEN}`
29
+ // (npm expands env vars in `.npmrc` values at read time), so the file is safe to
30
+ // commit — it holds no secret. Scoping @bongos to the registry keeps every unrelated
31
+ // install on the default public registry.
32
+ function npmrcContent({ scope = CORE_SCOPE, host = REGISTRY_HOST, tokenEnv = TOKEN_ENV } = {}) {
33
+ return [
34
+ `; Cloud Bongos — env-fed auth for the PRIVATE ${scope}/* packages (ADR 0134, task 2090).`,
35
+ `; The token EXPANDS FROM THE ENVIRONMENT at install time; nothing secret is committed here.`,
36
+ `; \`npm ci\` needs ${tokenEnv} set (a read-only automation token from the paid \`bongos\` org);`,
37
+ `; without it the private package 404s — that is the expected tokenless outcome.`,
38
+ `${scope}:registry=https://${host}/`,
39
+ `//${host}/:_authToken=\${${tokenEnv}}`,
40
+ '',
41
+ ].join('\n');
42
+ }
43
+
44
+ // Is a registry token present in the environment? The fail-soft gate: provisioning
45
+ // falls back to the vendored tarball when this is false, so a tokenless standup still
46
+ // works (never a broken instance).
47
+ function hasNpmToken(env = process.env) {
48
+ return !!String(env[TOKEN_ENV] || '').trim();
49
+ }
50
+
51
+ // Has the operator opted the standalone scaffold into the registry path? OFF by
52
+ // default — the vendored tarball stays the default until the registry path is proven
53
+ // end-to-end (ADR 0134). '1'/'true'/'yes'/'on' (case-insensitive) → true.
54
+ function registryCoreRequested(env = process.env) {
55
+ return /^(1|true|yes|on)$/i.test(String(env[REQUEST_ENV] || '').trim());
56
+ }
57
+
58
+ // Write the env-fed `.npmrc` into an instance dir. Default overwrites (a greenfield
59
+ // scaffold owns the file); pass overwrite:false to leave a pre-existing `.npmrc`
60
+ // untouched (an upgrade must not clobber a consumer's other registries). Returns the
61
+ // path (or null when skipped). Dry-run writes nothing.
62
+ function writeNpmrc(dir, { fsImpl = fs, dryRun = false, overwrite = true, log = () => {} } = {}) {
63
+ const p = path.join(dir, '.npmrc');
64
+ if (!overwrite && fsImpl.existsSync(p)) {
65
+ log(` • .npmrc already present — left untouched (add \`${CORE_SCOPE}:registry\` + \`_authToken=\${${TOKEN_ENV}}\` if the private core doesn't resolve).`);
66
+ return null;
67
+ }
68
+ if (dryRun) {
69
+ log(` [dry-run] would write ${p} (env-fed ${CORE_SCOPE} auth; token via $${TOKEN_ENV}, never committed)`);
70
+ return p;
71
+ }
72
+ fsImpl.writeFileSync(p, npmrcContent());
73
+ log(` ✓ wrote .npmrc (env-fed ${CORE_SCOPE} auth; token via $${TOKEN_ENV}, never committed)`);
74
+ return p;
75
+ }
76
+
77
+ module.exports = {
78
+ REGISTRY_HOST, CORE_SCOPE, TOKEN_ENV, REQUEST_ENV,
79
+ npmrcContent, hasNpmToken, registryCoreRequested, writeNpmrc,
80
+ };
81
+
82
+ // Tiny CLI: `node scripts/gds/npmrc.js [--print | --write <dir>]` — print the `.npmrc`
83
+ // body or materialize it into <dir>. Handy for a manual registry-standup rehearsal.
84
+ if (require.main === module) {
85
+ const args = process.argv.slice(2);
86
+ const writeIdx = args.indexOf('--write');
87
+ if (writeIdx !== -1 && args[writeIdx + 1]) {
88
+ const p = writeNpmrc(path.resolve(args[writeIdx + 1]), { log: console.log });
89
+ process.stdout.write(`${p}\n`);
90
+ } else {
91
+ process.stdout.write(npmrcContent());
92
+ }
93
+ }
@@ -22,28 +22,12 @@ const oauth = require(path.join(__dirname, 'oauth-secret.js')); // validateC
22
22
 
23
23
  // ── PURE core (exported for tests) ───────────────────────────────────────────
24
24
 
25
- // The canonical step sequence — the SINGLE source of truth the skill + hall wizard
26
- // mirror. `kind: 'human'` steps are the irreducibly-manual ones (UI-first prompts).
27
- // The sequence branches at exactly ONE leg — scaffold — into greenfield (`bongos init`,
28
- // a fresh empty repo) vs brownfield (`bongos init --adopt`, LAYER onto an existing repo;
29
- // ADR 0121 §Decision 1 / task 2046). Every other leg is identical, so passing `mode`
30
- // only relabels the github-repo / init / scaffold steps.
31
- function onboardPlan(mode = 'greenfield') {
32
- const adopt = mode === 'adopt';
33
- return [
34
- { key: 'name-address', kind: 'auto', label: 'Decide name + address (interview)' },
35
- { key: 'github-repo', kind: 'human', label: adopt ? 'Use your EXISTING GitHub repository' : 'Create the empty GitHub repository' },
36
- { key: 'dns-preflight', kind: 'auto', label: 'DNS token preflight — before the address step (task 1980)' },
37
- { key: 'init', kind: 'auto', label: adopt ? 'bongos init --adopt — LAYER onto the existing repo (pre-flight + accept)' : 'bongos init — scaffold the instance config' },
38
- { key: 'scaffold', kind: 'auto', label: adopt ? 'Layer config + pinned @cloudbongos/core onto the existing repo (no clobber)' : 'Scaffold the standalone repo (own repo + pinned @cloudbongos/core)' },
39
- { key: 'provision', kind: 'auto', label: 'Provision core + DB + address + service (standalone, task 1978)' },
40
- { key: 'oauth-app', kind: 'human', label: 'Wire GitHub sign-in — one-click GitHub App (task 2080), or a manual OAuth app' },
41
- { key: 'oauth-place', kind: 'auto', label: 'Place the OAuth secret — auto via the Manifest flow, or verified manual (task 1979)' },
42
- { key: 'first-sign-in', kind: 'human', label: 'First sign-in (seats the founding Archon)' },
43
- { key: 'verify', kind: 'auto', label: 'Verify /healthz auth_configured' },
44
- { key: 'brand', kind: 'auto', label: 'Brand the instance look' },
45
- ];
46
- }
25
+ // The canonical step sequence now lives in ONE place — the provisioning module — so the
26
+ // CLI (here), the branded /create page, and the hall wizard can't drift (task 2085; goal
27
+ // 35 / criterion 165: "the canonical step sequence is the source of truth; skill/CLI/hall
28
+ // are forms over it"). The web forms read it over GET /provisioning/onboard-plan; the CLI
29
+ // requires it directly. Re-exported below so `bongos onboard` + tests keep the same API.
30
+ const { onboardPlan } = require(path.join(__dirname, '..', '..', 'modules', 'provisioning', 'onboard-plan.js'));
47
31
 
48
32
  // The exact GitHub OAuth-app values for a domain. The callback is PINNED to the apex
49
33
  // /api/gds/auth/web/callback (auth.js WEB_REDIRECT_ORIGIN — a sign-in begun on a
@@ -54,6 +38,10 @@ function oauthAppInstructions(domain) {
54
38
  location: "GitHub → your ACCOUNT's Settings → Developer settings → OAuth Apps → New OAuth App",
55
39
  homepageUrl: origin,
56
40
  callbackUrl: `${origin}/api/gds/auth/web/callback`,
41
+ // GitHub disables Device Flow on a new OAuth App by DEFAULT, but the terminal
42
+ // `bongos login` uses it — leaving it off makes CLI sign-in fail with
43
+ // device_flow_disabled (the 2026-07-08 demo login-500). Must be enabled. (task 2127)
44
+ enableDeviceFlow: 'Check "Enable Device Flow" on the app page and save — off by default; the terminal `bongos login` needs it.',
57
45
  };
58
46
  }
59
47
 
@@ -149,13 +137,13 @@ async function run() {
149
137
  // Step 5 — the standalone repo (greenfield scaffolds it; adopt already layered it in ③)
150
138
  if (adopt) {
151
139
  log('\n④ Your repo is now the standalone instance (ADR 0108):');
152
- log(' `bongos init --adopt` already layered config/ + .claude/ + the pinned @cloudbongos/core dep onto it.');
140
+ log(' `bongos init --adopt` already layered config/ + .claude/ + the pinned @bongos/core dep onto it.');
153
141
  log(` Commit + push ${a.repoOwner}/${a.repoName}. That checkout is what the service runs.`);
154
142
  await ask(rl, 'Press Enter once the layered repo is committed + pushed…');
155
143
  } else {
156
144
  log('\n④ Scaffold the STANDALONE repo (ADR 0108):');
157
145
  log(` Clone ${a.repoOwner}/${a.repoName} to the control-plane standalone root, add the config/ + .claude/`);
158
- log(' from init, pin @cloudbongos/core in its package.json, commit + push. That checkout is what the service runs.');
146
+ log(' from init, pin @bongos/core in its package.json, commit + push. That checkout is what the service runs.');
159
147
  await ask(rl, 'Press Enter once the standalone repo is pushed…');
160
148
  }
161
149
 
@@ -180,6 +168,7 @@ async function run() {
180
168
  log(' (Developer settings is under your ACCOUNT, not the repository.)');
181
169
  log(` Homepage URL: ${oa.homepageUrl}`);
182
170
  log(` Authorization callback URL: ${oa.callbackUrl}`);
171
+ log(` Enable Device Flow: ✅ ${oa.enableDeviceFlow}`);
183
172
  log(' Copy the Client ID; Generate a new client secret and copy it (shown once).');
184
173
  const clientId = await ask(rl, 'Paste the OAuth Client ID:');
185
174
  const clientSecret = await ask(rl, 'Paste the OAuth Client Secret:');