@bongos/core 1.19.579 → 1.19.581
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 +75 -40
- package/bin/bongos.js +10 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0260-the-application-is-the-consent-and-the-echo-is-the-gate.md +146 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +73 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +21 -18
- package/docs/copy-registry.json +50 -23
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/oversight.css +7 -0
- package/modules/hall-ui/public/watch.js +75 -0
- package/modules/onboarding/routes/access-requests.js +56 -1
- package/modules/platform-identity/applicant-profile.js +158 -0
- package/modules/platform-identity/application-echo.js +50 -0
- package/modules/platform-identity/recruiter-sliver.js +19 -0
- package/modules/platform-identity/routes/sso.js +126 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/build-cli-package.js +8 -2
- package/scripts/gds/cli-lib.js +11 -3
- package/scripts/gds/recall.js +1 -1
- package/scripts/gds/start.js +39 -26
- package/scripts/gds/surface.js +89 -0
- package/src/bongos/auth-admission.js +61 -0
- package/src/bongos/auth.js +2 -1
- package/src/module-api.js +11 -1
- package/tests/adr_renumber_integrity.mjs +84 -0
- package/tests/applicant_profile_boundary.mjs +641 -0
- package/tests/applicant_profile_read_bounds.mjs +95 -0
- package/tests/cli_surface.mjs +146 -0
- package/tests/module_api.mjs +1 -0
- package/tests/recruiter_sliver_boundary.mjs +35 -2
- package/tests/watch_applications_queue.mjs +94 -0
package/scripts/gds/start.js
CHANGED
|
@@ -156,7 +156,7 @@ function quietStatusLine(header) {
|
|
|
156
156
|
if (ideas != null) parts.push(`${ideas} idea${ideas === 1 ? '' : 's'} in inbox`);
|
|
157
157
|
if (blockers != null) parts.push(`${blockers} open blocker${blockers === 1 ? '' : 's'}`);
|
|
158
158
|
if (parts.length === 0) return null;
|
|
159
|
-
return
|
|
159
|
+
return surface.em(parts.join(' · '));
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// Calibrated estimate, compact: ~8m / ~2h / ~1.5h.
|
|
@@ -201,6 +201,7 @@ function whatItIs(t) {
|
|
|
201
201
|
// The claimable table — # / Version / Type / Reward / Est. / What it is.
|
|
202
202
|
// Ids are emitted as hall links (renders as a clean #N) — a bare "#NNN" in a
|
|
203
203
|
// relayed chat message autolinks to GitHub and 404s (CLAUDE.md ref rule).
|
|
204
|
+
const surface = require('./surface');
|
|
204
205
|
const HALL_TASK = `${hallBase()}#/task/`;
|
|
205
206
|
function claimableTable(rows, rewardFromServer) {
|
|
206
207
|
// On a cost-plus-only instance the Reward column is an advertised estimate that
|
|
@@ -208,15 +209,18 @@ function claimableTable(rows, rewardFromServer) {
|
|
|
208
209
|
// and footnote what IS paid below the table (task 1002469).
|
|
209
210
|
const perTask = rewardPolicy(rewardFromServer).perTaskCredit;
|
|
210
211
|
const rewardHead = perTask ? 'Reward' : 'Reward (est.)';
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
const headers = ['#', 'Version', 'Type', rewardHead, 'Est.', 'What it is'];
|
|
213
|
+
const body = rows.map((t) => [
|
|
214
|
+
surface.taskRef(t.id, HALL_TASK),
|
|
215
|
+
cell(t.version_id || '?'),
|
|
216
|
+
cell(disciplineLabel(t) || '—'),
|
|
217
|
+
`${t.credits_reward ?? 0}c`,
|
|
218
|
+
estimateShort(t),
|
|
219
|
+
cell(whatItIs(t)),
|
|
220
|
+
]);
|
|
221
|
+
const out = [surface.table(headers, body)];
|
|
218
222
|
const cpNote = costPlusOnlyNote(rewardFromServer);
|
|
219
|
-
if (cpNote) out.push('',
|
|
223
|
+
if (cpNote) out.push('', surface.em(cpNote));
|
|
220
224
|
return out.join('\n');
|
|
221
225
|
}
|
|
222
226
|
|
|
@@ -268,7 +272,8 @@ function rebaseWarningModel(needsRebaseTasks) {
|
|
|
268
272
|
function rebaseWarningMarkdown(needsRebaseTasks) {
|
|
269
273
|
const w = rebaseWarningModel(needsRebaseTasks);
|
|
270
274
|
if (!w) return null;
|
|
271
|
-
return `⚠️
|
|
275
|
+
return `⚠️ ${surface.strong('Rebase owed — your queue is GATED.')} ${w.count} ${w.noun} can't land and block new claims`
|
|
276
|
+
+ ` until you rebase + re-ship (or ${surface.code(surface.action('release'))}) ${w.pronoun}: ${w.refs}.`;
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
// The widget warning block (Tabler alert icon, danger-tinted), or '' when none.
|
|
@@ -302,6 +307,9 @@ const CARD_DIRECTIVE = {
|
|
|
302
307
|
markdown: '[otb-card] stdout is the finished /builder-start card (markdown). Output it verbatim as your reply, then stop — no rebuilt table, recommendation, nudge, or recap.',
|
|
303
308
|
};
|
|
304
309
|
function emitCardDirective(form) {
|
|
310
|
+
// Addressed to the model, not the reader. A terminal user has no card to relay and no
|
|
311
|
+
// show_widget to call, so this line is pure confusion there (task 1003680).
|
|
312
|
+
if (surface.isTerminal()) return;
|
|
305
313
|
process.stderr.write(CARD_DIRECTIVE[form] + '\n');
|
|
306
314
|
}
|
|
307
315
|
|
|
@@ -550,20 +558,20 @@ function isLocalOrigin(origin) {
|
|
|
550
558
|
// ships inside the PRIVATE @bongos/core package) even exists.
|
|
551
559
|
function instanceNotRunningCard({ origin, depsMissing } = {}) {
|
|
552
560
|
const out = [];
|
|
553
|
-
out.push(
|
|
561
|
+
out.push(surface.strong("builder-start — this instance isn't reachable yet"));
|
|
554
562
|
out.push('');
|
|
555
563
|
|
|
556
564
|
if (!isLocalOrigin(origin)) {
|
|
557
|
-
out.push(`I couldn't reach
|
|
565
|
+
out.push(`I couldn't reach ${surface.code(origin)}. That instance is ${surface.strong('hosted')} — it isn't this machine's server to start, so there's nothing to bring up here.`);
|
|
558
566
|
out.push('');
|
|
559
|
-
out.push(`1.
|
|
567
|
+
out.push(`1. ${surface.strong('The board is a website, and it needs nothing installed')} — open ${origin}/builders to see and claim work right now.`);
|
|
560
568
|
out.push(`2. If that doesn't load either, the instance itself is down: \`curl -sI ${origin}/healthz\` says whether it's answering.`);
|
|
561
|
-
out.push(`3. Once it answers, re-run
|
|
569
|
+
out.push(`3. Once it answers, re-run ${surface.code(surface.action('start'))}.`);
|
|
562
570
|
out.push('');
|
|
563
|
-
out.push(
|
|
571
|
+
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.`);
|
|
564
572
|
if (depsMissing) {
|
|
565
573
|
out.push('');
|
|
566
|
-
out.push(`(The
|
|
574
|
+
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 a ${surface.strong('private')} package, so installing it needs an ${surface.code('NPM_TOKEN')}; a 404 without one is expected, not a missing version.)`);
|
|
567
575
|
}
|
|
568
576
|
return out.join('\n');
|
|
569
577
|
}
|
|
@@ -575,7 +583,7 @@ function instanceNotRunningCard({ origin, depsMissing } = {}) {
|
|
|
575
583
|
out.push(`${n++}. \`npm install\` — installs the toolchain first (the \`bongos\` CLI ships inside \`@bongos/core\`, so nothing else runs until this does).`);
|
|
576
584
|
}
|
|
577
585
|
out.push(`${n++}. \`bongos dev\` — one command: starts Postgres, builds the tables, runs the server, and signs you in.`);
|
|
578
|
-
out.push(`${n++}. Re-run
|
|
586
|
+
out.push(`${n++}. Re-run ${surface.code(surface.action('start'))}.`);
|
|
579
587
|
out.push('');
|
|
580
588
|
out.push(`Meant to work on a *different* instance that's already running? Sign in to it with \`bongos login <url>\` first.`);
|
|
581
589
|
return out.join('\n');
|
|
@@ -800,7 +808,7 @@ async function main() {
|
|
|
800
808
|
? b.preferred_disciplines.join(' + ')
|
|
801
809
|
: 'all disciplines';
|
|
802
810
|
const credits = Number(b.total_credits || 0).toLocaleString();
|
|
803
|
-
console.log(
|
|
811
|
+
console.log(`${surface.strong('builder-start')} · ${b.github_login} (${rankLabel}) · ${credits} credits · ${discLabel}`);
|
|
804
812
|
const sk = me.data.streak;
|
|
805
813
|
if (sk && Number(sk.current) > 0) {
|
|
806
814
|
console.log(`🔥 ${sk.current}-day streak${Number(sk.longest) > Number(sk.current) ? ` · longest ${sk.longest}` : ''}`);
|
|
@@ -847,16 +855,18 @@ async function main() {
|
|
|
847
855
|
// not block — feedback_parallel_sessions). Ids are hall links (ref rule).
|
|
848
856
|
if (me.ok) {
|
|
849
857
|
const myClaims = me.data.active_claims || (me.data.active_claim ? [me.data.active_claim] : []);
|
|
850
|
-
const heldActions = (id) => ` → ship:
|
|
858
|
+
const heldActions = (id) => ` → ship: ${surface.code(surface.action('ship', id))}`
|
|
859
|
+
+ ` · release: ${surface.code(surface.action('release', id))}`
|
|
860
|
+
+ ` · open: ${surface.taskRef(id, HALL_TASK)}`;
|
|
851
861
|
if (myClaims.length === 1) {
|
|
852
862
|
const c = myClaims[0];
|
|
853
|
-
console.log(`You're holding
|
|
863
|
+
console.log(`You're holding ${surface.taskRef(c.task_id, HALL_TASK)} — ${clip(c.title, 56)}`);
|
|
854
864
|
console.log(heldActions(c.task_id));
|
|
855
865
|
console.log('');
|
|
856
866
|
} else if (myClaims.length > 1) {
|
|
857
867
|
console.log(`You're holding ${myClaims.length} claims (parallel sessions OK):`);
|
|
858
868
|
for (const c of myClaims) {
|
|
859
|
-
console.log(`
|
|
869
|
+
console.log(` ${surface.taskRef(c.task_id, HALL_TASK)} — ${clip(c.title, 50)}`);
|
|
860
870
|
console.log(heldActions(c.task_id));
|
|
861
871
|
}
|
|
862
872
|
console.log('');
|
|
@@ -869,7 +879,7 @@ async function main() {
|
|
|
869
879
|
console.log(
|
|
870
880
|
`🚦 ${s.versionId} looks ready to close ` +
|
|
871
881
|
`(${s.criteriaSatisfied}/${s.criteriaTotal} criteria, ${s.percentComplete}% weighted; ` +
|
|
872
|
-
`${s.tasksShipped} shipped, ${s.tasksCarryOver} carry-over). Run
|
|
882
|
+
`${s.tasksShipped} shipped, ${s.tasksCarryOver} carry-over). Run ${surface.action('version-close', s.versionId)}.`
|
|
873
883
|
);
|
|
874
884
|
console.log('');
|
|
875
885
|
}
|
|
@@ -882,7 +892,7 @@ async function main() {
|
|
|
882
892
|
fit && `fit=${fit}`,
|
|
883
893
|
].filter(Boolean);
|
|
884
894
|
const filterNote = filterBits.length ? ` (${filterBits.join(', ')})` : '';
|
|
885
|
-
console.log(
|
|
895
|
+
console.log(`${surface.strong('Claim one to start')} — ${filtered.length} ready for you right now${matched}${filterNote}:`);
|
|
886
896
|
console.log('');
|
|
887
897
|
if (claimable.length) {
|
|
888
898
|
const cardRows = claimable.slice(0, CARD_ROWS);
|
|
@@ -890,10 +900,13 @@ async function main() {
|
|
|
890
900
|
const moreN = filtered.length - cardRows.length;
|
|
891
901
|
if (moreN > 0) {
|
|
892
902
|
console.log('');
|
|
893
|
-
console.log(
|
|
903
|
+
console.log(surface.em(`…and ${moreN} more — say "show all", or run with --limit 30.`));
|
|
894
904
|
}
|
|
895
905
|
} else {
|
|
896
|
-
console.log(
|
|
906
|
+
console.log(surface.em(
|
|
907
|
+
"Nothing claimable right now — everything is in flight, blocked, or shipped."
|
|
908
|
+
+ (surface.isTerminal() ? '' : " Run /blocker-review to see what's stuck.")
|
|
909
|
+
));
|
|
897
910
|
}
|
|
898
911
|
|
|
899
912
|
// Cross-discipline pointer (one line) — only when preference-routed.
|
|
@@ -908,7 +921,7 @@ async function main() {
|
|
|
908
921
|
|
|
909
922
|
// ---------- next ----------
|
|
910
923
|
console.log('');
|
|
911
|
-
console.log('
|
|
924
|
+
console.log(`→ ${surface.isTerminal() ? `Claim one with ${surface.code('bongos claim <id>')} in a checkout` : `${surface.strong('Tell me a number')} to claim it`}.`);
|
|
912
925
|
if (full) {
|
|
913
926
|
console.log('');
|
|
914
927
|
console.log('Commands:');
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// scripts/gds/surface.js — who is reading this output: an agent, or a person in a terminal?
|
|
2
|
+
// (task 1003680, follow-up to ADR 0258.)
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS
|
|
5
|
+
// The CLI's cards were written for exactly one reader: a Claude Code session. They carry
|
|
6
|
+
// `[otb-card]` directives addressed to the model, `/builder-ship N` slash commands, and markdown
|
|
7
|
+
// link syntax — all correct in a chat feed, all wrong in a terminal. That stayed invisible while
|
|
8
|
+
// the CLI lived inside the private core, because the only way to run it was from a session.
|
|
9
|
+
// `@cloudbongos/cli` (ADR 0258) put the same output in front of newcomers on a bare laptop, where
|
|
10
|
+
// the first thing they read is an instruction written to somebody else and a command that does
|
|
11
|
+
// not exist.
|
|
12
|
+
//
|
|
13
|
+
// HOW THE SURFACE IS DECIDED — and why not isTTY
|
|
14
|
+
// AGENT is the default, so nothing about the Claude Code path changes unless something opts out.
|
|
15
|
+
// The opt-out is `BONGOS_SURFACE=terminal`, set by the `bongos` DISPATCHER (bin/bongos.js, and the
|
|
16
|
+
// generated one in the public package) when it spawns a verb. The skills and the card-delivery
|
|
17
|
+
// hook run `node scripts/gds/<verb>.js` DIRECTLY, never through the dispatcher, so they keep the
|
|
18
|
+
// agent shape byte-for-byte.
|
|
19
|
+
//
|
|
20
|
+
// `process.stdout.isTTY` would get this exactly backwards: a Claude Code session's stdout is not a
|
|
21
|
+
// TTY either, so TTY-sniffing would classify every agent run as a terminal and break the card
|
|
22
|
+
// contract the skills depend on. The signal has to be an explicit statement of intent, not a guess.
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const AGENT = 'agent';
|
|
27
|
+
const TERMINAL = 'terminal';
|
|
28
|
+
|
|
29
|
+
// Which reader is this output for? Pure: takes the env, returns a string.
|
|
30
|
+
function resolveSurface(env = process.env) {
|
|
31
|
+
const raw = String((env && env.BONGOS_SURFACE) || '').trim().toLowerCase();
|
|
32
|
+
return raw === TERMINAL ? TERMINAL : AGENT;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isTerminal(env = process.env) {
|
|
36
|
+
return resolveSurface(env) === TERMINAL;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A task reference. Agents get a hall link because a bare `#NNN` autolinks to GitHub issues and
|
|
40
|
+
// 404s (the CLAUDE.md ref rule); a terminal reader gets the plain id and the URL beside it, since
|
|
41
|
+
// `[#123](https://…)` is just noise on a console.
|
|
42
|
+
function taskRef(id, url, env = process.env) {
|
|
43
|
+
return isTerminal(env) ? `#${id}` : `[#${id}](${url}${id})`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 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.
|
|
48
|
+
function action(verb, arg, env = process.env) {
|
|
49
|
+
const tail = arg == null ? '' : ` ${arg}`;
|
|
50
|
+
return isTerminal(env) ? `bongos ${verb}${tail}` : `/builder-${verb}${tail}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Markdown emphasis is decoration in a feed and literal asterisks on a console.
|
|
54
|
+
function strong(s, env = process.env) {
|
|
55
|
+
return isTerminal(env) ? String(s) : `**${s}**`;
|
|
56
|
+
}
|
|
57
|
+
function em(s, env = process.env) {
|
|
58
|
+
return isTerminal(env) ? String(s) : `_${s}_`;
|
|
59
|
+
}
|
|
60
|
+
function code(s, env = process.env) {
|
|
61
|
+
return isTerminal(env) ? String(s) : `\`${s}\``;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A table. Agents get GitHub-flavoured markdown (the feed renders it); a terminal gets columns
|
|
65
|
+
// padded to width with a rule under the header, which is what a console can actually align.
|
|
66
|
+
function table(headers, rows, env = process.env) {
|
|
67
|
+
if (!isTerminal(env)) {
|
|
68
|
+
return [
|
|
69
|
+
`| ${headers.join(' | ')} |`,
|
|
70
|
+
`|${headers.map(() => '---').join('|')}|`,
|
|
71
|
+
...rows.map((r) => `| ${r.join(' | ')} |`),
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
const widths = headers.map((h, i) => Math.max(
|
|
75
|
+
String(h).length,
|
|
76
|
+
...rows.map((r) => String(r[i] == null ? '' : r[i]).length),
|
|
77
|
+
));
|
|
78
|
+
const line = (cells) => cells
|
|
79
|
+
.map((c, i) => String(c == null ? '' : c).padEnd(widths[i]))
|
|
80
|
+
.join(' ')
|
|
81
|
+
.replace(/\s+$/, '');
|
|
82
|
+
return [
|
|
83
|
+
line(headers),
|
|
84
|
+
widths.map((w) => '─'.repeat(w)).join(' '),
|
|
85
|
+
...rows.map(line),
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { AGENT, TERMINAL, resolveSurface, isTerminal, taskRef, action, strong, em, code, table };
|
|
@@ -394,9 +394,70 @@ async function safeSyncRankRole(builder) {
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
|
|
397
|
+
// readApplicantProfileFromHub(githubId) — the reviewer queue's LIVE read of one
|
|
398
|
+
// applicant's profile view from the hub (privacy spec D7, task 1002972). The
|
|
399
|
+
// project side of POST /sso/applicant-profile.
|
|
400
|
+
//
|
|
401
|
+
// WHY IT LIVES IN THE KERNEL AND NOT IN THE onboarding MODULE THAT USES IT. The
|
|
402
|
+
// call is authenticated with this instance's HUB CLIENT SECRET, and
|
|
403
|
+
// loadIdpConfig is deliberately not on the doorway — putting it there would hand
|
|
404
|
+
// every module the credential that speaks for this whole project. So the secret
|
|
405
|
+
// stays in core beside the two other hub calls that hold it
|
|
406
|
+
// (reportMembershipCheckIn, reportActivityRollup) and the module reaches a
|
|
407
|
+
// NARROW port instead: one github_id in, one already-bounded view out.
|
|
408
|
+
//
|
|
409
|
+
// RESOLVED FRESH ON EVERY CALL — no cache, no memo, no store. That is D7's
|
|
410
|
+
// "evaluated LIVE at review time, never a snapshot" (spec C2), and the reason a
|
|
411
|
+
// hub outage answers null rather than a stale-but-helpful last-known value: an
|
|
412
|
+
// applicant who hid their stats an hour ago must not have them rendered from
|
|
413
|
+
// this instance's memory. The hub already sends Cache-Control: no-store; this
|
|
414
|
+
// end simply keeps nothing.
|
|
415
|
+
//
|
|
416
|
+
// FAILS SOFT TO null, ALWAYS. A self-hosted instance with no hub (idp unset), a
|
|
417
|
+
// hub that is down, a 404 for an applicant this project has no echo for, a
|
|
418
|
+
// non-JSON body — every one of them is the same null, and the queue renders "no
|
|
419
|
+
// record" beside a row that still shows its login, note and vouch pill. A
|
|
420
|
+
// reviewer losing the profile block is a degraded queue; a queue that fails to
|
|
421
|
+
// load because the hub is slow is a broken one, and admission does not depend on
|
|
422
|
+
// any of this (ADR 0016).
|
|
423
|
+
//
|
|
424
|
+
// BOUNDED IN TIME, which is what makes the paragraph above true rather than
|
|
425
|
+
// aspirational (task 1003681, grader finding). This is the only hub call that
|
|
426
|
+
// FANS OUT: the other two fire at most once per sign-in or per ship, while this
|
|
427
|
+
// one runs up to MAX_LIVE_PROFILE_READS (25) times concurrently inside the
|
|
428
|
+
// Promise.all that GET /access-requests awaits. Unbounded, one unresponsive hub
|
|
429
|
+
// holds the whole reviewer queue open for as long as the socket lives — the
|
|
430
|
+
// exact "broken one" this comment disclaims. The timeout converts that into the
|
|
431
|
+
// null this function already promises on every other failure.
|
|
432
|
+
const APPLICANT_PROFILE_TIMEOUT_MS = 5000;
|
|
433
|
+
|
|
434
|
+
async function readApplicantProfileFromHub(githubId) {
|
|
435
|
+
const idp = loadIdpConfig();
|
|
436
|
+
if (!idp || githubId == null) return null;
|
|
437
|
+
try {
|
|
438
|
+
const r = await fetch(`${idp.origin}/api/gds/sso/applicant-profile`, {
|
|
439
|
+
method: 'POST',
|
|
440
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': userAgent() },
|
|
441
|
+
signal: AbortSignal.timeout(APPLICANT_PROFILE_TIMEOUT_MS),
|
|
442
|
+
body: JSON.stringify({
|
|
443
|
+
client_id: idp.clientId,
|
|
444
|
+
client_secret: idp.clientSecret,
|
|
445
|
+
github_id: githubId,
|
|
446
|
+
}),
|
|
447
|
+
});
|
|
448
|
+
if (!r.ok) return null;
|
|
449
|
+
const body = await r.json().catch(() => null);
|
|
450
|
+
return (body && body.profile) || null;
|
|
451
|
+
} catch (err) {
|
|
452
|
+
log.info('[auth] hub applicant-profile read failed (non-blocking):', err.message);
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
397
457
|
module.exports = {
|
|
398
458
|
firstAdminLogin,
|
|
399
459
|
firstTimeAdmissionBlocked,
|
|
460
|
+
readApplicantProfileFromHub,
|
|
400
461
|
// Re-exported, not defined here (task 1003579 moved it to project-door.js with
|
|
401
462
|
// the two knobs it composes against). auth.js, the doorway's api.joinabilityMode
|
|
402
463
|
// and four test files import it by this path, and the read-back it answers —
|
package/src/bongos/auth.js
CHANGED
|
@@ -55,7 +55,7 @@ const ic = require('../instance-config');
|
|
|
55
55
|
const { branding, userAgent } = require('../branding');
|
|
56
56
|
|
|
57
57
|
const { SCOPE, authConfigured, grantedScopeCoversRepos, idpConfigured, idpEnabled, loadIdpConfig, scopeForWebFlow } = require('./auth-config.js');
|
|
58
|
-
const { firstAdminLogin, firstTimeAdmissionBlocked, joinabilityMode, maybeSeatFirstAdmin, membershipKind, openEnrollmentEnabled, webAdmissionStatus } = require('./auth-admission.js');
|
|
58
|
+
const { firstAdminLogin, firstTimeAdmissionBlocked, joinabilityMode, maybeSeatFirstAdmin, membershipKind, openEnrollmentEnabled, readApplicantProfileFromHub, webAdmissionStatus } = require('./auth-admission.js');
|
|
59
59
|
const { buildAuthorizeUrl, completeWebFlow, pollDeviceFlow, startDeviceFlow } = require('./auth-github.js');
|
|
60
60
|
const { buildIdpAuthorizeUrl, completeDeviceExchange, completeIdpFlow, idpAuthorizeUrl, loadHubPublicKey, logoutTokenJtiSeenAndRecord, verifyHubAssertion, verifyHubLogoutToken } = require('./auth-idp.js');
|
|
61
61
|
const { addDiscordGuildMember, buildDiscordAuthorizeUrl, discordAuthorizeUrl, discordBotConfigured, discordConfigured, exchangeDiscordCode, fetchDiscordUser, isValidDiscordUserId, loadDiscordBotToken, loadDiscordCreds, removeDiscordGuildMember } = require('./auth-discord.js');
|
|
@@ -787,6 +787,7 @@ module.exports = {
|
|
|
787
787
|
idpConfigured,
|
|
788
788
|
openEnrollmentEnabled,
|
|
789
789
|
joinabilityMode,
|
|
790
|
+
readApplicantProfileFromHub,
|
|
790
791
|
firstTimeAdmissionBlocked,
|
|
791
792
|
webAdmissionStatus,
|
|
792
793
|
firstAdminLogin,
|
package/src/module-api.js
CHANGED
|
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
|
|
|
55
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
56
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.581'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
59
59
|
|
|
60
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
61
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -273,6 +273,16 @@ module.exports = {
|
|
|
273
273
|
// the other two had closed. Optional `{ publishable }` for a caller that
|
|
274
274
|
// holds a publish verdict — see src/bongos/project-door.js.
|
|
275
275
|
projectJoinDoor: (opts) => require('./bongos/project-door').effectiveJoinDoor(opts),
|
|
276
|
+
// readApplicantProfileFromHub(githubId) — the reviewer queue's LIVE per-render
|
|
277
|
+
// read of one applicant's hub profile view (privacy spec D7, task
|
|
278
|
+
// 1002972). Published as a NARROW port on purpose: the underlying call is
|
|
279
|
+
// authenticated with this instance's hub client secret, and loadIdpConfig
|
|
280
|
+
// stays off the doorway so no module ever holds the credential that speaks
|
|
281
|
+
// for the whole project. A module passes one github_id and gets one
|
|
282
|
+
// already-bounded view (or null) — it can neither widen the request nor
|
|
283
|
+
// re-address it. Fails soft to null on every error, so a hub outage costs
|
|
284
|
+
// the profile block and never the queue.
|
|
285
|
+
get readApplicantProfileFromHub() { return require('./bongos/auth').readApplicantProfileFromHub; },
|
|
276
286
|
// userAgent — the instance's HTTP User-Agent string (branding-derived). The
|
|
277
287
|
// carved lifecycle module's github-push.js stamps every GitHub API request with
|
|
278
288
|
// it instead of reaching into src/branding.js directly (BV1.R86).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// tests/adr_renumber_integrity.mjs — a renumbered ADR must move ALL of itself
|
|
2
|
+
// (task 1003681, from a grader finding).
|
|
3
|
+
//
|
|
4
|
+
// Renumbering an ADR is not one edit, it is four, and `fitness.js` only checks
|
|
5
|
+
// two of them (numbers are unique, and the index has one row per file). This
|
|
6
|
+
// pins the other two, plus the class of stale reference that caused the finding.
|
|
7
|
+
//
|
|
8
|
+
// THE INCIDENT. Task 1002972's branch authored ADR 0241 while main landed a
|
|
9
|
+
// DIFFERENT 0241; the collision failed fitness and stranded the PR for four
|
|
10
|
+
// days. Renumbering the never-landed one to 0260 moved its filename, its
|
|
11
|
+
// heading and its index row's link — but left its own session log still saying
|
|
12
|
+
// "ADR 0241" in prose, twice. Because 0241 is a REAL, DIFFERENT, ALREADY-LANDED
|
|
13
|
+
// ADR, that is worse than a dangling link: a reader follows it and arrives,
|
|
14
|
+
// with no error, at the wrong decision. `docs-entropy.js` cannot see this — it
|
|
15
|
+
// resolves LINKS, and these were bare prose references to a number that does
|
|
16
|
+
// resolve, just to something else.
|
|
17
|
+
//
|
|
18
|
+
// What each check would have caught:
|
|
19
|
+
// • heading vs filename — a rename that forgot the `# ADR NNNN` line
|
|
20
|
+
// • index row number vs its own link target — a row renumbered IN PLACE
|
|
21
|
+
// (which is also how the row ended up sorted between 0240 and 0241)
|
|
22
|
+
// • the session log's references — the finding itself, as a regression pin
|
|
23
|
+
//
|
|
24
|
+
// Run: node --test --test-reporter=tap tests/adr_renumber_integrity.mjs
|
|
25
|
+
|
|
26
|
+
import assert from 'node:assert/strict';
|
|
27
|
+
import { test } from 'node:test';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const ADR_DIR = path.join(ROOT, 'docs', 'adr');
|
|
34
|
+
|
|
35
|
+
const adrFiles = fs
|
|
36
|
+
.readdirSync(ADR_DIR)
|
|
37
|
+
.filter((f) => f.endsWith('.md') && /^\d{4}-/.test(f))
|
|
38
|
+
.sort();
|
|
39
|
+
|
|
40
|
+
test('every ADR file’s heading carries its own number — a rename must move the heading too', () => {
|
|
41
|
+
assert.ok(adrFiles.length > 200, 'the ADR corpus should be substantial; a tiny count means the glob broke');
|
|
42
|
+
const wrong = [];
|
|
43
|
+
for (const f of adrFiles) {
|
|
44
|
+
const fileNum = /^(\d{4})-/.exec(f)[1];
|
|
45
|
+
const first = fs.readFileSync(path.join(ADR_DIR, f), 'utf8').split(/\r?\n/)[0];
|
|
46
|
+
// both conventions ship in this corpus: "# ADR 0259 — …" and "# 0001 — …"
|
|
47
|
+
const heading = /^#\s*(?:ADR\s*)?(\d{4})\b/.exec(first);
|
|
48
|
+
if (!heading) { wrong.push(`${f} :: heading carries no number (${first.slice(0, 60)})`); continue; }
|
|
49
|
+
if (heading[1] !== fileNum) wrong.push(`${f} :: heading says ${heading[1]}`);
|
|
50
|
+
}
|
|
51
|
+
assert.deepEqual(wrong, [], 'an ADR whose heading and filename disagree is a half-finished renumber');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('every index row’s number matches the file it links — a row renumbered in place is caught here', () => {
|
|
55
|
+
const readme = fs.readFileSync(path.join(ADR_DIR, 'README.md'), 'utf8');
|
|
56
|
+
const rows = readme.split(/\r?\n/).filter((l) => /^\|\s*\d{4}\s*\|/.test(l));
|
|
57
|
+
assert.ok(rows.length > 200, 'the index should carry a row per ADR');
|
|
58
|
+
const wrong = [];
|
|
59
|
+
for (const row of rows) {
|
|
60
|
+
const rowNum = /^\|\s*(\d{4})\s*\|/.exec(row)[1];
|
|
61
|
+
// A row's summary CITES many other ADRs, so the first `](NNNN-….md)` is
|
|
62
|
+
// usually a citation. The row's OWN target is the one closing its
|
|
63
|
+
// `[**Title**](…)` cell — i.e. the last link before the trailing category
|
|
64
|
+
// cell. Matching the first one instead reports ~60 false mismatches.
|
|
65
|
+
const link = /\]\((\d{4})-[^)]*\.md\)\s*\|[^|]*\|\s*$/.exec(row);
|
|
66
|
+
if (!link) continue; // a handful of legacy rows link elsewhere or not at all
|
|
67
|
+
if (link[1] !== rowNum) wrong.push(`row ${rowNum} links ${link[1]}-…`);
|
|
68
|
+
}
|
|
69
|
+
assert.deepEqual(wrong, [], 'a row whose number and link disagree sends every citation to the wrong ADR');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the ADR that was renumbered out of a collision leaves no reference to its old number (task 1003681)', () => {
|
|
73
|
+
// The regression itself. 0241 is a real, different, landed ADR — so a stale
|
|
74
|
+
// reference here resolves silently to the wrong decision rather than 404ing,
|
|
75
|
+
// which is exactly why no link checker flagged it.
|
|
76
|
+
const log = path.join(ROOT, 'docs', 'session-logs', '2026-09-03-goal-1000045-applicant-profile-d7.md');
|
|
77
|
+
const src = fs.readFileSync(log, 'utf8');
|
|
78
|
+
assert.doesNotMatch(src, /ADR 0241\b/, 'this task’s ADR moved to 0260; 0241 now names the artist-gate decision');
|
|
79
|
+
assert.match(src, /ADR 0260\b/, 'the log must cite the decision it actually produced');
|
|
80
|
+
assert.ok(
|
|
81
|
+
fs.existsSync(path.join(ADR_DIR, '0260-the-application-is-the-consent-and-the-echo-is-the-gate.md')),
|
|
82
|
+
'and that ADR must exist under the number the log now cites',
|
|
83
|
+
);
|
|
84
|
+
});
|