@bongos/core 1.21.70 → 1.21.71

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 (63) hide show
  1. package/.bongos-core.json +114 -59
  2. package/.claude/skills/goal-create/SKILL.md +21 -3
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +2 -0
  5. package/clients/bongos-client/index.cjs +2 -0
  6. package/clients/bongos-client/index.d.ts +5 -3
  7. package/clients/bongos-client/index.mjs +2 -0
  8. package/docs/adr/0368-red-and-blue-teams-practice-on-a-range-never-on-live.md +99 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +46 -5
  11. package/docs/api-reference.md +4 -3
  12. package/docs/copy-inventory.md +25 -21
  13. package/docs/copy-registry.json +76 -30
  14. package/docs/file-map.md +1 -1
  15. package/docs/module-api-changelog.md +2 -0
  16. package/docs/page-readings.json +8 -8
  17. package/docs/recipes/core-release-pipeline.md +6 -0
  18. package/docs/recipes/upgrading-the-core.md +2 -0
  19. package/modules/hall-ui/public/settings-software-update.js +77 -2
  20. package/modules/hall-ui/public/settings.html +1 -1
  21. package/modules/lifecycle/db-tasks.js +12 -0
  22. package/modules/lifecycle/goal-spec-task.js +129 -0
  23. package/modules/lifecycle/routes/goals.js +7 -9
  24. package/modules/lifecycle/routes/task-write-routes.js +11 -1
  25. package/modules/lifecycle/upkeep-home.js +55 -0
  26. package/package-lock.json +2 -2
  27. package/package.json +1 -1
  28. package/release-notes.json +37 -0
  29. package/scripts/gds/claude-materialize.js +125 -31
  30. package/scripts/gds/claude-md-template.js +47 -10
  31. package/scripts/gds/cli-lib.js +4 -4
  32. package/scripts/gds/feedback-send.js +3 -3
  33. package/scripts/gds/paste-token.js +1 -1
  34. package/scripts/gds/provision-net.js +1 -8
  35. package/scripts/gds/provision-units.js +28 -2
  36. package/scripts/gds/reauth.js +1 -1
  37. package/scripts/gds/run-unit-tests.js +3 -0
  38. package/scripts/gds/setup.js +1 -1
  39. package/scripts/gds/start.js +4 -4
  40. package/scripts/gds/surface.js +15 -3
  41. package/scripts/gds/task.js +32 -5
  42. package/scripts/gds/upgrade-operator.js +73 -0
  43. package/scripts/gds/upgrade.js +7 -2
  44. package/scripts/hall-preview/server.js +3 -0
  45. package/src/bongos/routes/core-update.js +98 -4
  46. package/src/bongos/routes/feedback.js +11 -1
  47. package/src/bongos/upgrade-request.js +158 -0
  48. package/src/module-api.js +1 -1
  49. package/tests/claude_materialize.mjs +195 -18
  50. package/tests/cli_allowlist_permissions.mjs +6 -2
  51. package/tests/cli_surface.mjs +2 -2
  52. package/tests/fixtures/page-ledger.mjs +5 -0
  53. package/tests/goal_admission_override.mjs +1 -1
  54. package/tests/goal_category.mjs +1 -1
  55. package/tests/goal_create_spec_entry.mjs +61 -0
  56. package/tests/goal_spec_task.mjs +197 -0
  57. package/tests/goal_spec_task_db.mjs +95 -0
  58. package/tests/provision_settings_apply.mjs +25 -1
  59. package/tests/software_update.mjs +18 -1
  60. package/tests/start_env_guard.mjs +2 -2
  61. package/tests/upgrade_operator.mjs +105 -0
  62. package/tests/upgrade_request.mjs +348 -0
  63. package/tests/upkeep_home.mjs +156 -0
@@ -56,15 +56,15 @@ function renderInstanceClaudeMd(branding = {}) {
56
56
 
57
57
  Read this at the start of every session. Keep it lean — it loads in full every time.
58
58
 
59
- ## First run — before any \`bongos\` command
59
+ ## First run — before any \`npx bongos\` command
60
60
 
61
- **If \`node_modules/@bongos/core\` is missing, run \`npm install\` first.** The \`bongos\` CLI ships *inside* that dependency, so until the install completes there is no \`bongos\` command and every instruction below fails with \`command not found\`:
61
+ **If \`node_modules/@bongos/core\` is missing, run \`npm install\` first.** The \`bongos\` CLI ships *inside* that dependency and is **not on your PATH**, so it always runs through npx — \`npx bongos <command>\`, never a bare \`bongos\` (that fails with \`command not found\`). Until the install completes, every \`npx bongos\` instruction below fails too:
62
62
 
63
63
  \`\`\`bash
64
64
  npm install
65
65
  \`\`\`
66
66
 
67
- Once per machine, per clone — it is not committed (\`node_modules/\` is ignored on purpose), so a fresh \`git clone\` always needs it. If a \`bongos\` command then still misbehaves, \`bongos doctor\` checks the toolchain and session and prints the fix.
67
+ Once per machine, per clone — it is not committed (\`node_modules/\` is ignored on purpose), so a fresh \`git clone\` always needs it. If an \`npx bongos\` command then still misbehaves, \`npx bongos doctor\` checks the toolchain and session and prints the fix.
68
68
 
69
69
  ## What this is
70
70
 
@@ -74,17 +74,17 @@ ${ident}
74
74
 
75
75
  **Every change is backed by a claimed Bongos task. No exceptions.** Code, docs, migrations, config — if you are about to change project state and do not hold an active claim covering it, stop: create or find the task, claim it, then make the change. There is no "tiny fix" or "just a doc" loophole; the database is the complete ledger of what happened to this project.
76
76
 
77
- - **Start every session with \`bongos start\`** (or \`/builder-start\`) — it lists the tasks you can claim right now, parallel-safe against everyone else's active claims.
78
- - **\`bongos claim <id>\`** locks a task to your session. If it refuses (already claimed, not ready, dependencies not shipped), pick another.
79
- - **\`bongos ship\`** runs the smoke + grade, then merges and deploys; **\`bongos release <id>\`** drops a claim with no credit. Ship or release before you sign off.
77
+ - **Start every session with \`npx bongos start\`** (or \`/builder-start\`) — it lists the tasks you can claim right now, parallel-safe against everyone else's active claims.
78
+ - **\`npx bongos claim <id>\`** locks a task to your session. If it refuses (already claimed, not ready, dependencies not shipped), pick another.
79
+ - **\`npx bongos ship\`** runs the smoke + grade, then merges and deploys; **\`npx bongos release <id>\`** drops a claim with no credit. Ship or release before you sign off.
80
80
  - **Dependencies gate everything** — a task becomes claimable only once every task it depends on has shipped.
81
81
  - **Code you write or touch carries a doc-comment** — each module a header, each public function/class a line or two, saying *what* it does and *why* it exists (never how), in the words someone would search for. Agents find code by searching text, so meaning written into the source is what makes it findable.
82
82
 
83
83
  ## Where things live
84
84
 
85
- - **Live work state** (tasks, versions, ranks, ${currency}): the Bongos DB — \`bongos start\` / \`bongos status\`, or ${hallLine}.${statusLine}
85
+ - **Live work state** (tasks, versions, ranks, ${currency}): the Bongos DB — \`npx bongos start\` / \`npx bongos status\`, or ${hallLine}.${statusLine}
86
86
  - **This instance's identity** (product, world, brand, addresses): \`config/branding.json\`. **Which features are on**: \`config/modules.json\`.
87
- - **The platform you stand on** (rank ladder, the ship/grade/deploy pipeline, permissions, migrations) is vendored **Cloud Bongos core** — build the product, not the platform under it. Landing code is Bongos's job: claim a task and run \`bongos ship\`, never a raw \`git push\`/deploy.
87
+ - **The platform you stand on** (rank ladder, the ship/grade/deploy pipeline, permissions, migrations) is vendored **Cloud Bongos core** — build the product, not the platform under it. Landing code is Bongos's job: claim a task and run \`npx bongos ship\`, never a raw \`git push\`/deploy.
88
88
 
89
89
  ## Permissions
90
90
 
@@ -96,8 +96,45 @@ Builders are rewarded in **${currency}** for shipped work.
96
96
 
97
97
  ---
98
98
 
99
- *This is a starter, authored by \`bongos init\`. Grow it as the project grows — add your architecture, versioning methodology, and hard-won gotchas — but keep it lean: every line spends context budget in every session.*
99
+ *This is a starter, authored by \`npx bongos init\`. Grow it as the project grows — add your architecture, versioning methodology, and hard-won gotchas — but keep it lean: every line spends context budget in every session.*
100
100
  `;
101
101
  }
102
102
 
103
- module.exports = { renderInstanceClaudeMd };
103
+ // The starter's own sentences as an earlier `bongos init` wrote them, each beside its `npx bongos`
104
+ // form (task 1004621). Every pair is branding-independent text this template authored, so an exact
105
+ // match can only be a line the owner never changed.
106
+ const LEGACY_CLI_LINES = [
107
+ ['## First run — before any `bongos` command\n\n**If `node_modules/@bongos/core` is missing, run `npm install` first.** The `bongos` CLI ships *inside* that dependency, so until the install completes there is no `bongos` command and every instruction below fails with `command not found`:',
108
+ '## First run — before any `npx bongos` command\n\n**If `node_modules/@bongos/core` is missing, run `npm install` first.** The `bongos` CLI ships *inside* that dependency and is **not on your PATH**, so it always runs through npx — `npx bongos <command>`, never a bare `bongos` (that fails with `command not found`). Until the install completes, every `npx bongos` instruction below fails too:'],
109
+ ['If a `bongos` command then still misbehaves, `bongos doctor` checks', 'If an `npx bongos` command then still misbehaves, `npx bongos doctor` checks'],
110
+ ['- **Start every session with `bongos start`** (or `/builder-start`)', '- **Start every session with `npx bongos start`** (or `/builder-start`)'],
111
+ ['- **`bongos claim <id>`** locks a task to your session.', '- **`npx bongos claim <id>`** locks a task to your session.'],
112
+ ['- **`bongos ship`** runs the smoke + grade, then merges and deploys; **`bongos release <id>`** drops', '- **`npx bongos ship`** runs the smoke + grade, then merges and deploys; **`npx bongos release <id>`** drops'],
113
+ ['the Bongos DB — `bongos start` / `bongos status`, or', 'the Bongos DB — `npx bongos start` / `npx bongos status`, or'],
114
+ ['claim a task and run `bongos ship`, never', 'claim a task and run `npx bongos ship`, never'],
115
+ ['*This is a starter, authored by `bongos init`.', '*This is a starter, authored by `npx bongos init`.'],
116
+ ];
117
+
118
+ // refreshStarterCliLines — bring an EXISTING project's CLAUDE.md onto the `npx bongos` spelling
119
+ // (task 1004621). The starter is written only when CLAUDE.md is absent, so every project scaffolded
120
+ // before this kept telling its assistant to type a bare `bongos start` — a command that is not on
121
+ // PATH — in the one file loaded every session. The instance owns the file, so this replaces ONLY
122
+ // the starter's own sentences, matched exactly: a line the owner reworded is left alone, nothing
123
+ // is added or removed, and a file grown from the starter keeps everything else byte for byte.
124
+ // Pure: returns { text, changed } where `changed` counts the sentences replaced.
125
+ function refreshStarterCliLines(text) {
126
+ let out = String(text == null ? '' : text);
127
+ let changed = 0;
128
+ // A Windows checkout (git autocrlf) holds the file with CRLF endings; match it in its own.
129
+ const eol = out.includes('\r\n') ? (s) => s.replace(/\n/g, '\r\n') : (s) => s;
130
+ for (const [legacyLf, currentLf] of LEGACY_CLI_LINES) {
131
+ const legacy = eol(legacyLf);
132
+ const current = eol(currentLf);
133
+ if (!out.includes(legacy)) continue;
134
+ out = out.split(legacy).join(current);
135
+ changed += 1;
136
+ }
137
+ return { text: out, changed };
138
+ }
139
+
140
+ module.exports = { renderInstanceClaudeMd, refreshStarterCliLines, LEGACY_CLI_LINES };
@@ -512,7 +512,7 @@ function printUnauthGuidanceAndExit() {
512
512
  console.error(` Recover the fast (UI-first) way — run ${surfaceAction('reauth')}`);
513
513
  console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
514
514
  console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
515
- console.error(' Fallback (browser sign-in): bongos login <instance-url>');
515
+ console.error(' Fallback (browser sign-in): npx bongos login <instance-url>');
516
516
  } else {
517
517
  console.error(' Recover the fast (UI-first) way — run /builder-reauth, or:');
518
518
  console.error(' node scripts/gds/reauth.js');
@@ -609,7 +609,7 @@ function printInstanceMismatchAndExit({ expected, actual }) {
609
609
  console.error(` Your session is signed in to: ${actual}`);
610
610
  console.error('');
611
611
  console.error(' Sign in to THIS instance, then re-run your command:');
612
- console.error(` bongos login ${expected}`);
612
+ console.error(` npx bongos login ${expected}`);
613
613
  console.error(` (or, from a checkout without the bongos bin: node scripts/gds/login.js ${expected})`);
614
614
  console.error('');
615
615
  console.error(' Intentionally targeting another instance? Set the base explicitly, e.g.');
@@ -764,8 +764,8 @@ async function requireSession() {
764
764
  if (isTerminalSurface()) {
765
765
  // A terminal reader may have no instance configured at all — naming one is the whole point
766
766
  // of `login`, and `/builder-*` does not exist outside a Claude Code session (task 1003730).
767
- console.error(` • First time here → run bongos login <instance-url> (sign in with a browser click)`);
768
- console.error(` • Signed in before / expired → run bongos reauth (re-issue your CLI token)`);
767
+ console.error(` • First time here → run npx bongos login <instance-url> (sign in with a browser click)`);
768
+ console.error(` • Signed in before / expired → run npx bongos reauth (re-issue your CLI token)`);
769
769
  } else {
770
770
  console.error(` • First time on the project → run /builder-setup (one-time GitHub sign-in that also registers you)`);
771
771
  console.error(` • Signed in before / expired → run /builder-reauth (fast UI re-issue of your CLI token)`);
@@ -29,9 +29,9 @@ const ic = require('../../src/instance-config');
29
29
 
30
30
  const USAGE = [
31
31
  'usage:',
32
- ' bongos feedback "what went wrong" report a problem with Bongos',
33
- ' bongos feedback --idea "what you wish for" suggest an improvement',
34
- ' bongos feedback --message-file notes.md send a longer note from a file (- for stdin)',
32
+ ' npx bongos feedback "what went wrong" report a problem with Bongos',
33
+ ' npx bongos feedback --idea "what you wish for" suggest an improvement',
34
+ ' npx bongos feedback --message-file notes.md send a longer note from a file (- for stdin)',
35
35
  '',
36
36
  'Goes to the Cloud Bongos maintainers. Only the text you type and the Bongos',
37
37
  'versions you run are sent — no code, chat or files from your project.',
@@ -86,7 +86,7 @@ const API_BASE = RESOLVED.base;
86
86
  function usage(extra) {
87
87
  if (extra) console.error(extra);
88
88
  console.error('');
89
- console.error('Usage: bongos paste-token <token> [--api-base <url>]');
89
+ console.error('Usage: npx bongos paste-token <token> [--api-base <url>]');
90
90
  console.error(' (inside a core checkout: node scripts/gds/paste-token.js <token>)');
91
91
  console.error('');
92
92
  // Fall back to the inferred base for this hint when --api-base was the thing that
@@ -12,14 +12,7 @@
12
12
  const path = require('node:path');
13
13
  const crypto = require('node:crypto');
14
14
  const { CONFIG } = require('./provision-config.js');
15
- const { provisionedEnvPrefix } = require('./provision-units.js');
16
-
17
- function federationHubOrigin() {
18
- const explicit = process.env.PROVISION_HUB_ORIGIN;
19
- if (explicit) return String(explicit).replace(/\/+$/, '');
20
- const own = process.env[`${provisionedEnvPrefix()}_PUBLIC_ORIGIN`];
21
- return own ? String(own).replace(/\/+$/, '') : null;
22
- }
15
+ const { federationHubOrigin, provisionedEnvPrefix } = require('./provision-units.js'); // lives beside the hub-upgrade address it also feeds (task 1004608)
23
16
 
24
17
  // Whether provisioning should federate this instance's sign-in through the hub.
25
18
  //
@@ -258,13 +258,15 @@ GITHUB_APP_PRIVATE_KEY=${String(appPem).replace(/\r?\n/g, '\\n')}
258
258
  // Postgres with every sibling and the control plane, so its pool may not take pg's
259
259
  // default 10 of the server's 97. src/bongos/pool.js reads it bare.
260
260
  const poolLine = `PGPOOL_MAX=${CONFIG.instancePoolMax}\n`;
261
+ // Where this project's Update button goes (task 1004608) — see hubUpgradeEnvVarsFor.
262
+ const hubUpgradeLines = Object.entries(hubUpgradeEnvVarsFor(inst)).map(([k, v]) => `${k}=${v}\n`).join('');
261
263
  return `# /etc/${inst.slug}/web.env — per-instance secrets (chmod 600, OUTSIDE the repo).
262
264
  ${credComment}
263
265
  ${prefix}_GITHUB_CLIENT_ID=${clientId || ''}
264
266
  ${prefix}_GITHUB_CLIENT_SECRET=${clientSecret || ''}
265
267
  ${firstAdminLine}${originLines}${appLines}PORT=${inst.port}
266
268
  PGDATABASE=${dbName(inst)}
267
- ${poolLine}${dbLines}${fedLines}${settingsEnvLines(inst, prefix)}`;
269
+ ${poolLine}${dbLines}${fedLines}${hubUpgradeLines}${settingsEnvLines(inst, prefix)}`;
268
270
  }
269
271
  function webEnvPath(inst, { etcBase = CONFIG.etcBase } = {}) {
270
272
  return path.posix.join(etcBase, inst.slug, 'web.env');
@@ -295,7 +297,7 @@ function originEnvVarsFor(inst) {
295
297
  function settingsEnvVarsFor(inst, envBody) {
296
298
  const { settingsEnvVars } = require(path.join(REPO_ROOT, 'modules/provisioning/provisioning')); // eslint-disable-line global-require
297
299
  const prefix = inst && inst.hosting_shape === 'control-plane' ? selfEnvPrefix({ envBody }) : provisionedEnvPrefix();
298
- return settingsEnvVars(inst, prefix);
300
+ return { ...settingsEnvVars(inst, prefix), ...hubUpgradeEnvVarsFor(inst) };
299
301
  }
300
302
 
301
303
  // The env-var prefix the CONTROL PLANE's own web process resolves (task 1004334). The
@@ -414,8 +416,32 @@ function renamedEnvCarry(body) {
414
416
  // <PREFIX>_HUB_ORIGIN (what auth.js loadIdpConfig reads). Explicit PROVISION_HUB_ORIGIN
415
417
  // wins; otherwise the control plane's OWN public origin (<PREFIX>_PUBLIC_ORIGIN, the
416
418
  // env the hub already runs under). Null → not hub-configured → shouldFederate is false.
419
+ function federationHubOrigin() {
420
+ const explicit = process.env.PROVISION_HUB_ORIGIN;
421
+ if (explicit) return String(explicit).replace(/\/+$/, '');
422
+ const own = process.env[`${provisionedEnvPrefix()}_PUBLIC_ORIGIN`];
423
+ return own ? String(own).replace(/\/+$/, '') : null;
424
+ }
425
+
426
+ // WHERE A HOSTED PROJECT'S UPDATE BUTTON GOES (task 1004608): the hub's deploy door for
427
+ // THIS project, written as <PREFIX>_HUB_UPGRADE_URL. The hall used to learn the hub only
428
+ // from the sign-in federation block, which only a co-tenant gets, so every cloud-host
429
+ // project (and any that keeps its own GitHub app) told its owner to run `bongos upgrade`
430
+ // on a server the platform runs. The door admits the owner of both tenant shapes
431
+ // (modules/provisioning/core-upgrade-authz.js), so both get the address. Not a secret and
432
+ // grants nothing: the door re-checks every request. Empty for the tenant shapes when no hub
433
+ // origin resolves (a key that came and went would leave a stale line behind); absent for
434
+ // every other shape, which has no door on this hub.
435
+ const HUB_UPGRADE_SHAPES = new Set(['co-tenant', 'cloud-host']);
436
+ function hubUpgradeEnvVarsFor(inst) {
437
+ if (!inst || !HUB_UPGRADE_SHAPES.has(inst.hosting_shape) || !inst.slug) return {};
438
+ const origin = federationHubOrigin();
439
+ return { [`${provisionedEnvPrefix()}_HUB_UPGRADE_URL`]: origin ? `${origin}/deploy?project=${encodeURIComponent(String(inst.slug))}` : '' };
440
+ }
417
441
 
418
442
  module.exports = {
443
+ federationHubOrigin,
444
+ hubUpgradeEnvVarsFor,
419
445
  ENV_PREFIX_PACK,
420
446
  envPrefixMismatch,
421
447
  normalizePackEnvPrefix,
@@ -96,7 +96,7 @@ function printGuidance({ opened }) {
96
96
  console.log(" you're already signed in there via your browser cookie)");
97
97
  console.log(' 2. Under the "CLI Access" section, click "Re-issue CLI token".');
98
98
  console.log(' 3. Click "Copy" — it copies a one-line command that looks like:');
99
- console.log(' bongos paste-token <token>');
99
+ console.log(' npx bongos paste-token <token>');
100
100
  console.log(' 4. Paste that one line back to Claude. It will run it for you,');
101
101
  console.log(' verify the token, and write your new session.');
102
102
  console.log('');
@@ -106,6 +106,9 @@ const INTEGRATION = new Set([
106
106
  // never was a dedicated consumer-layout-boot workflow, whatever older notes say.
107
107
  'consumer_layout_boot',
108
108
  'claim_gate_open_blocker', 'claim_gate_rebase',
109
+ // task 1004585: a hall-made goal's spec task gates its other work — the edge, the
110
+ // claim gate's unsatisfiedDeps read, and the ship-time auto-promotion, on real Postgres.
111
+ 'goal_spec_task_db',
109
112
  // task 1003457: proves the blocker-resolve auto-promotion trigger (migration
110
113
  // 009, tightened by core_223) fires on the HALL'S OWN write path — the real
111
114
  // buildGdsRouter() HTTP routes, not a static SQL-text pin (blocker_resolve_guard.mjs,
@@ -356,7 +356,7 @@ async function finishSetup(builder) {
356
356
  console.log(` 3. Claim + ship: ${surfaceAction('claim', 'N')} → ${surfaceAction('ship')}`);
357
357
  // Claiming writes code, so it needs a checkout. In a terminal the public CLI answers
358
358
  // `bongos claim` with where to get one; say so here rather than let step 3 look self-serve.
359
- if (isTerminalSurface()) console.log(' (claim + ship run in a checkout — `bongos claim` shows how to get one)');
359
+ if (isTerminalSurface()) console.log(' (claim + ship run in a checkout — `npx bongos claim` shows how to get one)');
360
360
  console.log(' 4. Ship three works and you rise to Thetes — the whole queue opens.');
361
361
  }
362
362
  } catch (_) {
@@ -615,7 +615,7 @@ function instanceNotRunningCard({ origin, depsMissing } = {}) {
615
615
  out.push(`2. If that doesn't load either, the instance itself is down: \`curl -sI ${origin}/healthz\` says whether it's answering.`);
616
616
  out.push(`3. Once it answers, re-run ${surface.code(surface.action('start'))}.`);
617
617
  out.push('');
618
- out.push(`${surface.strong(`Don't run ${surface.code('bongos dev')} for this.`)} It stands up a separate local instance with its own empty database — it cannot show you this board, and it would hide the outage behind an empty one.`);
618
+ out.push(`${surface.strong(`Don't run ${surface.code(surface.cli('dev'))} for this.`)} It stands up a separate local instance with its own empty database — it cannot show you this board, and it would hide the outage behind an empty one.`);
619
619
  if (depsMissing) {
620
620
  out.push('');
621
621
  out.push(`(The ${surface.code('bongos')} CLI isn't installed here. It's optional — the website above is the same board. ${surface.code('@bongos/core')} is ${surface.strong('public')} on npm, so ${surface.code('npm install @bongos/core')} needs no token or login; a failure here is a real error, not the expected tokenless 404 it used to be.)`);
@@ -629,10 +629,10 @@ function instanceNotRunningCard({ origin, depsMissing } = {}) {
629
629
  if (depsMissing) {
630
630
  out.push(`${n++}. \`npm install\` — installs the toolchain first (the \`bongos\` CLI ships inside \`@bongos/core\`, so nothing else runs until this does).`);
631
631
  }
632
- out.push(`${n++}. \`bongos dev\` — one command: starts Postgres, builds the tables, runs the server, and signs you in.`);
632
+ out.push(`${n++}. ${surface.code(surface.cli('dev'))} — one command: starts Postgres, builds the tables, runs the server, and signs you in.`);
633
633
  out.push(`${n++}. Re-run ${surface.code(surface.action('start'))}.`);
634
634
  out.push('');
635
- out.push(`Meant to work on a *different* instance that's already running? Sign in to it with \`bongos login <url>\` first.`);
635
+ out.push(`Meant to work on a *different* instance that's already running? Sign in to it with ${surface.code(surface.cli('login <url>'))} first.`);
636
636
  return out.join('\n');
637
637
  }
638
638
  function emitInstanceNotRunningCard(opts) {
@@ -980,7 +980,7 @@ async function main() {
980
980
 
981
981
  // ---------- next ----------
982
982
  console.log('');
983
- console.log(`→ ${surface.isTerminal() ? `Claim one with ${surface.code('bongos claim <id>')} in a checkout` : `${surface.strong('Tell me a number')} to claim it`}.`);
983
+ console.log(`→ ${surface.isTerminal() ? `Claim one with ${surface.code(surface.cli('claim <id>'))} in a checkout` : `${surface.strong('Tell me a number')} to claim it`}.`);
984
984
  if (full) {
985
985
  console.log('');
986
986
  console.log('Commands:');
@@ -43,11 +43,23 @@ function taskRef(id, url, env = process.env) {
43
43
  return isTerminal(env) ? `#${id}` : `[#${id}](${url}${id})`;
44
44
  }
45
45
 
46
+ // How a reader runs the CLI (task 1004621). The `bongos` bin ships inside @bongos/core and is not
47
+ // on PATH — in a project it sits at node_modules/.bin/bongos, and the global @cloudbongos/cli that
48
+ // once put a bare `bongos` on PATH is retired (ADR 0324). `npx` finds the local bin with no setup,
49
+ // in a project and in a core checkout alike, so every printed command uses it. A bare `bongos …`
50
+ // sent the first command of a project's session into `command not found`.
51
+ const CLI = 'npx bongos';
52
+
53
+ // A CLI command line to print: `cli('claim <id>')` → `npx bongos claim <id>`.
54
+ function cli(rest) {
55
+ return rest ? `${CLI} ${rest}` : CLI;
56
+ }
57
+
46
58
  // An action the reader can actually run. In a session that is the slash command; in a terminal it
47
- // is the `bongos` verb. `/builder-ship 123` does not exist outside Claude Code.
59
+ // is the `npx bongos` verb. `/builder-ship 123` does not exist outside Claude Code.
48
60
  function action(verb, arg, env = process.env) {
49
61
  const tail = arg == null ? '' : ` ${arg}`;
50
- return isTerminal(env) ? `bongos ${verb}${tail}` : `/builder-${verb}${tail}`;
62
+ return isTerminal(env) ? cli(`${verb}${tail}`) : `/builder-${verb}${tail}`;
51
63
  }
52
64
 
53
65
  // Markdown emphasis is decoration in a feed and literal asterisks on a console.
@@ -86,4 +98,4 @@ function table(headers, rows, env = process.env) {
86
98
  ].join('\n');
87
99
  }
88
100
 
89
- module.exports = { AGENT, TERMINAL, resolveSurface, isTerminal, taskRef, action, strong, em, code, table };
101
+ module.exports = { AGENT, TERMINAL, CLI, cli, resolveSurface, isTerminal, taskRef, action, strong, em, code, table };
@@ -19,10 +19,12 @@
19
19
  // Task CREATION is Archon-gated SERVER-SIDE (creating work shapes scope, ADR 0016). This
20
20
  // CLI adds no authority — a lower rank gets a clear 403 message pointing at idea capture.
21
21
 
22
- const { cliClient, requireSession, arg, hasFlag } = require('./cli-lib');
22
+ const { cliClient, requireSession, arg, hasFlag, errCode } = require('./cli-lib');
23
23
  // task 1003109: the ship-time VISUAL. The CLI screens the file with the SERVER's
24
24
  // own constants, so what it accepts is exactly what the upload route accepts.
25
25
  const taskVisuals = require('../../modules/lifecycle/task-visuals');
26
+ // task 1004597: the one spelling of "this is platform upkeep" (kind infra, no goal).
27
+ const { UPKEEP_KIND } = require('../../modules/lifecycle/upkeep-home');
26
28
 
27
29
  // Valid enums (mirrors the server's task-classifier vocab; a friendly client-side check
28
30
  // so an obvious typo fails before the round-trip — the server stays authoritative).
@@ -87,6 +89,8 @@ function usage() {
87
89
  console.error(' bongos task visual <id> <image> [--alt "caption"] attach or replace the ship-time visual');
88
90
  console.error(' bongos task visual <id> --remove remove it again');
89
91
  console.error(' bongos task visual <id> <image> --slot <name> attach or replace a named visual (e.g. after-phone-dark)');
92
+ console.error('\n Platform upkeep (a Bongos upgrade, maintenance) needs no goal: pass --kind infra');
93
+ console.error(" and leave out --goal, and it is filed under the version's maintenance goal.");
90
94
  console.error(`\n kinds: ${VALID_KINDS.join(' | ')}`);
91
95
  console.error(` disciplines: ${VALID_DISCIPLINES.join(' | ')}`);
92
96
  }
@@ -111,6 +115,22 @@ async function resolveDefaultVersion(api) {
111
115
  return building[0].id;
112
116
  }
113
117
 
118
+ // goalRequiredHelp — PURE: what a builder reads when a task names no goal
119
+ // (task 1004597). Two ways forward: name a product goal, or — when the work is
120
+ // platform upkeep such as a Bongos upgrade — file it as kind infra with no goal.
121
+ function goalRequiredHelp(body) {
122
+ const lines = ['Every task belongs to a goal, and this one did not name one.'];
123
+ if (body && body.kind === UPKEEP_KIND) {
124
+ lines.push(`Version ${body.version_id} is not being built right now, so it has no maintenance goal.`);
125
+ lines.push('Re-run with --version <the version being built> (see `bongos status`).');
126
+ } else {
127
+ lines.push(' - Product work: re-run with --goal <id> (`bongos status` lists the goals).');
128
+ lines.push(' - Platform upkeep (a Bongos upgrade or other maintenance): re-run with --kind infra and no --goal.');
129
+ lines.push(" It goes into this version's maintenance goal.");
130
+ }
131
+ return lines.join('\n');
132
+ }
133
+
114
134
  async function cmdNew(args) {
115
135
  const { body, errors } = buildTaskBody(args);
116
136
  if (errors.length) { console.error('task new: ' + errors.join('; ') + '\n'); usage(); process.exit(2); }
@@ -120,7 +140,7 @@ async function cmdNew(args) {
120
140
  if (!body.version_id) {
121
141
  body.version_id = await resolveDefaultVersion(api);
122
142
  if (!body.version_id) {
123
- console.error('task new: could not resolve an active version — pass --version <id> (see `bongos status`).');
143
+ console.error('task new: could not resolve an active version — pass --version <id> (see `npx bongos status`).');
124
144
  process.exit(2);
125
145
  }
126
146
  }
@@ -128,7 +148,13 @@ async function cmdNew(args) {
128
148
  if (r.status === 403) {
129
149
  console.error('Creating a task is Archon-gated (creating work shapes scope). Your rank is lower, so the server declined.');
130
150
  console.error('To propose it instead, capture an idea for triage:');
131
- console.error(' bongos exec scripts/gds/capture.js "<your idea>"');
151
+ console.error(' npx bongos exec scripts/gds/capture.js "<your idea>"');
152
+ process.exit(1);
153
+ }
154
+ // task 1004597: a goal-less filing is refused unless it is platform upkeep.
155
+ // Say both ways forward in plain words instead of dumping the raw error.
156
+ if (r.status === 400 && errCode(r.data) === 'goal_id_required') {
157
+ console.error(goalRequiredHelp(body));
132
158
  process.exit(1);
133
159
  }
134
160
  if (r.status !== 201 || !r.data?.task) {
@@ -141,8 +167,9 @@ async function cmdNew(args) {
141
167
  console.log(`✓ Created task #${task.id} [${task.status}] — ${task.title}`);
142
168
  console.log(` kind=${task.kind} · discipline=${task.discipline}${task.priority ? ` · P${task.priority}` : ''}${task.goal_id ? ` · goal ${task.goal_id}` : ''}`);
143
169
  console.log(` ${hallLink(API_BASE, task.id)}`);
170
+ if (r.data.goal_home === 'maintenance') console.log(` Filed under ${body.version_id}'s maintenance goal, the standing home for platform upkeep.`);
144
171
  if (task.status !== 'ready') console.log(` (status '${task.status}' — pass --ready to make it claimable now, deps permitting)`);
145
- console.log(` next: bongos claim ${task.id}`);
172
+ console.log(` next: npx bongos claim ${task.id}`);
146
173
  }
147
174
 
148
175
  async function cmdShow(args) {
@@ -243,4 +270,4 @@ if (require.main === module) {
243
270
  main().catch((err) => { console.error('fatal:', err); process.exit(1); });
244
271
  }
245
272
 
246
- module.exports = { buildTaskBody, VALID_KINDS, VALID_DISCIPLINES, csv, hallLink };
273
+ module.exports = { buildTaskBody, goalRequiredHelp, VALID_KINDS, VALID_DISCIPLINES, csv, hallLink };
@@ -0,0 +1,73 @@
1
+ // scripts/gds/upgrade-operator.js — "who upgrades this project?" for `bongos upgrade`
2
+ // run from a builder's own computer (task 1004596).
3
+ //
4
+ // A hosted project (its Settings → Software update button leads to the hub — door kind
5
+ // 'hub', see src/bongos/routes/core-update.js updateDoor) lives on its operator's server.
6
+ // Its builders work from a laptop checkout that has no database and no server access. Run
7
+ // there, `bongos upgrade` used to say only "cannot determine which database to migrate.
8
+ // Set PGDATABASE…" — and an assistant reading that, plus the release recipe, wrote the
9
+ // builder server steps full of guessed names for a machine they cannot reach. This module
10
+ // replaces that dead end with the one supported path: the operator upgrades it, and here
11
+ // is how to ask.
12
+ //
13
+ // THE CONDITION, and why it cannot touch the real operator path: BOTH must hold —
14
+ // 1. no database is known in this checkout (no PGDATABASE, no db.database in the
15
+ // branding pack). On the server a database is always known, or upgrade.js already
16
+ // refuses there; so the server never reaches step 2.
17
+ // 2. the project, asked through this builder's own CLI session, says its update door is
18
+ // the hub's. No session, no answer, an older core, a self-hosted or provisioning
19
+ // instance → no message, and upgrade.js behaves exactly as before.
20
+
21
+ // Ask the project (via this builder's CLI session) where its Update button leads.
22
+ // Returns { kind, url, hall } or null on ANY failure — a missing answer is never a verdict.
23
+ async function readUpdateDoor({ timeoutMs = 5000 } = {}) {
24
+ try {
25
+ const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT, hallBase } = require('./cli-lib');
26
+ const session = readSessionToken();
27
+ if (!session) return null;
28
+ const res = await dispatchedFetch(`${session.apiBase}/api/bongos/software-update`, {
29
+ headers: { Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT },
30
+ signal: AbortSignal.timeout(timeoutMs),
31
+ dispatcher: noKeepAliveDispatcher(),
32
+ });
33
+ if (!res.ok) return null;
34
+ const body = await res.json();
35
+ const door = body && body.door;
36
+ if (!door || typeof door.kind !== 'string') return null;
37
+ return { kind: door.kind, url: door.url || null, hall: hallBase(session.apiBase) };
38
+ } catch { return null; }
39
+ }
40
+
41
+ // The door when the operator runs this project and this checkout is not where it runs;
42
+ // null otherwise (including whenever the answer cannot be read).
43
+ async function operatorRunsThisProject({ instanceDir }, deps = {}) {
44
+ const { db } = (deps.resolveInstanceDb || require('./upgrade').resolveInstanceDb)({ instanceDir }, deps.run);
45
+ if (db) return null;
46
+ const door = await (deps.readUpdateDoor || readUpdateDoor)();
47
+ return door && door.kind === 'hub' ? door : null;
48
+ }
49
+
50
+ // What the builder (and any assistant reading over their shoulder) is told. Plain words,
51
+ // one supported path, no server steps and nothing for anyone to fill in.
52
+ function operatorRunMessage(door = {}) {
53
+ const settings = door.hall ? `${door.hall}/settings#software-update` : 'Settings → Software update';
54
+ return [
55
+ 'This project is upgraded by its operator, not from this computer.',
56
+ 'It runs on the operator\'s server, and nothing you run here can upgrade it.',
57
+ '',
58
+ 'To ask for the upgrade:',
59
+ ` • Open ${settings} and use the button there.`,
60
+ ' • Or, from this terminal: bongos feedback "Please upgrade this project to the latest version"',
61
+ '',
62
+ 'There are no server steps for you to take. An assistant should not write any, or guess',
63
+ 'server, database or service names for this project — pass the request on instead.',
64
+ ].join('\n') + '\n';
65
+ }
66
+
67
+ // The message to show, or null — what `bongos upgrade` (upgrade.js main) asks before anything else.
68
+ async function operatorNotice({ instanceDir }, deps = {}) {
69
+ const door = await operatorRunsThisProject({ instanceDir }, deps);
70
+ return door ? operatorRunMessage(door) : null;
71
+ }
72
+
73
+ module.exports = { readUpdateDoor, operatorRunsThisProject, operatorRunMessage, operatorNotice };
@@ -1433,7 +1433,7 @@ function parseArgs(argv) {
1433
1433
  };
1434
1434
  }
1435
1435
 
1436
- async function main(argv = process.argv.slice(2)) {
1436
+ async function main(argv = process.argv.slice(2), deps = {}) {
1437
1437
  if (hasFlag('--help', argv) || argv[0] === 'help') {
1438
1438
  process.stdout.write([
1439
1439
  'bongos upgrade — pull the Cloud Bongos core to a new version and apply it.',
@@ -1471,9 +1471,14 @@ async function main(argv = process.argv.slice(2)) {
1471
1471
  }
1472
1472
  // Backward-compatible: bare `bongos upgrade` (no --to) is the documented module-compatibility
1473
1473
  // PRE-CHECK — unchanged behavior, delegated to the module CLI. A --to switches to the real channel.
1474
+ // task 1004596: from a builder's own checkout of a project its OPERATOR runs, say who upgrades it and how to ask — never a server-only refusal (upgrade-operator.js has the condition).
1475
+ const operatorMsg = await (deps.operatorNotice || require('./upgrade-operator').operatorNotice)({ instanceDir: path.resolve(arg('--instance', argv) || process.cwd()) });
1474
1476
  if (!arg('--to', argv)) {
1475
- return require('./module').cmdUpgrade(argv);
1477
+ const code = await (deps.cmdUpgrade || require('./module').cmdUpgrade)(argv);
1478
+ if (operatorMsg) process.stdout.write(`\n${operatorMsg}`);
1479
+ return code;
1476
1480
  }
1481
+ if (operatorMsg) { process.stderr.write(`\n✖ ${operatorMsg}`); return 1; }
1477
1482
  const res = await runUpgrade(parseArgs(argv));
1478
1483
  if (!res.ok) { process.stderr.write(`\n✖ ${res.error || 'upgrade failed'}\n${require('./upgrade-outcome').outcomeLine(res)}`); return 1; } // the outcome line: how far it got, for the deploy runner (task 1004446)
1479
1484
  return 0;
@@ -180,6 +180,9 @@ const CANNED_WRITES = [
180
180
  // a vote cast from the street's matter card (task 1004528), so the card's
181
181
  // answer (the toast, the building lighting) renders here too.
182
182
  ['POST', /^government\/board\/items\/\d+\/votes$/, 'government__board__items__votes.post'],
183
+ // Settings → Software update's "Ask for this upgrade" (task 1004595), so the
184
+ // pending line the panel draws after the press renders here too.
185
+ ['POST', /^software-update\/request$/, 'software-update__request.post'],
183
186
  ];
184
187
 
185
188
  // A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request