@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.
- package/.bongos-core.json +58 -38
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0029-status-page-outage-resilience.md +2 -2
- package/docs/api/openapi.json +4 -2
- package/docs/copy-inventory.md +36 -32
- package/docs/copy-registry.json +86 -50
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +7 -7
- package/docs/recipes/core-release-pipeline.md +3 -3
- package/modules/hall-ui/public/settings.js +49 -5
- package/modules/lifecycle/merge-reach.js +15 -8
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +34 -0
- package/scripts/bongos/claim.js +16 -5
- package/scripts/bongos/deploy-divergence.js +5 -3
- package/scripts/bongos/docs-entropy.js +4 -1
- package/scripts/bongos/fitness-lib.js +19 -1
- package/scripts/bongos/paste-token.js +75 -4
- package/scripts/bongos/ship-check.js +11 -3
- package/src/bongos/device-flow-blocker.js +56 -0
- package/src/bongos/routes/auth.js +5 -0
- package/src/instance-config.js +7 -10
- package/src/module-api.js +1 -1
- package/tests/agent_standing_rules.mjs +14 -0
- package/tests/bongos_cli.mjs +2 -1
- package/tests/claim_finish_hint.mjs +50 -0
- package/tests/cli_token_reissue.mjs +4 -2
- package/tests/deploy_divergence_line.mjs +5 -1
- package/tests/hall_onboarding_bongos_setup.mjs +3 -1
- package/tests/hall_settings_world.mjs +2 -2
- package/tests/instance_config.mjs +32 -1
- package/tests/paste_token_windows.mjs +165 -0
- package/tests/shell_hooks.mjs +7 -0
- package/tests/status_mirror_sync.mjs +22 -1
- 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.
|
|
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.
|
|
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.
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -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
|
}
|
package/scripts/bongos/claim.js
CHANGED
|
@@ -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(
|
|
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)
|
|
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
|
|
339
|
-
say(' owner
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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);
|
package/src/instance-config.js
CHANGED
|
@@ -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
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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);
|
package/tests/bongos_cli.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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], /
|
|
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
|
|