@bongos/core 1.24.68 → 1.24.70

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 (36) hide show
  1. package/.bongos-core.json +58 -38
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/docs/adr/0029-status-page-outage-resilience.md +2 -2
  4. package/docs/api/openapi.json +4 -2
  5. package/docs/copy-inventory.md +36 -32
  6. package/docs/copy-registry.json +86 -50
  7. package/docs/module-api-changelog.md +2 -0
  8. package/docs/page-readings.json +7 -7
  9. package/docs/recipes/core-release-pipeline.md +3 -3
  10. package/modules/hall-ui/public/settings.js +49 -5
  11. package/modules/lifecycle/merge-reach.js +15 -8
  12. package/package-lock.json +2 -2
  13. package/package.json +1 -1
  14. package/release-notes.json +34 -0
  15. package/scripts/bongos/claim.js +16 -5
  16. package/scripts/bongos/deploy-divergence.js +5 -3
  17. package/scripts/bongos/docs-entropy.js +4 -1
  18. package/scripts/bongos/fitness-lib.js +19 -1
  19. package/scripts/bongos/paste-token.js +75 -4
  20. package/scripts/bongos/ship-check.js +11 -3
  21. package/src/bongos/device-flow-blocker.js +56 -0
  22. package/src/bongos/routes/auth.js +5 -0
  23. package/src/instance-config.js +7 -10
  24. package/src/module-api.js +1 -1
  25. package/tests/agent_standing_rules.mjs +14 -0
  26. package/tests/bongos_cli.mjs +2 -1
  27. package/tests/claim_finish_hint.mjs +50 -0
  28. package/tests/cli_token_reissue.mjs +4 -2
  29. package/tests/deploy_divergence_line.mjs +5 -1
  30. package/tests/hall_onboarding_bongos_setup.mjs +3 -1
  31. package/tests/hall_settings_world.mjs +2 -2
  32. package/tests/instance_config.mjs +32 -1
  33. package/tests/paste_token_windows.mjs +165 -0
  34. package/tests/shell_hooks.mjs +7 -0
  35. package/tests/status_mirror_sync.mjs +22 -1
  36. package/tests/vendored_work_root.mjs +72 -0
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.68",
3
+ "version": "1.24.70",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.68",
9
+ "version": "1.24.70",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.68",
3
+ "version": "1.24.70",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -11411,5 +11411,39 @@
11411
11411
  "id": "1005132",
11412
11412
  "text": "The design checker now catches text hidden under a dark overlay (the bug that made the components page unreadable in dark mode), instead of reporting it clean."
11413
11413
  }
11414
+ ],
11415
+ "1.24.70": [
11416
+ {
11417
+ "id": "1005100",
11418
+ "text": "In a project that installs Bongos from npm (like toast), the docs check and the pre-ship check now look at the project itself instead of Bongos's own installed files, so they stop failing on nothing and stop reporting fresh fi"
11419
+ },
11420
+ {
11421
+ "id": "1005033",
11422
+ "text": "Signing in to the command line on Windows is smoother: Settings now shows a command that works in PowerShell, says to go to your project folder first, and the tool can ask for the token instead of it sitting in the command."
11423
+ },
11424
+ {
11425
+ "id": "1005113",
11426
+ "text": "In a project that installs Bongos from npm (like toast), the claim card now tells you to finish with 'npx bongos ship', a command that exists there, instead of a script path that doesn't."
11427
+ },
11428
+ {
11429
+ "id": "1005131",
11430
+ "text": "The status-page docs no longer claim an automatic sync that does not exist. They now say plainly that the off-site status page updates only when someone publishes it by hand, and a test keeps that claim honest."
11431
+ },
11432
+ {
11433
+ "id": "1005022",
11434
+ "text": "The 'you may have made a mistake' nudge no longer fires on automatic messages from other sessions or CI — only on a person's own corrections."
11435
+ },
11436
+ {
11437
+ "id": "1005020",
11438
+ "text": "Status messages now say plainly that cloudbongos.com picks up small (patch) releases on its own — no one has to move it by hand. Only a bigger (minor) release waits for the owner."
11439
+ },
11440
+ {
11441
+ "id": "1005042",
11442
+ "text": "The old-style settings warning no longer promises a removal date that already passed; it now says honestly the old names still work for now."
11443
+ },
11444
+ {
11445
+ "id": "1004949",
11446
+ "text": "When a shell command was blocked by one hook and another hook was missing, the warning about the missing hook vanished. Now both messages show."
11447
+ }
11414
11448
  ]
11415
11449
  }
@@ -763,10 +763,7 @@ async function main() {
763
763
  // best-effort and never block the claim.
764
764
  const shownLearningKeys = await printRelevantLearnings(task);
765
765
  await printClaimRecall(task, { excludeKeys: shownLearningKeys });
766
- console.log('\nWhen you finish:');
767
- console.log(` node scripts/bongos/ship.js ${task.id} --notes "..." --summary "..."`);
768
- console.log('Or to abandon:');
769
- console.log(` node scripts/bongos/release.js ${task.id}`);
766
+ for (const line of finishHintLines(task.id)) console.log(line);
770
767
  // V3.R13 (#237): post-claim nudge — for a newcomer, this becomes
771
768
  // "Next: Ship thy first work" so the path forward is visible. Reuses the
772
769
  // /me fetched above for the context pack (no second round-trip).
@@ -802,6 +799,20 @@ async function main() {
802
799
 
803
800
  // Only auto-run when invoked directly (`node claim.js …`); a `require()` (the
804
801
  // unit test) just imports formatClaimFailure without executing the claim flow.
802
+ // task 1005113 - claim card finish: the "When you finish" hint names the command that
803
+ // exists where this claim ran. A project that installs the core from npm has no
804
+ // scripts/bongos/ of its own — the CLI ships in node_modules/@bongos/core and runs as
805
+ // `npx bongos <verb>` — so a node path there points at nothing. isInstalledCore() is the
806
+ // one positive "this core sits under node_modules" check fitness already uses.
807
+ function finishHintLines(taskId, { installed } = {}) {
808
+ if (installed === undefined) {
809
+ try { installed = require('./fitness-lib').isInstalledCore(); } catch { installed = false; }
810
+ }
811
+ const ship = installed ? `npx bongos ship ${taskId}` : `node scripts/bongos/ship.js ${taskId}`;
812
+ const release = installed ? `npx bongos release ${taskId}` : `node scripts/bongos/release.js ${taskId}`;
813
+ return ['\nWhen you finish:', ` ${ship} --notes "..." --summary "..."`, 'Or to abandon:', ` ${release}`];
814
+ }
815
+
805
816
  if (require.main === module) {
806
817
  main().catch((err) => {
807
818
  console.error('fatal:', err);
@@ -809,4 +820,4 @@ if (require.main === module) {
809
820
  });
810
821
  }
811
822
 
812
- module.exports = { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, materializeModeSkills, printRoleLine, worktreeDetectionNote, liveStrandLeases, QUEUE_GATED_EXIT };
823
+ module.exports = { finishHintLines, formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, materializeModeSkills, printRoleLine, worktreeDetectionNote, liveStrandLeases, QUEUE_GATED_EXIT };
@@ -5,7 +5,8 @@
5
5
  // modules/lifecycle/merge-reach.js): every instance, the hub included, gets a merge
6
6
  // only when a published core release carries it AND that instance's pin moves. The
7
7
  // publish lane cuts a release per merge (ADR 0161) only while the integration lane
8
- // is green; on the hub (cloudbongos.com) the pin move is owner-gated.
8
+ // is green; on the hub (cloudbongos.com) its patch-channel subscription moves the pin
9
+ // on its own (task 1005020), and only a new minor release waits for the owner.
9
10
  // Two blanket claims have been wrong here: "Merging to main deploys nothing" (ADR
10
11
  // 0137, false since ADR 0161) and "the hub redeploys from main on every merge"
11
12
  // (never true — the hub pins a published core). Sessions repeated each to the owner.
@@ -335,8 +336,9 @@ function report(r, { quiet = false } = {}) {
335
336
  }
336
337
  if (r.behind) {
337
338
  say(` ⚠ UNDEPLOYED — live is ${r.live.coreVersion}, main declares ${r.onMain}.`);
338
- say(' A release exists that this instance has not picked up: its pin has not moved. On the hub that move is');
339
- say(' owner-gated; on a hosted project it is its update channel or a core-upgrade (docs/recipes/upgrading-the-core.md).');
339
+ say(' A release exists that this instance has not picked up: its pin has not moved. On the hub its patch-channel');
340
+ say(' sweep moves it on its own (a new minor waits for the owner); on a hosted project it is its update channel or a');
341
+ say(' core-upgrade (docs/recipes/upgrading-the-core.md). A gap that stays means the sweep is held or failing.');
340
342
  }
341
343
  if (r.schemaBehind) {
342
344
  const n = r.live.schemaPending;
@@ -25,7 +25,10 @@ const path = require('node:path');
25
25
  const { execFileSync } = require('node:child_process');
26
26
  const { hasFlag, cliClient, requireSession, cliExit } = require('./cli-lib');
27
27
 
28
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
28
+ // The tree being scanned (task 1005100): this checkout when the core is a checkout, the
29
+ // PROJECT when the core is installed under node_modules — the package dir is not a repo of
30
+ // its own, so `git ls-files` there found 0 files and fitness died on a "scan defect".
31
+ const REPO_ROOT = require('./fitness-lib.js').resolveWorkRoot();
29
32
  const { currentScriptPath } = require('./forwarder.js');
30
33
  const MIN_DUPE_CHARS = 120; // minimum paragraph length to flag as duplicate
31
34
  const MAX_DUPES_REPORTED = 20; // cap to avoid flooding the report
@@ -11,7 +11,7 @@
11
11
 
12
12
  const fs = require('node:fs');
13
13
  const path = require('node:path');
14
- const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-config'); // ADR 0108 §1: configurable-root
14
+ const { resolveCoreRoot, resolveInstanceRoot, resolveInstanceRootExplicit } = require('../../src/instance-config'); // ADR 0108 §1: configurable-root
15
15
 
16
16
 
17
17
  // ADR 0108 §Consequences #4 (configurable-root W6 / task 1886) — the guardrail walks TWO trees.
@@ -97,6 +97,23 @@ function isInstalledCore(root = CORE_ROOT) {
97
97
  } catch { return true; } // a package with no repo around it at all
98
98
  }
99
99
 
100
+ // resolveWorkRoot — the tree a repo-wide tool (docs-entropy, ship-check) works on (task 1005100).
101
+ // A core CHECKOUT works on itself, whatever the cwd, exactly as path.resolve(__dirname,'..','..')
102
+ // always did. An INSTALLED core's package dir is not the project — `git ls-files` there finds
103
+ // nothing and the generators read the package's own files — so it works on the project instead:
104
+ // an explicit pointer (<PREFIX>_INSTANCE_ROOT / bongos.config.json), else the git top of the cwd
105
+ // (`npx bongos exec` runs from the project), else the cwd.
106
+ function gitTopOf(cwd) {
107
+ try {
108
+ return require('node:child_process').execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
109
+ } catch { return null; }
110
+ }
111
+ function resolveWorkRoot({ installed, cwd = process.cwd(), explicit = resolveInstanceRootExplicit, gitTop = gitTopOf } = {}) {
112
+ if (installed === undefined) installed = isInstalledCore();
113
+ if (!installed) return CORE_ROOT;
114
+ return explicit() || gitTop(cwd) || cwd;
115
+ }
116
+
100
117
  // runChecks — run every fitness check, or none (task 1004737). Every check guards the core's
101
118
  // own source, and an INSTALLED core's source is not the project's to change: the core repo's
102
119
  // CI ran this gate before publishing it (the same stance ship-merge-gate.js takes for a
@@ -115,6 +132,7 @@ function runChecks(checks, { installed = isInstalledCore() } = {}) {
115
132
 
116
133
  module.exports = {
117
134
  isInstalledCore,
135
+ resolveWorkRoot,
118
136
  runChecks,
119
137
  keyTokenRe,
120
138
  CORE_ROOT,
@@ -8,6 +8,8 @@
8
8
  //
9
9
  // Usage:
10
10
  // bongos paste-token <token> # what the hall hands you (task 1003961)
11
+ // bongos paste-token # no argument: asks for the token without echo,
12
+ // # or reads it from stdin when piped (task 1005033)
11
13
  // node scripts/bongos/paste-token.js <token> # equivalent, inside a core checkout
12
14
  //
13
15
  // The portal returns the exact invocation as the "paste_command" field, so a
@@ -83,10 +85,71 @@ const ARGS = parseArgs(process.argv.slice(2));
83
85
  const RESOLVED = resolveBaseForPaste({ flag: ARGS.apiBase });
84
86
  const API_BASE = RESOLVED.base;
85
87
 
88
+ // READ THE TOKEN WITHOUT PUTTING IT ON THE COMMAND LINE (task 1005033). A token passed
89
+ // as an argument lands in shell history. With no argument we ask for it: on a terminal
90
+ // the read is raw and nothing is echoed (a 64-char secret has no business on screen
91
+ // either); piped (`... | bongos paste-token`) we take the first word of stdin. The
92
+ // argument form still works — the hall's one-liner uses it, and older instances hand
93
+ // out nothing else. Exported so the no-echo read is tested, not assumed.
94
+ function readTokenFromInput({ input = process.stdin, output = process.stderr } = {}) {
95
+ if (!input.isTTY) {
96
+ // Piped: settle on the FIRST line, not at end of stream — a pipe that never closes
97
+ // must not hang the paste, and nothing after the token is ours to buffer.
98
+ return new Promise((resolve, reject) => {
99
+ let data = '';
100
+ let settled = false;
101
+ const settle = () => {
102
+ if (settled) return;
103
+ settled = true;
104
+ input.removeListener('data', onData);
105
+ input.pause();
106
+ resolve(data.trim().split(/\s+/)[0] || '');
107
+ };
108
+ function onData(c) {
109
+ data += c;
110
+ if (data.replace(/^\s+/, '').includes('\n')) settle();
111
+ }
112
+ input.setEncoding('utf8');
113
+ input.on('data', onData);
114
+ input.on('end', settle);
115
+ input.on('error', (err) => { if (!settled) { settled = true; reject(err); } });
116
+ });
117
+ }
118
+ output.write('Paste your CLI token (Settings → CLI Access) and press Enter: ');
119
+ return new Promise((resolve, reject) => {
120
+ let buf = '';
121
+ const raw = typeof input.setRawMode === 'function';
122
+ if (raw) input.setRawMode(true);
123
+ input.setEncoding('utf8');
124
+ input.resume();
125
+ function finish(err) {
126
+ input.removeListener('data', onData);
127
+ if (raw) input.setRawMode(false);
128
+ input.pause();
129
+ output.write('\n');
130
+ if (err) reject(err); else resolve(buf.trim());
131
+ }
132
+ function onData(chunk) {
133
+ for (const ch of String(chunk)) {
134
+ if (ch === '\r' || ch === '\n') { finish(); return; }
135
+ // Ctrl+C always cancels; Ctrl+D (end of input) cancels too unless a token is typed.
136
+ if (ch === '\u0003' || (ch === '\u0004' && !buf.trim())) { finish(new Error('cancelled — nothing was saved')); return; }
137
+ if (ch === '\u0004') { finish(); return; }
138
+ if (ch === '\u007f' || ch === '\b') { buf = buf.slice(0, -1); continue; }
139
+ buf += ch;
140
+ }
141
+ }
142
+ input.on('data', onData);
143
+ });
144
+ }
145
+
86
146
  function usage(extra) {
87
147
  if (extra) console.error(extra);
88
148
  console.error('');
89
- console.error('Usage: npx bongos paste-token <token> [--api-base <url>]');
149
+ console.error('Usage: npx bongos paste-token [<token>] [--api-base <url>]');
150
+ console.error(' Leave out <token> and it asks for it, so the token stays out of shell history.');
151
+ console.error(' Run it from inside your project folder (cd there first).');
152
+ console.error(' In Windows PowerShell type npx.cmd instead of npx.');
90
153
  console.error(' (inside a core checkout: node scripts/bongos/paste-token.js <token>)');
91
154
  console.error('');
92
155
  // Fall back to the inferred base for this hint when --api-base was the thing that
@@ -103,9 +166,17 @@ async function main() {
103
166
  usage(`--api-base must be an http(s) URL, got: ${ARGS.apiBase}`);
104
167
  process.exit(2);
105
168
  }
106
- const token = ARGS.token;
169
+ let token = ARGS.token;
170
+ if (!token) {
171
+ try {
172
+ token = await readTokenFromInput();
173
+ } catch (err) {
174
+ console.error(`paste-token: ${err.message}`);
175
+ process.exit(2);
176
+ }
177
+ }
107
178
  if (!token) {
108
- usage('No token argument provided.');
179
+ usage('No token given.');
109
180
  process.exit(2);
110
181
  }
111
182
  // Tokens are hex-encoded crypto.randomBytes(32) → 64 lowercase hex chars.
@@ -209,4 +280,4 @@ if (require.main === module) {
209
280
  });
210
281
  }
211
282
 
212
- module.exports = { parseArgs, resolveBaseForPaste };
283
+ module.exports = { parseArgs, resolveBaseForPaste, readTokenFromInput };
@@ -21,7 +21,12 @@ const { spawnSync } = require('node:child_process');
21
21
  const fs = require('node:fs');
22
22
  const path = require('node:path');
23
23
 
24
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
24
+ // Two roots (task 1005100). CORE_ROOT is where the step scripts live; WORK_ROOT is the tree they
25
+ // check. In a project that installs the core they differ: running the generators from the
26
+ // package dir checked the package's files, so gen-api-docs read STALE while the same --check
27
+ // run from the project said up to date. In a core checkout both are this checkout.
28
+ const CORE_ROOT = path.resolve(__dirname, '..', '..');
29
+ const WORK_ROOT = require('./fitness-lib.js').resolveWorkRoot();
25
30
 
26
31
  // Each step: a label, the argv to run (node scripts run with THIS node), the command that heals it,
27
32
  // and how to read a non-zero exit. `optional` steps are skipped when their script is absent. An `advisory`
@@ -45,8 +50,11 @@ const STEPS = [
45
50
  { key: 'tests', label: 'DB-free unit lane (npm test)', argv: ['scripts/bongos/run-unit-tests.js'], fix: 'read the FAIL lines; compare against origin/main before calling any of them environmental', onlyWith: '--tests', slow: true },
46
51
  ];
47
52
 
48
- function runStep(step, { cwd = REPO_ROOT } = {}) {
49
- const script = path.join(cwd, step.argv[0]);
53
+ // A caller that passes only `cwd` (tests) gets scripts resolved there too, as before.
54
+ function runStep(step, { cwd, coreRoot } = {}) {
55
+ coreRoot = coreRoot || cwd || CORE_ROOT;
56
+ cwd = cwd || WORK_ROOT;
57
+ const script = path.join(coreRoot, step.argv[0]);
50
58
  if (!fs.existsSync(script)) return { ...step, status: step.optional ? 'skipped' : 'missing', ms: 0, tail: `${step.argv[0]} not present` };
51
59
  const t0 = Date.now();
52
60
  const r = spawnSync(process.execPath, [script, ...step.argv.slice(1)], { cwd, encoding: 'utf8', timeout: step.slow ? 15 * 60 * 1000 : 5 * 60 * 1000, env: { ...process.env, GDS_QUIET: '1' } });
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+ // src/bongos/device-flow-blocker.js — tell the OWNER, loudly, that terminal sign-in is off
3
+ // (task 1005033).
4
+ //
5
+ // WHY. GitHub creates an App from a manifest with Device Flow OFF and has no API to switch it
6
+ // on — only a checkbox in the App's settings. Until the owner ticks it, every builder's
7
+ // `bongos setup` / `bongos login` is refused, and the only person told was that builder, in a
8
+ // terminal, who cannot fix it. The provisioning checklist shows the step as optional
9
+ // ("later"), which no owner reads as urgent. So the first time this server sees GitHub's
10
+ // device_flow_disabled refusal it files ONE blocker for the owner, through the ideas
11
+ // module's `ideas.fileBlocker` port (core never imports a module). The writer is idempotent
12
+ // on (source, source_ref), so a thousand refused sign-ins still make one blocker.
13
+ //
14
+ // Best effort by construction: no ideas module, a refused write or a DB hiccup changes
15
+ // nothing about the sign-in reply the builder gets. Never throws, never rejects.
16
+
17
+ const seams = require('../module-seams');
18
+
19
+ const SOURCE = 'auth';
20
+ const SOURCE_REF = 'github-device-flow-disabled';
21
+
22
+ function isDeviceFlowDisabledReason(reason) {
23
+ return /device_flow_disabled/i.test(String(reason || ''));
24
+ }
25
+
26
+ function blockerFor(origin) {
27
+ const settings = origin ? `${origin}/builders/settings` : 'Settings → CLI Access';
28
+ return {
29
+ title: 'Terminal sign-in is off: tick "Enable Device Flow" on the GitHub App',
30
+ bodyMd: [
31
+ 'A builder just tried to sign in from a terminal (`bongos setup` or `bongos login`) and GitHub refused:',
32
+ 'this project\'s GitHub App has **Device Flow switched off**. Apps made by the one-click setup start',
33
+ 'that way, and GitHub offers no way for the setup to switch it on.',
34
+ '',
35
+ '**Fix (the project owner, once, about a minute):** GitHub → Settings → Developer settings →',
36
+ 'GitHub Apps → this project\'s app → tick **Enable Device Flow** → Save. Web sign-in is unaffected.',
37
+ '',
38
+ `Until then builders can still sign in through the web: ${settings}, "Re-issue CLI token".`,
39
+ ].join('\n'),
40
+ source: SOURCE,
41
+ sourceRef: SOURCE_REF,
42
+ };
43
+ }
44
+
45
+ async function raiseDeviceFlowOffBlocker({ reason, origin, resolveOptional = seams.resolveOptional } = {}) {
46
+ try {
47
+ if (!isDeviceFlowDisabledReason(reason)) return null;
48
+ const file = resolveOptional('ideas.fileBlocker', null);
49
+ if (typeof file !== 'function') return null;
50
+ return (await file(blockerFor(origin))) || null;
51
+ } catch (_) {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ module.exports = { raiseDeviceFlowOffBlocker, isDeviceFlowDisabledReason, SOURCE, SOURCE_REF };
@@ -42,6 +42,7 @@ const { accountExistenceReadRateLimit } = require('../middleware/rate-limit');
42
42
  const { branding } = require('../../branding');
43
43
  const { resolveEnv, envPrefix } = require('../../instance-config');
44
44
  const seams = require('../../module-seams');
45
+ const { raiseDeviceFlowOffBlocker } = require('../device-flow-blocker');
45
46
  // task 1003208: structured logging — was console.*. A core file logs through
46
47
  // the kernel logger directly; only MODULES go via api.logger (the doorway).
47
48
  const log = require('../logger').logger.child({ src: 'auth' });
@@ -623,6 +624,7 @@ module.exports = function buildAuthRouter() {
623
624
  // fresh-instance auth failure could only be read via curl on the box itself
624
625
  // (finding W2, task 1003052). A 4xx passes the proxy with the JSON intact.
625
626
  const reason = String(err && err.message ? err.message : 'internal error').slice(0, 300);
627
+ raiseDeviceFlowOffBlocker({ reason, origin: WEB_REDIRECT_ORIGIN }); // owner blocker, once (task 1005033)
626
628
  res.fail('device_start_failed', { status: 424, message: reason });
627
629
  }
628
630
  });
@@ -1353,6 +1355,9 @@ module.exports = function buildAuthRouter() {
1353
1355
  paste_command: issuer
1354
1356
  ? `npx bongos paste-token ${token} --api-base ${issuer}`
1355
1357
  : `npx bongos paste-token ${token}`,
1358
+ paste_command_windows: issuer
1359
+ ? `npx.cmd bongos paste-token ${token} --api-base ${issuer}`
1360
+ : `npx.cmd bongos paste-token ${token}`, // task 1005033: PowerShell blocks npx.ps1 by default
1356
1361
  });
1357
1362
  } catch (err) {
1358
1363
  log.error('[gds] POST /auth/cli-token/issue', err);
@@ -46,12 +46,10 @@ const LEGACY_DIRS = ['otb'];
46
46
  // test that it FIRES rather than one that merely tolerates it.
47
47
  const LEGACY_ENV_PREFIXES = ['CLOUDBONGOS', 'OTB', 'GDS', 'PMS'];
48
48
 
49
- // The core release in which the legacy prefixes above stop being read. Named in
50
- // every deprecation warning so an operator learns the deadline from the warning
51
- // itself rather than from a changelog they will not read. This lands in 1.19.x,
52
- // so 1.21 leaves a full minor of overlap; task 1003706 is the task that both
53
- // removes the fallback and moves this constant's meaning to "already gone".
54
- const LEGACY_ENV_SUNSET_RELEASE = '1.21';
49
+ // The deprecation warning names no removal release (task 1005042 - legacy env
50
+ // warning): it promised 1.21, the retirement (task 1003706 - C3d) was abandoned,
51
+ // and 1.24 still read the old names. Name a real release only in the diff that
52
+ // deletes the fallback.
55
53
 
56
54
  // Lazy require (ADR 0108 §1 made this a two-way edge: branding.js now requires
57
55
  // resolveCoreRoot()/resolveInstanceRoot() from this module, so a top-level
@@ -267,7 +265,7 @@ function warnLegacyEnv({ legacyName, canonicalName, shadowed, differs, log = con
267
265
  : shadowed
268
266
  ? `${canonicalName} is also set and wins`
269
267
  : `rename it to ${canonicalName}`;
270
- log.warn(`[bongos] deprecated env ${legacyName} — ${fate}. The legacy spelling stops being read in core ${LEGACY_ENV_SUNSET_RELEASE}.`);
268
+ log.warn(`[bongos] deprecated env ${legacyName} — ${fate}. The legacy spelling is still read for now, but will stop being read in a future core release.`);
271
269
  return true;
272
270
  }
273
271
 
@@ -282,11 +280,10 @@ function warnPackPrefix({ prefix, name, log = console } = {}) {
282
280
  return true;
283
281
  }
284
282
 
285
- // NOT exported, deliberately: warnLegacyEnv, LEGACY_ENV_SUNSET_RELEASE and the
286
- // ledger are internals. resolveEnv's injectable `warn` is the whole test seam a
283
+ // NOT exported, deliberately: warnLegacyEnv and the ledger are internals. resolveEnv's injectable `warn` is the whole test seam a
287
284
  // caller needs, and a test of the DEFAULT path captures console.warn — which
288
285
  // exercises the real wiring rather than a hand-called helper. Exporting them
289
- // would also add three untraceable entries to the knip dead-code ratchet.
286
+ // would also add untraceable entries to the knip dead-code ratchet.
290
287
 
291
288
  // resolveCoreRoot() / resolveInstanceRoot() — the ADR 0108 §1 configurable-root
292
289
  // resolvers, extending this module's established zero-risk pattern from
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.68'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.70'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -112,6 +112,20 @@ t('the app\'s own status messages never trigger the slop check (task 1005023)',
112
112
  assert.ok(conductor.evaluateTriggers({ prompt: "that's wrong, it's already live" }).some((x) => x.id === 'slop-check'), 'a real correction still fires');
113
113
  });
114
114
 
115
+ t('side-session events, cross-session messages and CI events never trigger the slop check (task 1005022)', () => {
116
+ const words = "It's already live, not true, actually it works, you said it was slop";
117
+ for (const notice of [
118
+ `<side-sessions-event>Session bug-x ended a turn: ${words}</side-sessions-event>`,
119
+ `<cross-session-message from="local_abc">${words}</cross-session-message>`,
120
+ `<teammate-message teammate_id="t1">${words}</teammate-message>`,
121
+ `<ci-monitor-event pr="12">${words}</ci-monitor-event>`,
122
+ ` \n<channel source="discord">${words}</channel>`,
123
+ ]) {
124
+ assert.ok(!conductor.evaluateTriggers({ prompt: notice }).some((x) => x.id === 'slop-check'), notice);
125
+ }
126
+ assert.ok(conductor.evaluateTriggers({ prompt: "<b>that's wrong</b>, it's already live" }).some((x) => x.id === 'slop-check'), 'a person typing a tag still gets checked');
127
+ });
128
+
115
129
  t('ordinary prompts do not trigger the slop check', () => {
116
130
  for (const p of ['add a dark mode toggle', 'run the tests', 'what can I work on', 'make the button wider']) {
117
131
  assert.ok(!conductor.evaluateTriggers({ prompt: p }).some((x) => x.id === 'slop-check'), p);
@@ -76,7 +76,8 @@ test('the server emits exactly the verb the CLI registers', () => {
76
76
  assert.ok(cmds.length > 0, 'auth.js should emit at least one paste_command template literal');
77
77
  for (const cmd of cmds) {
78
78
  // `npx bongos <verb> …` since task 1004575 — a clone has no bare `bongos` on PATH.
79
- const verb = cmd.replace(/^npx /, '').split(' ')[1];
79
+ // npx.cmd is the Windows PowerShell form (task 1005033) — same verb after it.
80
+ const verb = cmd.replace(/^npx(\.cmd)? /, '').split(' ')[1];
80
81
  assert.ok(cli.VERBS[verb], `server emits "bongos ${verb}" but the CLI has no such verb`);
81
82
  }
82
83
  });
@@ -0,0 +1,50 @@
1
+ // tests/claim_finish_hint.mjs
2
+ //
3
+ // task 1005113 - claim card finish: the claim card's "When you finish" hint printed
4
+ // `node scripts/bongos/ship.js N` everywhere. In a project that installs the core from npm
5
+ // (toast), that path is not in the project — the CLI ships inside node_modules/@bongos/core
6
+ // and runs as `npx bongos ship N`. finishHintLines() picks the command for where the core
7
+ // is: a core checkout keeps the node path; an installed core gets the npx verbs.
8
+ //
9
+ // Run: node tests/claim_finish_hint.mjs
10
+
11
+ import { strict as assert } from 'node:assert';
12
+ import { createRequire } from 'node:module';
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const { finishHintLines } = require('../scripts/bongos/claim.js');
16
+
17
+ let passed = 0;
18
+ let failed = 0;
19
+ function t(name, fn) {
20
+ try { fn(); passed++; console.log(` PASS ${name}`); }
21
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
22
+ }
23
+
24
+ t('installed core: ship + release hints use npx bongos, never a scripts/ path', () => {
25
+ const text = finishHintLines('1005113', { installed: true }).join('\n');
26
+ assert.match(text, /npx bongos ship 1005113 --notes "\.\.\." --summary "\.\.\."/);
27
+ assert.match(text, /npx bongos release 1005113/);
28
+ assert.doesNotMatch(text, /scripts\//);
29
+ });
30
+
31
+ t('core checkout: hints keep the node scripts/bongos path', () => {
32
+ const text = finishHintLines('42', { installed: false }).join('\n');
33
+ assert.match(text, /node scripts\/bongos\/ship\.js 42 --notes/);
34
+ assert.match(text, /node scripts\/bongos\/release\.js 42/);
35
+ assert.doesNotMatch(text, /scripts\/gds\//);
36
+ });
37
+
38
+ t('hint keeps its headings', () => {
39
+ const lines = finishHintLines('7', { installed: true });
40
+ assert.equal(lines[0], '\nWhen you finish:');
41
+ assert.ok(lines.includes('Or to abandon:'));
42
+ });
43
+
44
+ t('defaults to detecting the core here (this checkout is not installed)', () => {
45
+ const text = finishHintLines('9').join('\n');
46
+ assert.match(text, /node scripts\/bongos\/ship\.js 9/);
47
+ });
48
+
49
+ console.log(`\n${passed} passed, ${failed} failed`);
50
+ if (failed) process.exit(1);
@@ -174,11 +174,13 @@ t('paste_command names a CLI VERB, never a filesystem path', () => {
174
174
  // applying without anyone editing it.
175
175
  const tail = issueBlock.slice(issueBlock.indexOf('paste_command:'));
176
176
  const cmds = [...tail.matchAll(/`([^`]*bongos paste-token[^`]*)`/g)].map((m) => m[1]);
177
- assert.equal(cmds.length, 2, `expected both ternary branches, got ${cmds.length}`);
177
+ // Four since task 1005033: paste_command's two branches, then paste_command_windows'
178
+ // two — the same line with npx.cmd, because npx.ps1 is blocked in a default PowerShell.
179
+ assert.equal(cmds.length, 4, `expected both ternary branches of both fields, got ${cmds.length}`);
178
180
  for (const cmd of cmds) {
179
181
  // Through npx (task 1004575): a builder's clone does not put bare `bongos` on
180
182
  // PATH, so a bare verb was "command not found" when pasted in the project folder.
181
- assert.ok(/^npx bongos paste-token /.test(cmd),
183
+ assert.ok(/^npx(\.cmd)? bongos paste-token /.test(cmd),
182
184
  `paste_command must start with the npx verb form, got: ${cmd}`);
183
185
  // The invariant is "names no FILE ON THE RECIPIENT'S DISK", which the original
184
186
  // spelling approximated as "contains no slash". That was too broad once the command
@@ -79,7 +79,11 @@ test('the line says who sees a merge, not a blanket rule in either direction', (
79
79
  const s = line(behind);
80
80
  assert.doesNotMatch(s, /deploys nothing/);
81
81
  assert.doesNotMatch(s, /redeploys from main|on every merge|within minutes/);
82
- assert.match(s, /cloudbongos[.]com gets a merge only when a published core release carries it and its pin moves [(]owner-gated[)]/);
82
+ assert.match(s, /cloudbongos[.]com gets a merge only when a published core release carries it and its pin moves/);
83
+ // task 1005020: the hub's pin is NOT owner-gated for a patch release — its patch-channel
84
+ // subscription moved cloudbongos.com to core 1.24.12 with no person (task 1005015).
85
+ assert.doesNotMatch(s, /owner-gated[)]/, 'a patch move on the hub needs no person');
86
+ assert.match(s, /its patch-channel subscription moves that pin on its own/);
83
87
  assert.match(s, /hosted projects: only when a published core release carries the change and their pin moves/);
84
88
  assert.match(s, /Never call work live until that instance's \/version shows it/);
85
89
  });
@@ -87,7 +87,9 @@ test('Settings CLI access says where to paste the npx command', () => {
87
87
  const js = read('modules/hall-ui/public/settings.js');
88
88
  assert.match(html, /into a terminal in your project folder/);
89
89
  assert.doesNotMatch(html, /bypasses Device Flow/);
90
- assert.match(js, /Paste this into a terminal in your project folder:/);
90
+ // Both spellings (task 1005033) name the project folder AND the cd that gets there.
91
+ assert.match(js, /Open a terminal, cd into your project folder first \(the command only runs there\), then paste this:/);
92
+ assert.match(js, /Open PowerShell, cd into your project folder first \(the command only runs there\), then paste this:/);
91
93
  });
92
94
 
93
95
  test('the npx paste command Settings hands out is pre-approved for a Claude session', () => {
@@ -338,8 +338,8 @@ test('the CLI token reaches the page as text, not as markup', () => {
338
338
  // The secret and the clipboard read the SAME resolved value, so the button can
339
339
  // never copy a different string than the one on screen — nor the literal
340
340
  // "undefined", which writeText(body.paste_command) produced on a body without one.
341
- assert.match(fn[0], /const pasteCommand = body\.paste_command \|\| ''/, 'one resolved value');
342
- assert.match(fn[0], /writeText\(pasteCommand\)/, 'the clipboard copies that same value');
341
+ assert.match(fn[0], /const pasteCommand = \(windows && body\.paste_command_windows\) \|\| body\.paste_command \|\| ''/, 'one resolved value');
342
+ assert.match(fn[0], /copy\(pasteCommand, /, 'the clipboard copies that same value');
343
343
  assert.match(fn[0], /result\.textContent = ''/, 're-issuing replaces the previous token rather than stacking a second');
344
344
  });
345
345