@bongos/core 1.19.1032 → 1.19.1034

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 (151) hide show
  1. package/.bongos-core.json +322 -117
  2. package/.claude/skills/builder-start/SKILL.md +1 -1
  3. package/.claude/skills/grade-audit/SKILL.md +1 -1
  4. package/.claude/skills/grade-recover/SKILL.md +2 -1
  5. package/.claude/skills/grader-health/SKILL.md +5 -1
  6. package/.claude/skills/merge-mode/SKILL.md +1 -1
  7. package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
  8. package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
  9. package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
  10. package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
  11. package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
  12. package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
  13. package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
  14. package/docs/adr/README.md +4 -4
  15. package/docs/api/openapi.json +1 -1
  16. package/docs/architecture.md +4 -3
  17. package/docs/canonical-permissions.md +2 -2
  18. package/docs/copy-inventory.md +34 -30
  19. package/docs/copy-registry.json +69 -33
  20. package/docs/file-map.md +4 -3
  21. package/docs/module-api-changelog.md +4 -0
  22. package/docs/recipes/autobongos-windows-host.md +3 -1
  23. package/docs/recipes/local-dev.md +45 -3
  24. package/docs/recipes/packaging-the-core.md +3 -1
  25. package/docs/recipes/redteam-patrol.md +2 -2
  26. package/docs/recipes/upgrading-the-core.md +1 -1
  27. package/modules/autonomy/cadence.js +52 -0
  28. package/modules/autonomy/gauge.js +57 -9
  29. package/modules/government/protected-surfaces.json +1 -0
  30. package/modules/grading/grade-engine.js +267 -0
  31. package/modules/grading/grader-subagent.js +4 -3
  32. package/modules/grading/grader-workers/index.js +67 -11
  33. package/modules/grading/grader-workers/worker-verdict.js +32 -2
  34. package/modules/grading/grader.js +109 -88
  35. package/modules/hall-ui/public/deploy.js +35 -0
  36. package/modules/hall-ui/public/project-settings.css +7 -0
  37. package/modules/hall-ui/records/builders-roster.md +10 -0
  38. package/modules/hall-ui/records/catalog.md +10 -0
  39. package/modules/hall-ui/records/collaboration.md +14 -0
  40. package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
  41. package/modules/hall-ui/records/oversight-family.md +19 -0
  42. package/modules/hall-ui/records/panel-family.md +19 -0
  43. package/modules/hall-ui/records/reading-rooms.md +12 -0
  44. package/modules/hall-ui/records/record-family.md +13 -0
  45. package/modules/hall-ui/records/reference-room.md +9 -0
  46. package/modules/hall-ui/records/roster-and-ledger.md +9 -0
  47. package/modules/hall-ui/records/sky.md +23 -0
  48. package/modules/lifecycle/dependency-advisory.js +2 -1
  49. package/modules/lifecycle/routes/tasks.js +19 -4
  50. package/modules/lifecycle/ship-preflight.js +91 -0
  51. package/modules/provisioning/control-plane-guard.js +14 -13
  52. package/modules/provisioning/core-upgrade.js +30 -17
  53. package/modules/provisioning/public-refusal.js +44 -0
  54. package/modules/provisioning/routes/disconnect.js +2 -1
  55. package/modules/provisioning/routes/env-manifest.js +5 -4
  56. package/modules/provisioning/routes/provisioning.js +16 -15
  57. package/modules/provisioning/routes/repo-private.js +2 -1
  58. package/package-lock.json +2 -2
  59. package/package.json +1 -1
  60. package/release-notes.json +7481 -0
  61. package/scripts/gds/address-park-sweep.sh +3 -1
  62. package/scripts/gds/autobongos-run.js +91 -3
  63. package/scripts/gds/backfill-cost.js +20 -9
  64. package/scripts/gds/baseline-staleness.js +1 -1
  65. package/scripts/gds/claim.js +2 -2
  66. package/scripts/gds/credential-presence.js +281 -0
  67. package/scripts/gds/db-backup-nightly.sh +9 -4
  68. package/scripts/gds/db-backup-offsite.sh +4 -2
  69. package/scripts/gds/deploy-main-audit-gate.sh +11 -4
  70. package/scripts/gds/dev-lib.js +212 -5
  71. package/scripts/gds/dev.js +102 -50
  72. package/scripts/gds/doctor.js +3 -2
  73. package/scripts/gds/grade-correlation-audit.js +280 -0
  74. package/scripts/gds/grade-replay.js +237 -0
  75. package/scripts/gds/hooks/pre-push.js +7 -3
  76. package/scripts/gds/main-audit.js +19 -10
  77. package/scripts/gds/memory-map.js +3 -1
  78. package/scripts/gds/onboarding-config.js +3 -2
  79. package/scripts/gds/package-core.js +72 -1
  80. package/scripts/gds/plain-cards.js +4 -0
  81. package/scripts/gds/preflight.js +12 -4
  82. package/scripts/gds/provision-core-upgrade.js +282 -80
  83. package/scripts/gds/provision-repo-auth.js +15 -0
  84. package/scripts/gds/provision.js +1 -1
  85. package/scripts/gds/publish-credential-check.js +3 -0
  86. package/scripts/gds/push-main.js +14 -3
  87. package/scripts/gds/push-path-brief.js +3 -2
  88. package/scripts/gds/redteam-patrol.js +4 -4
  89. package/scripts/gds/release-notes.js +271 -0
  90. package/scripts/gds/run-routine.js +18 -5
  91. package/scripts/gds/run-unit-tests.js +14 -3
  92. package/scripts/gds/seed-board-room-tasks.js +1 -1
  93. package/scripts/gds/session-cost.js +12 -3
  94. package/scripts/gds/ship-deploy-target.js +11 -6
  95. package/scripts/gds/ship-flow.js +5 -3
  96. package/scripts/gds/ship-grade-steps.js +33 -0
  97. package/scripts/gds/ship-land.js +241 -16
  98. package/scripts/gds/ship-merge.js +3 -2
  99. package/scripts/gds/ship-preflight-steps.js +86 -11
  100. package/scripts/gds/ship.js +1 -1
  101. package/scripts/gds/stray-unit-sweep.sh +3 -1
  102. package/scripts/gds/upgrade-migrate.js +67 -0
  103. package/scripts/gds/upgrade.js +9 -7
  104. package/scripts/hall-preview/README.md +20 -3
  105. package/scripts/hall-preview/live-identity.js +1 -1
  106. package/scripts/hall-preview/refresh-fixtures.js +71 -11
  107. package/scripts/hall-preview/scrub-identity.js +296 -0
  108. package/src/bongos/routes/security.js +3 -3
  109. package/src/build-info.js +4 -2
  110. package/src/module-api.js +1 -1
  111. package/tests/adr_0089_scope.mjs +70 -0
  112. package/tests/adr_0162_citation.mjs +184 -0
  113. package/tests/autobongos_cadence.mjs +74 -0
  114. package/tests/autobongos_loop.mjs +114 -0
  115. package/tests/autonomy_gauge.mjs +56 -0
  116. package/tests/consumer_layout_boot.mjs +363 -31
  117. package/tests/core_upgrade_door.mjs +40 -6
  118. package/tests/core_upgrade_runner.mjs +329 -5
  119. package/tests/credential_presence.mjs +153 -0
  120. package/tests/db_backup_nightly.mjs +21 -3
  121. package/tests/db_backup_offsite.mjs +81 -0
  122. package/tests/deploy_audit_gate.mjs +5 -2
  123. package/tests/deploy_page_projects.mjs +63 -2
  124. package/tests/dev_lib.mjs +2 -1
  125. package/tests/dev_windows.mjs +324 -0
  126. package/tests/env_knobs_neutral_prefix.mjs +203 -0
  127. package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
  128. package/tests/government_protected_surfaces.mjs +10 -0
  129. package/tests/grade_correlation_audit.mjs +159 -0
  130. package/tests/grade_engine.mjs +249 -0
  131. package/tests/grade_engine_replay.mjs +99 -0
  132. package/tests/grader_prompt_window_bound.mjs +132 -0
  133. package/tests/grader_unbacked_score.mjs +194 -0
  134. package/tests/hall_preview_scrub.mjs +214 -0
  135. package/tests/hall_records.mjs +85 -0
  136. package/tests/helpers.mjs +25 -0
  137. package/tests/instance_db_migrated_on_code_move.mjs +8 -1
  138. package/tests/integration_parked_ratchet.mjs +24 -1
  139. package/tests/plain_cards.mjs +1 -1
  140. package/tests/provisioning_public_refusal.mjs +195 -0
  141. package/tests/publish_status_branch.mjs +128 -0
  142. package/tests/redteam_patrol.mjs +1 -1
  143. package/tests/release_notes.mjs +327 -0
  144. package/tests/shell_stat_portability.mjs +49 -0
  145. package/tests/ship_land_conflict.mjs +181 -0
  146. package/tests/ship_land_red_pr.mjs +236 -0
  147. package/tests/ship_preflight.mjs +226 -3
  148. package/tests/start_env_guard.mjs +16 -0
  149. package/tests/sweep_app_account.mjs +15 -2
  150. package/tests/upgrade.mjs +20 -1
  151. package/tests/upgrade_migrate.mjs +195 -0
@@ -35,6 +35,9 @@ const DEFAULT_PORT = 3000;
35
35
  // scripts/gds/ -> the core package root (holds src/platform-server.js + scripts/migrate.sh).
36
36
  const CORE_ROOT = path.resolve(__dirname, '..', '..');
37
37
 
38
+ // The one role the Windows Postgres installer creates (task 1002707).
39
+ const WINDOWS_PG_USER = 'postgres';
40
+
38
41
  function readJsonSafe(p, f = fs) {
39
42
  try { return JSON.parse(f.readFileSync(p, 'utf8')); }
40
43
  catch { return null; }
@@ -102,9 +105,10 @@ function deriveDbName({ brandingDbName, instanceRoot, override } = {}) {
102
105
  }
103
106
 
104
107
  // Resolve every parameter a local run needs, from branding + modules + env + argv.
105
- // Pure: all IO comes through injected deps (f/env/userInfo/argv). PGHOST defaults to
106
- // TCP 'localhost' (NOT the Linux peer socket the pool defaults to) so a Homebrew Mac
107
- // Postgres is reachable; an explicit PGHOST/PGUSER in the environment still wins.
108
+ // Pure: all IO comes through injected deps (f/env/userInfo/argv/platform). PGHOST
109
+ // defaults to TCP 'localhost' (NOT the Linux peer socket the pool defaults to) so a
110
+ // Homebrew Mac Postgres is reachable; an explicit PGHOST/PGUSER in the environment
111
+ // still wins.
108
112
  function resolveRunParams({
109
113
  instanceRoot,
110
114
  coreRoot = CORE_ROOT,
@@ -113,6 +117,7 @@ function resolveRunParams({
113
117
  userInfo = os.userInfo,
114
118
  argv = [],
115
119
  mode = null,
120
+ platform = process.platform,
116
121
  } = {}) {
117
122
  const facts = loadBrandingFacts({ instanceRoot, coreRoot, f });
118
123
  const modules = readJsonSafe(path.join(instanceRoot, 'config', 'modules.json'), f) || {};
@@ -128,6 +133,10 @@ function resolveRunParams({
128
133
  const dbName = deriveDbName({ brandingDbName: facts.brandingDbName, instanceRoot, override: dbOverride });
129
134
 
130
135
  let pgUser = env.PGUSER;
136
+ // Windows (task 1002707): the Postgres installer creates exactly one role,
137
+ // `postgres`. Your Windows login name is never a role there, so defaulting to
138
+ // it would fail every connection with 'role "<you>" does not exist'.
139
+ if (!pgUser && platform === 'win32') pgUser = WINDOWS_PG_USER;
131
140
  if (!pgUser) { try { pgUser = userInfo().username; } catch { pgUser = undefined; } }
132
141
 
133
142
  const publicOrigin = facts.publicOrigin.replace(/\/+$/, '');
@@ -307,6 +316,195 @@ function depsInstallPlan({ instanceRoot, f = fs } = {}) {
307
316
  : { needed: true, cmd: 'install', args: ['install'] };
308
317
  }
309
318
 
319
+ // ========================= WINDOWS (task 1002707) =========================
320
+ // `bongos dev` is the local-hall path every project without an address is told to
321
+ // run (ADR 0323 made bring-your-own-host the free default), so a Windows owner has
322
+ // to be able to one-command it too. Four things differ on Windows, and each is a
323
+ // pure decision here so tests/dev_windows.mjs proves it on any OS:
324
+ // 1. npm is a .cmd shim, which a plain spawn cannot start;
325
+ // 2. the bash for migrate.sh is Git Bash, and the `bash.exe` on PATH may be WSL's
326
+ // launcher, which cannot reach a Windows Postgres or node;
327
+ // 3. the Postgres installer neither puts psql on PATH nor creates a role named
328
+ // after you: its one role is `postgres`, behind the password chosen at install;
329
+ // 4. Postgres runs as a Windows service, not under Homebrew or systemd.
330
+ // Docker (docs/recipes/self-host.md) stays the fallback when none of this fits.
331
+ // Paths are built with path.win32 so the answers are Windows-shaped on any host.
332
+
333
+ // Windows env names are case-insensitive (`Path`, `ProgramFiles`), but a copied or
334
+ // injected env is a plain object. Read it the way Windows would.
335
+ function envGet(env, name) {
336
+ if (!env) return undefined;
337
+ if (env[name] !== undefined) return env[name];
338
+ const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
339
+ return key === undefined ? undefined : env[key];
340
+ }
341
+
342
+ // How to run `npm <args>`. On Windows `npm` is `npm.cmd`, and Node will not spawn a
343
+ // .cmd without a shell: bare `npm` is ENOENT and `npm.cmd` is EINVAL since the
344
+ // CVE-2024-27980 fix (docs/recipes/windows-builders.md). Prefer npm's own entry
345
+ // point run by node (npm-cli.js ships beside node.exe), which needs no shell at
346
+ // all; else fall back to the shim through cmd.exe, as ONE command string so Node
347
+ // never concatenates an args array unescaped (DEP0190). The args are fixed verbs
348
+ // ('ci' / 'install'), never user input. Mac/Linux keep the plain spawn.
349
+ function npmInvocation(args = [], { platform = process.platform, execPath = process.execPath, exists = fs.existsSync } = {}) {
350
+ if (platform !== 'win32') return { cmd: 'npm', args: [...args], shell: false };
351
+ const npmCli = path.win32.join(path.win32.dirname(execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
352
+ if (exists(npmCli)) return { cmd: execPath, args: [npmCli, ...args], shell: false };
353
+ return { cmd: ['npm', ...args].join(' '), args: [], shell: true };
354
+ }
355
+
356
+ // Is this bash.exe WSL's launcher? C:\Windows\System32\bash.exe (and the
357
+ // WindowsApps alias) start a Linux distro that cannot see Windows' node, psql or
358
+ // PATH, so migrate.sh would "run" there and fail naming nothing useful.
359
+ function isWslBashLauncher(p) {
360
+ const s = String(p).replace(/\\/g, '/').toLowerCase();
361
+ return /\/windows\/(system32|sysnative)\/bash\.exe$/.test(s) || s.includes('/windowsapps/');
362
+ }
363
+
364
+ // Which bash runs migrate.sh. Mac/Linux: `bash` on PATH, as before. Windows: Git
365
+ // Bash, the shell Windows builders already use and the one migrate.sh is written to
366
+ // handle. Looked for cheapest-to-trust first: the Git Bash this terminal runs in
367
+ // (EXEPATH), Git's install folders, beside the `git` on PATH, then any non-WSL bash
368
+ // on PATH. Returns { ok, bin, source }, or { ok: false, wslOnly } when nothing
369
+ // usable exists (wslOnly = the only bash seen was WSL's, which deserves a sentence).
370
+ function resolveBash({ platform = process.platform, env = process.env, has = () => false, whereAll = () => [], exists = fs.existsSync } = {}) {
371
+ if (platform !== 'win32') return has('bash') ? { ok: true, bin: 'bash', source: 'PATH' } : { ok: false, wslOnly: false };
372
+ const w = path.win32;
373
+ const candidates = [];
374
+ const exe = envGet(env, 'EXEPATH'); // set by Git Bash: its install root or its bin folder
375
+ if (exe) candidates.push([w.join(exe, 'bash.exe'), 'Git Bash'], [w.join(exe, 'bin', 'bash.exe'), 'Git Bash']);
376
+ for (const base of [envGet(env, 'ProgramFiles'), envGet(env, 'ProgramW6432'), envGet(env, 'ProgramFiles(x86)')]) {
377
+ if (base) candidates.push([w.join(base, 'Git', 'bin', 'bash.exe'), 'Git for Windows']);
378
+ }
379
+ const local = envGet(env, 'LOCALAPPDATA');
380
+ if (local) candidates.push([w.join(local, 'Programs', 'Git', 'bin', 'bash.exe'), 'Git for Windows']);
381
+ // git.exe sits in <root>\cmd, <root>\bin or <root>\mingw64\bin; bash in <root>\bin.
382
+ for (const git of whereAll('git')) {
383
+ let dir = w.dirname(git);
384
+ for (let i = 0; i < 3; i++) { candidates.push([w.join(dir, 'bin', 'bash.exe'), 'Git for Windows']); dir = w.dirname(dir); }
385
+ }
386
+ let wslSeen = false;
387
+ for (const b of whereAll('bash')) {
388
+ if (isWslBashLauncher(b)) wslSeen = true;
389
+ else candidates.push([b, 'PATH']);
390
+ }
391
+ for (const [bin, source] of candidates) {
392
+ if (exists(bin)) return { ok: true, bin, source };
393
+ }
394
+ return { ok: false, wslOnly: wslSeen };
395
+ }
396
+
397
+ // migrate.sh's path as Git Bash must receive it. The script finds the core with
398
+ // `dirname "$0"`, and bash's dirname splits on `/` only: handed C:\x\scripts\migrate.sh
399
+ // it answers ".", so the script would look for migrations under whatever folder it
400
+ // was started in. Forward slashes (C:/x/scripts/migrate.sh) keep that working.
401
+ function bashScriptPath(p, platform = process.platform) {
402
+ return platform === 'win32' ? String(p).replace(/\\/g, '/') : p;
403
+ }
404
+
405
+ // The Postgres Windows installer (postgresql.org/download/windows) puts the server
406
+ // and its client tools in %ProgramFiles%\PostgreSQL\<version>\bin but does NOT add
407
+ // that folder to PATH, so `psql` looks missing on a machine where Postgres is
408
+ // installed and running. Find those folders, newest first, so dev.js can put the
409
+ // tools on PATH for this run instead of asking an owner to edit PATH by hand. Each
410
+ // entry also names the installer's Windows service (postgresql-x64-<version>).
411
+ function windowsPgInstalls({ env = process.env, readdir = (d) => fs.readdirSync(d), exists = fs.existsSync } = {}) {
412
+ const w = path.win32;
413
+ const seen = new Set();
414
+ const out = [];
415
+ for (const base of [envGet(env, 'ProgramFiles'), envGet(env, 'ProgramW6432'), envGet(env, 'ProgramFiles(x86)')]) {
416
+ if (!base) continue;
417
+ const root = w.join(base, 'PostgreSQL');
418
+ let entries;
419
+ try { entries = readdir(root); } catch { continue; }
420
+ for (const name of entries) {
421
+ const version = Number.parseFloat(String(name));
422
+ if (!Number.isFinite(version) || version <= 0) continue;
423
+ const bin = w.join(root, String(name), 'bin');
424
+ if (seen.has(bin.toLowerCase()) || !exists(w.join(bin, 'psql.exe'))) continue;
425
+ seen.add(bin.toLowerCase());
426
+ out.push({ version, bin, service: `postgresql-x64-${name}` });
427
+ }
428
+ }
429
+ return out.sort((a, b) => b.version - a.version);
430
+ }
431
+
432
+ // psql/createdb connection flags: -h/-U only when we have them (libpq fills the
433
+ // rest). On Windows add -w (never prompt): the installer's `postgres` role is
434
+ // password-only, and a console prompt is a trap there. psql would take a typed
435
+ // password that migrations, the server and every later run never see. Failing fast
436
+ // lets dev.js say which password to set (pgAuthHint). pg_isready has no -w flag, so
437
+ // its caller passes { forReady: true }.
438
+ function pgClientArgs(params, { platform = process.platform, forReady = false } = {}) {
439
+ const a = ['-h', params.pgHost];
440
+ if (params.pgUser) a.push('-U', params.pgUser);
441
+ if (platform === 'win32' && !forReady) a.push('-w');
442
+ return a;
443
+ }
444
+
445
+ // The shared "give it your Postgres password" lines (Windows installs only).
446
+ const WINDOWS_PGPASSWORD_LINES = [
447
+ 'Give bongos dev that password in the terminal you run it from, then re-run it:',
448
+ ' PowerShell: $env:PGPASSWORD = "<that password>"; npx bongos dev',
449
+ ' Git Bash: PGPASSWORD="<that password>" npx bongos dev',
450
+ 'To keep it for next time: setx PGPASSWORD "<that password>" (then open a new terminal).',
451
+ ];
452
+
453
+ // What to tell someone whose Postgres client tools are missing, per platform, so a
454
+ // Windows owner gets Windows steps instead of a Homebrew line.
455
+ function pgInstallHint(platform = process.platform) {
456
+ if (platform === 'darwin') return ['brew install postgresql@16 && brew services start postgresql@16'];
457
+ if (platform === 'win32') {
458
+ return [
459
+ 'Download the Windows installer from https://www.postgresql.org/download/windows/ and run it.',
460
+ 'Keep the default port (5432), and note the password it asks you to choose for the "postgres" user.',
461
+ ...WINDOWS_PGPASSWORD_LINES,
462
+ 'Rather not install Postgres? The Docker path in docs/recipes/self-host.md runs everything in containers.',
463
+ ];
464
+ }
465
+ return ['(Debian/Ubuntu) sudo apt-get install -y postgresql (or your distro’s package)'];
466
+ }
467
+
468
+ // How to start Postgres by hand when dev.js could not, per platform.
469
+ function pgStartHint(platform = process.platform, { pgInstalls = [] } = {}) {
470
+ if (platform === 'darwin') return ['brew services start postgresql@16'];
471
+ if (platform === 'win32') {
472
+ const svc = (pgInstalls[0] && pgInstalls[0].service) || 'postgresql-x64-16';
473
+ return [
474
+ `Press Win+R, type services.msc, find "${svc}" in the list, and click Start.`,
475
+ `Or, in a PowerShell opened with "Run as administrator": Start-Service ${svc}`,
476
+ ];
477
+ }
478
+ return ['sudo service postgresql start'];
479
+ }
480
+
481
+ // Windows only: a createdb/psql refused on authentication almost always means
482
+ // PGPASSWORD is unset or wrong for the installer's `postgres` role. [] otherwise,
483
+ // so Mac/Linux output is unchanged.
484
+ function pgAuthHint(platform = process.platform, stderr = '') {
485
+ if (platform !== 'win32') return [];
486
+ if (!/password authentication failed|no password supplied|role .* does not exist/i.test(String(stderr))) return [];
487
+ return [
488
+ 'Postgres on Windows has one user, "postgres", protected by the password you chose when you installed it.',
489
+ ...WINDOWS_PGPASSWORD_LINES,
490
+ 'Using a different Postgres user? Set PGUSER to it the same way.',
491
+ ];
492
+ }
493
+
494
+ // No usable bash for migrations. Windows only (Mac and Linux ship bash); names Git
495
+ // for Windows first and Docker as the fallback, and explains a WSL-only bash.
496
+ function bashMissingHint(platform = process.platform, { wslOnly = false } = {}) {
497
+ if (platform !== 'win32') return [];
498
+ const lines = [];
499
+ if (wslOnly) lines.push('The only bash found belongs to WSL (C:\\Windows\\System32\\bash.exe). It runs Linux, so it cannot reach a Windows Postgres or Node.');
500
+ lines.push(
501
+ 'Install Git for Windows from https://git-scm.com/download/win (it includes Git Bash, which runs the migrations),',
502
+ 'then open a new terminal and re-run `npx bongos dev`.',
503
+ 'Or skip the local tools entirely with the Docker path: docs/recipes/self-host.md.',
504
+ );
505
+ return lines;
506
+ }
507
+
310
508
  // ===================== SERVER MODE (task 1002398, ADR 0162) =====================
311
509
  // The SAME boot path — deps -> Postgres -> createdb -> migrate -> env -> server ->
312
510
  // health — retargeted at a real host. Four decisions differ from local, and every
@@ -436,13 +634,19 @@ function withSudo(step, { isRoot = false, sudo = true } = {}) {
436
634
  // Ordered candidates for "start Postgres", best-first for the platform. dev.js
437
635
  // tries each until pg_isready passes. Homebrew keeps its own branch there (the
438
636
  // formula name has to be discovered at runtime); everything else is decided here.
439
- function pgStartPlan({ platform = process.platform, has = () => false, env = process.env, isRoot = false } = {}) {
637
+ // On Windows, Postgres is a Windows service per installed version (pgInstalls, from
638
+ // windowsPgInstalls). `net start` needs an Administrator terminal; from an ordinary
639
+ // one it fails fast with "Access is denied", and dev.js moves on to pgStartHint.
640
+ function pgStartPlan({ platform = process.platform, has = () => false, env = process.env, isRoot = false, pgInstalls = [] } = {}) {
440
641
  const out = [];
441
642
  if (platform === 'darwin' && has('brew')) out.push({ cmd: 'brew', args: ['services', 'start'], needsFormula: true, label: 'Homebrew' });
442
643
  if (platform === 'linux') {
443
644
  if (has('systemctl')) out.push(withSudo({ cmd: 'systemctl', args: ['start', 'postgresql'], label: 'systemd' }, { isRoot }));
444
645
  else if (has('service')) out.push(withSudo({ cmd: 'service', args: ['postgresql', 'start'], label: 'service' }, { isRoot }));
445
646
  }
647
+ if (platform === 'win32') {
648
+ for (const inst of pgInstalls) out.push({ cmd: 'net', args: ['start', inst.service], label: `Windows service ${inst.service}` });
649
+ }
446
650
  if (has('pg_ctl') && env.PGDATA) out.push({ cmd: 'pg_ctl', args: ['start', '-w', '-D', env.PGDATA], label: 'pg_ctl' });
447
651
  return out;
448
652
  }
@@ -502,9 +706,12 @@ async function publicHealth(params, deps = {}) {
502
706
  }
503
707
 
504
708
  module.exports = {
505
- HOST, DEFAULT_PORT, CORE_ROOT,
709
+ HOST, DEFAULT_PORT, CORE_ROOT, WINDOWS_PG_USER,
506
710
  readJsonSafe, loadBrandingFacts, parsePort, sanitizeDbName, deriveDbName,
507
711
  resolveRunParams, buildEnv, authWillWork, depsInstallPlan,
712
+ // Windows (task 1002707). envGet stays private — a lookup helper, not a surface.
713
+ npmInvocation, isWslBashLauncher, resolveBash, bashScriptPath, windowsPgInstalls,
714
+ pgClientArgs, pgInstallHint, pgStartHint, pgAuthHint, bashMissingHint,
508
715
  slug, pidPath, logPath, readPid, pidAlive, isRunning, serverEntry,
509
716
  startDetached, stop, health, argFrom,
510
717
  // server mode (task 1002398). withSudo/quoteIdent/shellJoin stay private —
@@ -26,6 +26,9 @@
26
26
  // * PORT is forced to branding's port (the server ignores branding — platform-server.js:44).
27
27
  // * PGDATABASE is set for BOTH migrate.sh and the server (fixes the pool.js/migrate.sh
28
28
  // default split); PGHOST defaults to TCP localhost (not the Linux peer socket).
29
+ // * Windows (task 1002707) runs the same steps: npm via its own entry point, the
30
+ // migrations in Git Bash, the Postgres installer's bin folder and `postgres` role.
31
+ // Every Windows decision is a pure function in dev-lib.js's WINDOWS section.
29
32
 
30
33
  const cp = require('node:child_process');
31
34
  const fs = require('node:fs');
@@ -70,7 +73,8 @@ Usage:
70
73
 
71
74
  What it does (idempotent — safe to re-run):
72
75
  1. Install dependencies if this checkout has none
73
- 2. Make sure Postgres is running (Homebrew on a Mac, systemd/service on a host)
76
+ 2. Make sure Postgres is running (Homebrew on a Mac, systemd/service on a host,
77
+ the Postgres Windows service on Windows)
74
78
  3. Create this instance's database if it doesn't exist
75
79
  4. Build/refresh its tables (migrations)
76
80
  5. Start the server on the port in config/branding.json
@@ -84,33 +88,44 @@ What --server changes:
84
88
  • it checks the PUBLIC origin, not just loopback, and never opens a browser
85
89
  Extra flags: --bind <addr>, --user <unix-user> (who the service runs as).
86
90
 
87
- Needs Postgres + Node >= 22 installed. macOS/Linux (uses bash for migrations;
88
- on Windows use WSL or the Docker path in docs/recipes/self-host.md).
89
- Server walkthrough: docs/recipes/server-bring-up.md`);
91
+ Needs Postgres + Node >= 22 installed. Runs on macOS, Linux and Windows.
92
+ On Windows the migrations run in Git Bash (it comes with Git for Windows), and
93
+ bongos dev connects to Postgres as "postgres" with the password you chose when
94
+ installing it (set PGPASSWORD). Fallback anywhere: the Docker path in docs/recipes/self-host.md.
95
+ Walkthroughs: docs/recipes/local-dev.md, docs/recipes/server-bring-up.md`);
90
96
  }
91
97
 
92
- // psql/createdb take -h/-U only when we have them; libpq fills the rest.
93
- function pgArgs(params) {
94
- const a = ['-h', params.pgHost];
95
- if (params.pgUser) a.push('-U', params.pgUser);
96
- return a;
97
- }
98
+ // psql/createdb connection flags (dev-lib.pgClientArgs decides; -w on Windows so a
99
+ // password prompt can never hang a run).
100
+ function pgArgs(params, opts) { return dev.pgClientArgs(params, opts); }
98
101
 
99
102
  function pgReady(params) {
100
- return run('pg_isready', [...pgArgs(params), '-q']).status === 0;
103
+ return run('pg_isready', [...pgArgs(params, { forReady: true }), '-q']).status === 0;
104
+ }
105
+
106
+ // Every path `where` prints for a binary (Windows lists all matches, in PATH order).
107
+ function whereAll(bin) {
108
+ if (process.platform !== 'win32') return [];
109
+ return run('where', [bin]).stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
110
+ }
111
+
112
+ // The Postgres installs the Windows installer left under Program Files ([] elsewhere).
113
+ function pgInstalls() {
114
+ return process.platform === 'win32' ? dev.windowsPgInstalls() : [];
101
115
  }
102
116
 
103
117
  // Am I root? (Only meaningful on POSIX; Windows has no getuid.)
104
118
  function isRoot() { return typeof process.getuid === 'function' && process.getuid() === 0; }
105
119
 
106
120
  // Best-effort: start whatever service manager owns Postgres on this platform
107
- // (Homebrew on a Mac, systemd/service on a Linux host, pg_ctl with PGDATA
108
- // anywhere), then poll until it accepts connections. The candidate list + sudo
109
- // prefixing are decided by dev-lib.pgStartPlan; this just executes them in order.
121
+ // (Homebrew on a Mac, systemd/service on a Linux host, the installer's service on
122
+ // Windows, pg_ctl with PGDATA anywhere), then poll until it accepts connections.
123
+ // The candidate list + sudo prefixing are decided by dev-lib.pgStartPlan; this just
124
+ // executes them in order.
110
125
  function ensurePostgres(params) {
111
126
  if (pgReady(params)) { ok('Postgres is running'); return true; }
112
127
  info('Postgres is not running — trying to start it…');
113
- for (const step of dev.pgStartPlan({ has, isRoot: isRoot() })) {
128
+ for (const step of dev.pgStartPlan({ has, isRoot: isRoot(), pgInstalls: pgInstalls() })) {
114
129
  let args = step.args;
115
130
  if (step.needsFormula) {
116
131
  // Homebrew's formula name has to be discovered at runtime (postgresql@16,
@@ -177,13 +192,16 @@ function createDb(params) {
177
192
  if (/already exists/i.test(r.stderr)) { ok(`database "${params.dbName}" exists`); return true; }
178
193
  if (needsRoleBootstrap(r.stderr) && process.platform === 'linux') return bootstrapRole(params);
179
194
  fail(`could not create database "${params.dbName}": ${tail(r.stderr, 2) || r.error}`);
195
+ for (const line of dev.pgAuthHint(process.platform, r.stderr)) console.error(` ${line}`);
180
196
  return false;
181
197
  }
182
198
 
183
- function migrate(params) {
184
- const migrateSh = path.join(params.coreRoot, 'scripts', 'migrate.sh');
199
+ // Run migrate.sh under `bashBin` (resolved by preflight: plain `bash` on Mac/Linux,
200
+ // Git Bash's bash.exe on Windows).
201
+ function migrate(params, bashBin = 'bash') {
202
+ const migrateSh = dev.bashScriptPath(path.join(params.coreRoot, 'scripts', 'migrate.sh'));
185
203
  const env = dev.buildEnv(params, { role: 'migrate' });
186
- const r = run('bash', [migrateSh], { cwd: params.instanceRoot, env });
204
+ const r = run(bashBin, [migrateSh], { cwd: params.instanceRoot, env });
187
205
  if (r.status !== 0) {
188
206
  fail('migrations failed:');
189
207
  console.error(tail(r.stderr || r.stdout, 12));
@@ -469,54 +487,86 @@ async function cmdServeStart(params) {
469
487
  return 0;
470
488
  }
471
489
 
472
- async function cmdUp(params, { foreground, noLogin, skipMigrate }) {
473
- info(`Standing up ${path.basename(params.instanceRoot)} on ${params.publicOrigin}`);
474
-
475
- // Step 0: dependencies. A freshly-cloned instance has no node_modules, so
476
- // migrate.sh (pg) and the server (pg/express) would crash. Install FIRST, so
477
- // `bongos dev` is a true one-command bring-up even from a bare checkout (task
478
- // 1002405 — pairs with the root START-HERE that names `npm install` for the
479
- // truly-bare case where not even the `bongos` bin exists yet).
490
+ // Step 0: dependencies. A freshly-cloned instance has no node_modules, so
491
+ // migrate.sh (pg) and the server (pg/express) would crash. Install FIRST, so
492
+ // `bongos dev` is a true one-command bring-up even from a bare checkout (task
493
+ // 1002405 — pairs with the root START-HERE that names `npm install` for the
494
+ // truly-bare case where not even the `bongos` bin exists yet). How npm is started
495
+ // is dev-lib.npmInvocation's call: on Windows a plain spawn of `npm` fails.
496
+ function installDeps(params) {
480
497
  const depsPlan = dev.depsInstallPlan({ instanceRoot: params.instanceRoot });
481
- if (depsPlan.needed) {
482
- if (!has('npm')) {
483
- fail('node_modules is missing and `npm` is not on PATH — install Node (it bundles npm), then re-run `bongos dev`.');
484
- return 1;
485
- }
486
- info(`installing dependencies (npm ${depsPlan.cmd}) — first run in this checkout, this can take a minute…`);
487
- const r = run('npm', depsPlan.args, { cwd: params.instanceRoot, stdio: 'inherit' });
488
- if (r.status !== 0) {
489
- fail(`\`npm ${depsPlan.cmd}\` failed (exit ${r.status ?? '?'}). Fix the error above, then re-run \`bongos dev\`.`);
490
- return 1;
491
- }
492
- ok('dependencies installed');
498
+ if (!depsPlan.needed) return true;
499
+ if (!has('npm')) {
500
+ fail('node_modules is missing and `npm` is not on PATH — install Node (it bundles npm), then re-run `bongos dev`.');
501
+ return false;
493
502
  }
503
+ info(`installing dependencies (npm ${depsPlan.cmd}) — first run in this checkout, this can take a minute…`);
504
+ const npm = dev.npmInvocation(depsPlan.args);
505
+ const r = run(npm.cmd, npm.args, { cwd: params.instanceRoot, stdio: 'inherit', shell: npm.shell });
506
+ if (r.status !== 0) {
507
+ fail(`\`npm ${depsPlan.cmd}\` failed (exit ${r.status ?? '?'}). Fix the error above, then re-run \`bongos dev\`.`);
508
+ return false;
509
+ }
510
+ ok('dependencies installed');
511
+ return true;
512
+ }
513
+
514
+ // Windows: the Postgres installer leaves psql & co. OFF PATH. Put the newest
515
+ // installed bin folder on PATH for this run (children inherit it), so an installed
516
+ // Postgres is found without anyone editing PATH. Returns the install used, or null.
517
+ function addWindowsPgToPath() {
518
+ if (process.platform !== 'win32') return null;
519
+ const inst = pgInstalls()[0];
520
+ if (!inst) return null;
521
+ process.env.PATH = `${inst.bin}${path.win32.delimiter}${process.env.PATH || ''}`;
522
+ return inst;
523
+ }
494
524
 
495
- // Preflight: DB tooling + bash for migrations.
496
- const missing = ['psql', 'createdb', 'pg_isready'].filter((b) => !has(b));
525
+ // Preflight: Postgres client tools + a bash for migrations. Returns
526
+ // { ok: true, bashBin } or { ok: false } after printing what to do, per platform.
527
+ function preflight({ skipMigrate = false } = {}) {
528
+ const PG_TOOLS = ['psql', 'createdb', 'pg_isready'];
529
+ let missing = PG_TOOLS.filter((b) => !has(b));
530
+ if (missing.length) {
531
+ const inst = addWindowsPgToPath();
532
+ if (inst) {
533
+ missing = PG_TOOLS.filter((b) => !has(b));
534
+ if (!missing.length) ok(`using the Postgres tools in ${inst.bin}`);
535
+ }
536
+ }
497
537
  if (missing.length) {
498
538
  fail(`Postgres client tools missing: ${missing.join(', ')}`);
499
539
  console.error(' Install Postgres, then re-run `bongos dev`:');
500
- console.error(process.platform === 'darwin'
501
- ? ' brew install postgresql@16 && brew services start postgresql@16'
502
- : ' (Debian/Ubuntu) sudo apt-get install -y postgresql (or your distro’s package)');
503
- return 1;
540
+ for (const line of dev.pgInstallHint(process.platform)) console.error(` ${line}`);
541
+ return { ok: false };
504
542
  }
505
- if (!skipMigrate && !has('bash')) {
543
+ if (skipMigrate) return { ok: true, bashBin: null };
544
+ const bash = dev.resolveBash({ has, whereAll });
545
+ if (!bash.ok) {
506
546
  fail('`bash` is required to run migrations (used by scripts/migrate.sh).');
507
- console.error(' On Windows, use WSL, or the Docker path in docs/recipes/self-host.md.');
508
- return 1;
547
+ for (const line of dev.bashMissingHint(process.platform, { wslOnly: bash.wslOnly })) console.error(` ${line}`);
548
+ return { ok: false };
509
549
  }
550
+ if (process.platform === 'win32') ok(`migrations will run in ${bash.bin}`);
551
+ return { ok: true, bashBin: bash.bin };
552
+ }
553
+
554
+ async function cmdUp(params, { foreground, noLogin, skipMigrate }) {
555
+ info(`Standing up ${path.basename(params.instanceRoot)} on ${params.publicOrigin}`);
556
+
557
+ if (!installDeps(params)) return 1;
558
+ const pre = preflight({ skipMigrate });
559
+ if (!pre.ok) return 1;
510
560
 
511
561
  if (!ensurePostgres(params)) {
512
562
  fail('Postgres is installed but not reachable, and I could not start it.');
513
563
  console.error(` Start it yourself, then re-run \`bongos dev\`:`);
514
- console.error(process.platform === 'darwin' ? ' brew services start postgresql@16' : ' sudo service postgresql start');
564
+ for (const line of dev.pgStartHint(process.platform, { pgInstalls: pgInstalls() })) console.error(` ${line}`);
515
565
  return 1;
516
566
  }
517
567
  if (!createDb(params)) return 1;
518
568
  if (skipMigrate) warn('skipping migrations (--skip-migrate)');
519
- else if (!migrate(params)) return 1;
569
+ else if (!migrate(params, pre.bashBin)) return 1;
520
570
 
521
571
  const authReady = dev.authWillWork();
522
572
  if (authReady) ok('sign-in configured');
@@ -590,4 +640,6 @@ if (require.main === module) {
590
640
  main().then((code) => process.exit(code || 0)).catch((e) => { fail(e && e.message ? e.message : String(e)); process.exit(1); });
591
641
  }
592
642
 
593
- module.exports = { usage, pgArgs };
643
+ // installDeps / preflight / migrate / createDb are exported for tests/dev_windows.mjs,
644
+ // which drives the win32 branches with process.platform and spawnSync stubbed.
645
+ module.exports = { usage, pgArgs, installDeps, preflight, migrate, createDb };
@@ -30,6 +30,7 @@ const { execSync, spawnSync } = require('node:child_process');
30
30
  const { cliClient, SESSION_PATH, loadSession } = require('./cli-lib');
31
31
  const { classifyMergeDriverState, MERGE_DRIVER_NAME } = require('./install-git-hooks.js');
32
32
  const { preflightChecks } = require('./preflight');
33
+ const { resolveEnv } = require('../../src/instance-config');
33
34
 
34
35
  // ANSI helpers (only if stdout is a TTY; otherwise emit plain text)
35
36
  const TTY = process.stdout.isTTY;
@@ -126,7 +127,7 @@ async function checkSession() {
126
127
  // this bit a real builder twice and was only diagnosed after a long manual dig.
127
128
  // This check makes it the FIRST thing doctor reports, with the one-line fix.
128
129
  //
129
- // Resolution mirrors the grader EXACTLY (OTB_CLAUDE_BIN / CLAUDE_BIN / 'claude',
130
+ // Resolution mirrors the grader EXACTLY (<PREFIX>_CLAUDE_BIN / CLAUDE_BIN / 'claude',
130
131
  // shell:true on win32 so a `claude.cmd` shim resolves) so a green row here means
131
132
  // the grader's spawn will work too. Mac/Linux: shell:false, identical to today.
132
133
  function checkToolchain() {
@@ -152,7 +153,7 @@ function checkToolchain() {
152
153
  // can pass --version yet misbehave under the grader's stdio redirection
153
154
  // (SUBAGENT_NONZERO_EXIT). Learning #42.
154
155
  const claudeOk = results.some((r) => r.title === 'claude CLI' && r.level === 'ok');
155
- if (claudeOk && process.platform === 'win32' && !process.env.OTB_CLAUDE_BIN && !process.env.CLAUDE_BIN) {
156
+ if (claudeOk && process.platform === 'win32' && !resolveEnv('CLAUDE_BIN') && !process.env.CLAUDE_BIN) {
156
157
  try {
157
158
  const where = spawnSync('where', ['claude'], { encoding: 'utf8', shell: true, timeout: 8000 });
158
159
  if (where.status === 0 && /WindowsApps/i.test(where.stdout || '')) {