@bongos/core 1.19.388 → 1.19.390
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 +40 -25
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/grader-health/SKILL.md +1 -1
- package/docs/copy-inventory.md +16 -15
- package/docs/copy-registry.json +25 -16
- package/docs/file-map.md +1 -0
- package/modules/grading/grader-config.js +5 -3
- package/modules/grading/grader-rubric.json +2 -2
- package/modules/grading/grader-score.js +86 -33
- package/modules/grading/grader-workers/index.js +19 -0
- package/modules/hall-ui/public/builders.js +8 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship-grade-steps.js +27 -0
- package/scripts/hall-preview/README.md +34 -1
- package/scripts/hall-preview/live-identity.js +121 -0
- package/scripts/hall-preview/refresh-fixtures.js +1 -0
- package/scripts/hall-preview/server.js +74 -10
- package/src/module-api.js +5 -1
- package/tests/grader.mjs +7 -6
- package/tests/grader_quality_window_overflow.mjs +324 -0
- package/tests/hall_preview_identity.mjs +163 -0
|
@@ -19,18 +19,23 @@ function isNum(n) {
|
|
|
19
19
|
return Number.isFinite(n);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// #1002653 corroboration helpers for validateAndScore's null-
|
|
23
|
-
//
|
|
24
|
-
// on an errored worker is the normal
|
|
25
|
-
// NON-ARRAY issues is malformed and
|
|
26
|
-
// schema_error downstream), never
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
// #1002653 / task 1003318 corroboration helpers for validateAndScore's null-axis
|
|
23
|
+
// lens branches (Narc → ff, Quality → cq). A worker entry is untrusted input —
|
|
24
|
+
// w.issues may be any shape. Absent issues on an errored worker is the normal
|
|
25
|
+
// outage shape (treated as none); a truthy NON-ARRAY issues is malformed and
|
|
26
|
+
// must DISQUALIFY corroboration (fail-closed schema_error downstream), never
|
|
27
|
+
// TypeError into a 500 on grade submission.
|
|
28
|
+
const isWorkerErroredClean = (kind) => (w) => {
|
|
29
|
+
if (!w || w.kind !== kind || !w.error) return false;
|
|
29
30
|
const issues = w.issues == null ? [] : w.issues;
|
|
30
31
|
if (!Array.isArray(issues)) return false;
|
|
31
32
|
return !issues.some((i) => i && (i.severity === 'blocker' || i.severity === 'major'));
|
|
32
33
|
};
|
|
33
|
-
const
|
|
34
|
+
const isWorkerHealthy = (kind) => (w) => !!w && w.kind === kind && !w.error;
|
|
35
|
+
const isNarcErroredClean = isWorkerErroredClean('narc');
|
|
36
|
+
const isNarcHealthy = isWorkerHealthy('narc');
|
|
37
|
+
const isQualityErroredClean = isWorkerErroredClean('quality');
|
|
38
|
+
const isQualityHealthy = isWorkerHealthy('quality');
|
|
34
39
|
|
|
35
40
|
// task 1002658 (ADR 0164): a SUBSTANTIVE issue is one a real panel emits — a
|
|
36
41
|
// known severity + a non-empty summary. The old plausibility guard counted raw
|
|
@@ -95,8 +100,8 @@ function validateAndScore(rawSubagent, meta = {}) {
|
|
|
95
100
|
// needs POSITIVE corroboration: workers[] carries an errored
|
|
96
101
|
// no-substantive-finding Narc (#473 missing-signal semantics) and no healthy
|
|
97
102
|
// Narc. Empty/narc-less/absent workers, ABSENT ff, and every other shape
|
|
98
|
-
// fall through to the fail-closed schema_error below. Quality
|
|
99
|
-
//
|
|
103
|
+
// fall through to the fail-closed schema_error below. Quality's twin branch
|
|
104
|
+
// (task 1003318, below) is gating, so it never passes.
|
|
100
105
|
// Trust note: workers[] is client-supplied like every SR-9 grade input — a
|
|
101
106
|
// forger gains nothing here they cannot get by sending ff:9 directly; the
|
|
102
107
|
// server-derivable-signals redesign is tracked separately (SR-9 residual).
|
|
@@ -105,8 +110,23 @@ function validateAndScore(rawSubagent, meta = {}) {
|
|
|
105
110
|
metaWorkers.some(isNarcErroredClean) && !metaWorkers.some(isNarcHealthy);
|
|
106
111
|
const ffExplicitNull = parsed.functional_fidelity === null;
|
|
107
112
|
const ffLensUnavailable = ffExplicitNull && narcMissingSignal;
|
|
113
|
+
// task 1003318: the same shape for the GATING Quality lens. The haiku-tier
|
|
114
|
+
// Quality worker died on a context-window overflow (`Prompt is too long`)
|
|
115
|
+
// before it started; aggregatePanel forwarded code_quality: null and
|
|
116
|
+
// Number(null) === 0 fabricated "FAIL — code_quality below 6" at (9+0)/2.
|
|
117
|
+
// Unlike the advisory ff branch this NEVER passes: a gating lens that did not
|
|
118
|
+
// run is recorded as unavailable (json-null axis, score on the axis that ran,
|
|
119
|
+
// signals.cq_lens_unavailable) and the task parks at completed for
|
|
120
|
+
// manual-confirm — fail-closed, and a forger gains nothing they could not get
|
|
121
|
+
// by sending cq:9 outright. Same corroboration: an errored no-finding Quality
|
|
122
|
+
// in workers[] and no healthy one; every other null shape is schema_error.
|
|
123
|
+
const qualityMissingSignal = !!metaWorkers &&
|
|
124
|
+
metaWorkers.some(isQualityErroredClean) && !metaWorkers.some(isQualityHealthy);
|
|
125
|
+
const cqExplicitNull = parsed.code_quality === null;
|
|
126
|
+
const cqLensUnavailable = cqExplicitNull && qualityMissingSignal;
|
|
127
|
+
const qualityErrorTag = cqLensUnavailable ? String(metaWorkers.find(isQualityErroredClean).error) : null;
|
|
108
128
|
const ff = ffExplicitNull ? null : Number(parsed.functional_fidelity);
|
|
109
|
-
const cq = Number(parsed.code_quality);
|
|
129
|
+
const cq = cqExplicitNull ? null : Number(parsed.code_quality);
|
|
110
130
|
const economyRaw = Number(parsed.economy);
|
|
111
131
|
const economy =
|
|
112
132
|
isNum(economyRaw) && economyRaw >= 0 && economyRaw <= 10 ? economyRaw : null;
|
|
@@ -138,9 +158,13 @@ function validateAndScore(rawSubagent, meta = {}) {
|
|
|
138
158
|
|
|
139
159
|
signals.confidence = confidence;
|
|
140
160
|
signals.issue_count = issues.length;
|
|
141
|
-
|
|
161
|
+
// Set AND cleared: `signals` spreads the client's own blob, so a forged
|
|
162
|
+
// `*_lens_unavailable: true` on a judged grade must not survive into the row
|
|
163
|
+
// (the ship card keys its outage reading on these).
|
|
164
|
+
if (ffLensUnavailable) signals.ff_lens_unavailable = true; else delete signals.ff_lens_unavailable;
|
|
165
|
+
if (cqLensUnavailable) signals.cq_lens_unavailable = true; else delete signals.cq_lens_unavailable;
|
|
142
166
|
|
|
143
|
-
if ((!ffLensUnavailable && (!isNum(ff) || ff < 0 || ff > 10)) || !isNum(cq) || cq < 0 || cq > 10) {
|
|
167
|
+
if ((!ffLensUnavailable && (!isNum(ff) || ff < 0 || ff > 10)) || (!cqLensUnavailable && (!isNum(cq) || cq < 0 || cq > 10))) {
|
|
144
168
|
return {
|
|
145
169
|
score: 0,
|
|
146
170
|
passed: false,
|
|
@@ -152,22 +176,34 @@ function validateAndScore(rawSubagent, meta = {}) {
|
|
|
152
176
|
notes_md: `Subagent grader ${VERSION}: subagent returned scores outside [0,10] or non-numeric. Got functional_fidelity=${parsed.functional_fidelity}, code_quality=${parsed.code_quality}.`
|
|
153
177
|
+ (ffExplicitNull && !narcMissingSignal
|
|
154
178
|
? ' Explicit null functional_fidelity without a corroborated Narc outage in signals.workers is fail-closed.'
|
|
179
|
+
: '')
|
|
180
|
+
+ (cqExplicitNull && !qualityMissingSignal
|
|
181
|
+
? ' Explicit null code_quality without a corroborated Quality outage in signals.workers is fail-closed.'
|
|
155
182
|
: ''),
|
|
156
183
|
issues,
|
|
157
184
|
};
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
const ffR = ffLensUnavailable ? null : r1(clamp(ff, 0, 10));
|
|
161
|
-
const cqR = r1(clamp(cq, 0, 10));
|
|
188
|
+
const cqR = cqLensUnavailable ? null : r1(clamp(cq, 0, 10));
|
|
162
189
|
const economyR = economy != null ? r1(clamp(economy, 0, 10)) : null;
|
|
163
190
|
const securityR = securityScore != null ? r1(clamp(securityScore, 0, 10)) : null;
|
|
164
191
|
const efficiencyR = efficiencyScore != null ? r1(clamp(efficiencyScore, 0, 10)) : null;
|
|
165
192
|
const economyPenalty =
|
|
166
193
|
economyR != null ? Math.max(0, (THRESHOLD - economyR) / 2) : 0;
|
|
167
|
-
const cqEffective =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
194
|
+
const cqEffective = cqLensUnavailable
|
|
195
|
+
? null
|
|
196
|
+
: economyR != null ? r1(clamp(cqR - economyPenalty, 0, 10)) : cqR;
|
|
197
|
+
// With a lens out, the score reflects only the axis that ran — never a blend
|
|
198
|
+
// with a synthetic 0 (the fabricated 4.5/10) or a synthetic threshold. Both
|
|
199
|
+
// out → 0: nothing was judged.
|
|
200
|
+
const blended = ffLensUnavailable && cqLensUnavailable
|
|
201
|
+
? 0
|
|
202
|
+
: ffLensUnavailable
|
|
203
|
+
? cqR
|
|
204
|
+
: cqLensUnavailable
|
|
205
|
+
? ffR
|
|
206
|
+
: r1((ffR + cqR) / 2);
|
|
171
207
|
|
|
172
208
|
// confidence must be present (not null) for a PASS. A subagent that omits
|
|
173
209
|
// confidence entirely silently bypassed the low-confidence gate under the
|
|
@@ -182,26 +218,40 @@ function validateAndScore(rawSubagent, meta = {}) {
|
|
|
182
218
|
// (null < THRESHOLD is true via JS coercion), and must not appear in the
|
|
183
219
|
// verdict's below-threshold branches.
|
|
184
220
|
const ffBelow = !ffLensUnavailable && ffR < THRESHOLD;
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
221
|
+
const cqBelow = !cqLensUnavailable && cqR < THRESHOLD;
|
|
222
|
+
const bothPass = !ffBelow && !cqBelow;
|
|
223
|
+
// A GATING lens that did not run is never a PASS (ff is advisory since #481;
|
|
224
|
+
// cq is not) — manual-confirm is the only way past it (task 1003318).
|
|
225
|
+
const passed = bothPass && !lowConfidence && !confidenceAbsent && !cqLensUnavailable;
|
|
226
|
+
|
|
227
|
+
// The UNAVAILABLE reading is only honest when the lenses that DID run found
|
|
228
|
+
// nothing substantive: a Hacker blocker beside a dead Quality is a rejection
|
|
229
|
+
// with a missing axis, not an outage, and must keep the ordinary FAIL chain
|
|
230
|
+
// (the #574 posture — a dark lens hardens the outcome, never softens it).
|
|
231
|
+
const hasSubstantiveFinding = issues.some((i) => i.severity === 'blocker' || i.severity === 'major');
|
|
188
232
|
const verdict = passed
|
|
189
233
|
? 'PASS'
|
|
190
|
-
:
|
|
191
|
-
?
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
234
|
+
: cqLensUnavailable && !hasSubstantiveFinding
|
|
235
|
+
? `UNAVAILABLE — the code_quality lens did not run (Quality worker errored: ${qualityErrorTag}); not a quality rejection — manual-confirm available`
|
|
236
|
+
+ (ffBelow ? ` (functional_fidelity below ${THRESHOLD})` : '')
|
|
237
|
+
// the confidence reasons the chain below would have named — kept as a
|
|
238
|
+
// tail so the #489 gate-walk signal is never hidden by the outage line
|
|
239
|
+
+ (confidenceAbsent ? '; confidence absent' : lowConfidence ? `; low confidence ${confidence}` : '')
|
|
240
|
+
: confidenceAbsent
|
|
241
|
+
? 'FAIL — confidence absent'
|
|
242
|
+
: lowConfidence
|
|
243
|
+
? 'FAIL — low confidence'
|
|
244
|
+
: bothPass
|
|
245
|
+
? 'FAIL'
|
|
246
|
+
: ffBelow && cqBelow
|
|
247
|
+
? `FAIL — both axes below ${THRESHOLD}`
|
|
248
|
+
: ffBelow
|
|
249
|
+
? `FAIL — functional_fidelity below ${THRESHOLD}`
|
|
250
|
+
: `FAIL — code_quality below ${THRESHOLD}`;
|
|
201
251
|
|
|
202
252
|
const notesLines = [];
|
|
203
253
|
notesLines.push(`Shot ${signals.shot_index != null ? signals.shot_index : '?'} (${signals.model}) — ${verdict}`);
|
|
204
|
-
notesLines.push(` ff=${ffLensUnavailable ? 'n/a (lens unavailable)' : `${ffR}/10`} cq=${cqR}/10${economyR != null ? ` econ=${economyR}/10 (advisory)` : ''} conf=${confidence ?? '?'}`);
|
|
254
|
+
notesLines.push(` ff=${ffLensUnavailable ? 'n/a (lens unavailable)' : `${ffR}/10`} cq=${cqLensUnavailable ? 'n/a (lens unavailable)' : `${cqR}/10`}${economyR != null ? ` econ=${economyR}/10 (advisory)` : ''} conf=${confidence ?? '?'}`);
|
|
205
255
|
if (notesFromSubagent) {
|
|
206
256
|
notesLines.push('');
|
|
207
257
|
notesLines.push(notesFromSubagent.trim());
|
|
@@ -559,6 +609,9 @@ function aggregateShots(shotResults, meta = {}) {
|
|
|
559
609
|
const cq = median(verdicts.map((v) => v.cq));
|
|
560
610
|
const economy = median(verdicts.map((v) => v.economy));
|
|
561
611
|
const cqEffective = median(verdicts.map((v) => v.cq_effective));
|
|
612
|
+
// NB (task 1003318): this v3 multi-shot path still collapses a null axis to
|
|
613
|
+
// 0 — it serves the trivial-diff single shot, which cannot overflow a window;
|
|
614
|
+
// the panel path's lens-outage handling lives in validateAndScore.
|
|
562
615
|
const blended = ff != null && cq != null ? r1((ff + cq) / 2) : 0;
|
|
563
616
|
// Top-level aggregated confidence = median of per-shot confidences. Without
|
|
564
617
|
// this, ship.js forwards a null top-level confidence to /tasks/:id/grade,
|
|
@@ -45,6 +45,17 @@ const TRANSIENT_WORKER_ERRORS = new Set([
|
|
|
45
45
|
'SUBAGENT_NONZERO_EXIT',
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
|
+
// task 1003318: a context-window overflow is NOT transient. The CLI refuses the
|
|
49
|
+
// request before the worker starts (`Prompt is too long: the request is ~251248
|
|
50
|
+
// tokens (limit 200000)`) and refuses the retry identically, so the #465 retry
|
|
51
|
+
// only burns another round. Its own tag names the cause on the grade row and
|
|
52
|
+
// in the server's verdict line (grader-score.js keys the lens outage on ANY
|
|
53
|
+
// errored no-finding Quality — the tag is the diagnosis, not the trigger).
|
|
54
|
+
// The pattern is the API's exact sentence, so a diff that merely mentions the
|
|
55
|
+
// phrase cannot trip it.
|
|
56
|
+
const PROMPT_TOO_LONG_RE = /prompt is too long: the request is ~?\d+ tokens/i;
|
|
57
|
+
const PROMPT_TOO_LONG = 'PROMPT_TOO_LONG';
|
|
58
|
+
|
|
48
59
|
// #467: per-worker subagent timeout. The global 240s applied to EVERY worker,
|
|
49
60
|
// which timed out the Read-heavy ones (Narc walks ACs + reads files — hence
|
|
50
61
|
// Phase 2.0.5's 5-read workaround; Quality is next-heaviest). Per-worker budgets
|
|
@@ -176,6 +187,14 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
176
187
|
// (`claude` prints its "not logged in" line there) — fall back so the tail
|
|
177
188
|
// is never empty when the subprocess did explain itself.
|
|
178
189
|
stderrTail = redactStderrTail(err.stderr) ?? redactStderrTail(err.stdout);
|
|
190
|
+
// Matched on the RAW stream (the redacted tail keeps only the last 400
|
|
191
|
+
// chars), stderr first and stdout only when stderr is silent — stdout
|
|
192
|
+
// carries the model's own text. A SUBAGENT_TIMEOUT keeps its tag: it is
|
|
193
|
+
// the #467 budget signal, never a refusal.
|
|
194
|
+
if (err.code !== 'SUBAGENT_TIMEOUT') {
|
|
195
|
+
const said = String(err.stderr || '').trim() || String(err.stdout || '');
|
|
196
|
+
if (PROMPT_TOO_LONG_RE.test(said)) subprocessError = PROMPT_TOO_LONG;
|
|
197
|
+
}
|
|
179
198
|
}
|
|
180
199
|
const parsed = subprocessError
|
|
181
200
|
? null
|
|
@@ -550,17 +550,22 @@
|
|
|
550
550
|
// A grader OUTAGE row (panel_outcome 'unavailable') carries a placeholder
|
|
551
551
|
// score of 0 — label it, never render it as a 0.0 fail (task 1002652).
|
|
552
552
|
const isOutage = g.panel_outcome === 'unavailable';
|
|
553
|
-
|
|
553
|
+
// A json-null gating axis (task 1003318 — the Quality worker never ran)
|
|
554
|
+
// is likewise not a judgment: the score is the surviving axis, not a fail.
|
|
555
|
+
const isLensOut = !isOutage && !!g.rubric_scores && g.rubric_scores.code_quality === null;
|
|
556
|
+
const score = isOutage || isLensOut ? '—' : Number(g.score).toFixed(1);
|
|
554
557
|
const verdict = isOutage
|
|
555
558
|
? `<span style="color:var(--ink-soft,#8a8577)">not graded (grader outage)</span>`
|
|
556
|
-
:
|
|
559
|
+
: isLensOut
|
|
560
|
+
? `<span style="color:var(--ink-soft,#8a8577)">not graded (quality lens unavailable)</span>`
|
|
561
|
+
: g.passed
|
|
557
562
|
? `<span style="color:var(--olive,#5b7a4d);font-weight:600">passed</span>`
|
|
558
563
|
: `<span style="color:#a04040;font-weight:600">fell short</span>`;
|
|
559
564
|
return `
|
|
560
565
|
<tr>
|
|
561
566
|
<td class="ledger__num">#${g.task_id}</td>
|
|
562
567
|
<td>${escapeHtml(g.title || '')}</td>
|
|
563
|
-
<td class="ledger__drachmae">${isOutage ? '—' : `${score} / 10`}</td>
|
|
568
|
+
<td class="ledger__drachmae">${isOutage || isLensOut ? '—' : `${score} / 10`}</td>
|
|
564
569
|
<td>${verdict}</td>
|
|
565
570
|
<td class="ledger__date">${fmtDate(g.graded_at)}</td>
|
|
566
571
|
</tr>
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.390",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.390",
|
|
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.19.
|
|
3
|
+
"version": "1.19.390",
|
|
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",
|
|
@@ -306,6 +306,33 @@ function formatGradeOutcome(g, taskId, cliFinding = null) {
|
|
|
306
306
|
return out.join('\n');
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// task 1003318: ONE gating lens went dark (the Quality worker died on a
|
|
310
|
+
// context-window overflow before it started) while the rest of the panel
|
|
311
|
+
// judged the diff and found nothing substantive. The server records that axis
|
|
312
|
+
// as unavailable instead of fabricating a 0 (validateAndScore
|
|
313
|
+
// cq_lens_unavailable); present it the way #943 presents a total outage —
|
|
314
|
+
// there are no findings to fix, and the paths forward are re-grade or
|
|
315
|
+
// manual-confirm. Sits BELOW the authorization wall (a refusal is never an
|
|
316
|
+
// outage) and steps aside when any lens raised a blocker/major (then the
|
|
317
|
+
// ordinary FAIL block prints the findings). The task stays at completed.
|
|
318
|
+
const cqLensOutage = !g.passed
|
|
319
|
+
&& g.signals?.cq_lens_unavailable === true
|
|
320
|
+
&& g.rubric_scores?.code_quality === null
|
|
321
|
+
&& !(g.issues || []).some((i) => i && (i.severity === 'blocker' || i.severity === 'major'));
|
|
322
|
+
if (cqLensOutage) {
|
|
323
|
+
out.push('');
|
|
324
|
+
out.push('Subagent grade: QUALITY LENS UNAVAILABLE · the Quality worker did not run — NOT a quality rejection');
|
|
325
|
+
if (g.notes_md) out.push(g.notes_md.split('\n').map((l) => ' ' + l).join('\n'));
|
|
326
|
+
out.push(...formatOutageDiagnostic(g, null));
|
|
327
|
+
out.push('');
|
|
328
|
+
out.push('The other lenses judged the diff and found nothing substantive; code_quality is recorded as unavailable, not 0. Task stays at completed.');
|
|
329
|
+
out.push('When the grader is healthy again, re-grade:');
|
|
330
|
+
out.push(` node scripts/gds/ship.js ${taskId} --regrade`);
|
|
331
|
+
out.push('Or land it now via manual-confirm:');
|
|
332
|
+
out.push(` curl -X POST -H "Authorization: Bearer <token>" ${resolveInstanceBase() || '<instance-origin>'}/api/gds/tasks/${taskId}/confirm`);
|
|
333
|
+
return out.join('\n');
|
|
334
|
+
}
|
|
335
|
+
|
|
309
336
|
const verdict = g.passed ? 'PASS' : 'FAIL';
|
|
310
337
|
out.push('');
|
|
311
338
|
out.push(`Subagent grade: ${verdict} · blended ${g.score}/10 · threshold ${g.threshold} (both axes)`);
|
|
@@ -21,11 +21,43 @@ node scripts/hall-preview/server.js # neutral pack, port 4600
|
|
|
21
21
|
| `--brand config/branding.json` | serve the instance pack (default: `config/branding.neutral.json`, the vanilla skin) |
|
|
22
22
|
| `--modules-off dev-box,provisioning` | report those modules disabled in `window.__MODULES__` — verifies `moduleOn()` gating (Harbor nav item, Instances/Start-a-project widgets must vanish) |
|
|
23
23
|
| `--empty` | drain every array in every fixture — the empty-state screenshot pass |
|
|
24
|
+
| `--fixture-me` | don't read your own account live — serve the checked-in `me.json` snapshot (see below) |
|
|
24
25
|
|
|
25
26
|
Writes are not wired: any `POST/PATCH/DELETE` answers `501 preview_readonly`.
|
|
26
27
|
A GET with no fixture answers 404 and logs
|
|
27
28
|
`no fixture: <path>` — that log line names the exact file to add.
|
|
28
29
|
|
|
30
|
+
## The account is yours (task 1003315)
|
|
31
|
+
|
|
32
|
+
The fixtures are one particular builder's snapshot, so the harness used to greet
|
|
33
|
+
everyone as that builder — their name, credits, achievements, goals and **rank**.
|
|
34
|
+
A metic snapshot also sealed the Archon-gated pages, so a UI change to Watch /
|
|
35
|
+
Harbor / Gate / Permissions could not be reviewed at all.
|
|
36
|
+
|
|
37
|
+
So at boot the harness reads **your** account: if this machine holds a GDS CLI
|
|
38
|
+
session, the me-scoped endpoints (`/me`, `/me/*`, `/builders/<you>/…`,
|
|
39
|
+
`/analytics/builder/<you>`, `/inbox/by-builder/<you>`, `/inbox/awaiting-nod`) are
|
|
40
|
+
fetched live — read-only GETs — and layered over the fixtures in memory. The boot
|
|
41
|
+
banner names whose hall you are looking at:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
account: @scootermack (archon) — your own, live
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Nothing is written to `fixtures/` (that's `refresh-fixtures.js`'s job), so the
|
|
48
|
+
checked-in snapshot stays the fallback. It serves whenever the live read can't
|
|
49
|
+
happen — signed out, expired session, offline, or an endpoint your rank can't
|
|
50
|
+
call (that one keeps its own fixture and the rest of your identity still lands).
|
|
51
|
+
Every fallback says why, and says the hall is about to show you someone else:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
account: @jaxri (metic) — NOT YOURS, the checked-in fixture: this machine has no Bongos CLI session — run /builder-setup
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**`--fixture-me` pins the snapshot.** Use it for the screenshot matrix below —
|
|
58
|
+
a live account makes the images vary run to run — and to review the signed-out
|
|
59
|
+
fallback deliberately. `--rank <rank>` still overrides whichever `/me` is served.
|
|
60
|
+
|
|
29
61
|
## Fixtures
|
|
30
62
|
|
|
31
63
|
`fixtures/<path>.json`, slashes as `__` (`/me/recent-ships` → `me__recent-ships.json`).
|
|
@@ -46,7 +78,8 @@ Box/terminal endpoints are deliberately never snapshotted (connection details).
|
|
|
46
78
|
|
|
47
79
|
Screenshot every changed page at **{light, dark} × {1280w, 390w}**, plus one
|
|
48
80
|
instance-pack run (`--brand config/branding.json`) proving the theming contract
|
|
49
|
-
survives.
|
|
81
|
+
survives. Run the harness with **`--fixture-me`** for these — the pinned snapshot
|
|
82
|
+
is what makes two runs comparable. With the Playwright MCP (`npx playwright install chrome` once):
|
|
50
83
|
|
|
51
84
|
1. `browser_navigate` → the page. `browser_console_messages` must show **0 errors**.
|
|
52
85
|
2. 1280w: `browser_resize 1280×900` → `browser_take_screenshot` (fullPage for tall pages).
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scripts/hall-preview/live-identity.js — serve the SIGNED-IN builder's own
|
|
3
|
+
// account in the hall preview harness (task 1003315).
|
|
4
|
+
//
|
|
5
|
+
// The harness answers every read from checked-in fixtures, and fixtures/me.json
|
|
6
|
+
// is one particular builder's snapshot. So every preview greeted whoever ran it
|
|
7
|
+
// as somebody else ("Welcome back, Jackson Rini"), with that builder's credits,
|
|
8
|
+
// achievements, goals and RANK — and because that snapshot is a metic, the
|
|
9
|
+
// Archon-gated pages rendered their sealed notice, so a UI change to them could
|
|
10
|
+
// not be reviewed at all. Reviewing your own UI change against someone else's
|
|
11
|
+
// account is the bug.
|
|
12
|
+
//
|
|
13
|
+
// The fix: at boot, when the machine has a GDS CLI session, the me-scoped reads
|
|
14
|
+
// are fetched LIVE (read-only GETs) and held in memory as overrides layered on
|
|
15
|
+
// top of the fixtures. Nothing is ever written to fixtures/ — the checked-in
|
|
16
|
+
// snapshot stays the signed-out fallback and the deterministic pass for the
|
|
17
|
+
// screenshot matrix (`--fixture-me` pins it).
|
|
18
|
+
//
|
|
19
|
+
// Every dependency is injected, so this file is testable with no disk, no
|
|
20
|
+
// network and no session (tests/hall_preview_identity.mjs).
|
|
21
|
+
|
|
22
|
+
// The me-scoped reads — everything the hall renders as "you". ':id' expands to
|
|
23
|
+
// the signed-in builder's id. Box/terminal endpoints are deliberately absent,
|
|
24
|
+
// the same rule refresh-fixtures.js follows: they carry connection details.
|
|
25
|
+
const ME_SCOPED_PATHS = [
|
|
26
|
+
'me',
|
|
27
|
+
'me/recent-ships?limit=30',
|
|
28
|
+
'me/cross-project',
|
|
29
|
+
'me/profile-nudge',
|
|
30
|
+
'me/render',
|
|
31
|
+
'me/wandering',
|
|
32
|
+
'me/skill-prefs',
|
|
33
|
+
'me/sound-prefs',
|
|
34
|
+
'me/event-sound-prefs',
|
|
35
|
+
'me/art-key/own',
|
|
36
|
+
'builders/:id/profile',
|
|
37
|
+
'builders/:id/active-claims',
|
|
38
|
+
'builders/:id/activity?section=tasks',
|
|
39
|
+
'builders/:id/activity?section=goals',
|
|
40
|
+
'analytics/builder/:id?days=90',
|
|
41
|
+
'inbox/by-builder/:id',
|
|
42
|
+
// Permission-gated (idea.triage, Metic+), so whether this queue exists at all
|
|
43
|
+
// is a fact about YOUR rank — the needs-attention inbox renders it or not.
|
|
44
|
+
'inbox/awaiting-nod?limit=20',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// The key an override is stored under, and the key a request looks itself up
|
|
48
|
+
// by. The api path alone — EXCEPT where ?section= picks the row shape rather
|
|
49
|
+
// than the path (the profile's activity log), which is the same discrimination
|
|
50
|
+
// the harness's FIXTURE_FALLBACKS already makes for that one endpoint.
|
|
51
|
+
function overrideKey(apiPath, query = {}) {
|
|
52
|
+
const p = String(apiPath || '').replace(/\/$/, '');
|
|
53
|
+
return query && query.section ? `${p}?section=${query.section}` : p;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A ':id' template → the concrete request path plus the key it lands under.
|
|
57
|
+
function expandPath(template, id) {
|
|
58
|
+
const [rawPath, search = ''] = String(template).split('?');
|
|
59
|
+
const p = rawPath.replace(':id', encodeURIComponent(String(id)));
|
|
60
|
+
const query = Object.fromEntries(new URLSearchParams(search));
|
|
61
|
+
return { key: overrideKey(p, query), url: search ? `${p}?${search}` : p };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Why we fell back, in the builder's words rather than an HTTP status — this
|
|
65
|
+
// line is printed in the boot banner and is the only signal that the hall is
|
|
66
|
+
// about to greet them as someone else.
|
|
67
|
+
function fallbackReason(status) {
|
|
68
|
+
if (status === 401 || status === 403) return 'your CLI session is no longer valid — run /builder-reauth';
|
|
69
|
+
return `the instance answered GET /me with ${status}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// resolveLiveIdentity — fetch the caller's own me-scoped reads, or explain why
|
|
73
|
+
// not. NEVER throws and never exits: a preview harness that dies because the
|
|
74
|
+
// laptop is offline is worse than one showing fixture data, so every failure
|
|
75
|
+
// path degrades to { ok: false, why } and the fixtures keep serving.
|
|
76
|
+
async function resolveLiveIdentity({
|
|
77
|
+
session,
|
|
78
|
+
fetchImpl = globalThis.fetch,
|
|
79
|
+
apiPrefix = '/api/bongos/v1',
|
|
80
|
+
timeoutMs = 8000,
|
|
81
|
+
} = {}) {
|
|
82
|
+
if (!session || !session.token) {
|
|
83
|
+
return { ok: false, why: 'this machine has no Bongos CLI session — run /builder-setup' };
|
|
84
|
+
}
|
|
85
|
+
const base = String(session.apiBase || '').replace(/\/+$/, '');
|
|
86
|
+
if (!base) return { ok: false, why: 'the CLI session names no instance to read from' };
|
|
87
|
+
|
|
88
|
+
const get = async (url) => {
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetchImpl(`${base}${apiPrefix}/${url}`, {
|
|
91
|
+
headers: { authorization: `Bearer ${session.token}`, accept: 'application/json' },
|
|
92
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) return { ok: false, status: res.status };
|
|
95
|
+
return { ok: true, data: await res.json() };
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { ok: false, status: 0, error: e && e.message };
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const me = await get('me');
|
|
102
|
+
if (!me.ok) return { ok: false, why: me.status ? fallbackReason(me.status) : `could not reach ${base} (${me.error || 'network error'})` };
|
|
103
|
+
const builder = me.data && me.data.builder;
|
|
104
|
+
if (!builder || !builder.id) return { ok: false, why: 'the instance returned no builder on GET /me' };
|
|
105
|
+
|
|
106
|
+
const overrides = new Map([['me', me.data]]);
|
|
107
|
+
const skipped = [];
|
|
108
|
+
const rest = ME_SCOPED_PATHS.filter((t) => t !== 'me').map((t) => expandPath(t, builder.id));
|
|
109
|
+
const results = await Promise.all(rest.map(({ url }) => get(url)));
|
|
110
|
+
rest.forEach(({ key }, i) => {
|
|
111
|
+
// A read this builder's rank can't make (or one this instance doesn't
|
|
112
|
+
// serve) simply keeps its fixture — a partial identity still shows YOUR
|
|
113
|
+
// name, rank and credits, which is the whole point.
|
|
114
|
+
if (results[i].ok) overrides.set(key, results[i].data);
|
|
115
|
+
else skipped.push(key);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return { ok: true, builder, base, overrides, skipped };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { ME_SCOPED_PATHS, overrideKey, expandPath, fallbackReason, resolveLiveIdentity };
|
|
@@ -24,6 +24,7 @@ const SNAPSHOTS = [
|
|
|
24
24
|
['versions', 'versions'],
|
|
25
25
|
['leaderboard', 'leaderboard'],
|
|
26
26
|
['inbox', 'inbox', trimList('ideas', 25)],
|
|
27
|
+
['inbox/awaiting-nod?limit=20', 'inbox__awaiting-nod', trimList('awaiting', 25)],
|
|
27
28
|
['goals', 'goals'],
|
|
28
29
|
['goals/inbox', 'goals__inbox'],
|
|
29
30
|
// goal details: a handful of REAL ids (exact-match fixtures) + goal 25
|
|
@@ -19,8 +19,16 @@
|
|
|
19
19
|
// window.__MODULES__ (verifies moduleOn() gating)
|
|
20
20
|
// --empty serve every fixture with its arrays emptied — the
|
|
21
21
|
// empty-state screenshot pass
|
|
22
|
-
// --rank archon override the /me
|
|
23
|
-
// pages (watch, harbor, gate) render
|
|
22
|
+
// --rank archon override the served /me builder.rank (live or fixture) so the
|
|
23
|
+
// Archon-gated pages (watch, harbor, gate) render in preview
|
|
24
|
+
// --fixture-me do NOT read your own account live; serve the checked-in
|
|
25
|
+
// fixtures/me.json snapshot (deterministic screenshot passes,
|
|
26
|
+
// and the offline/signed-out path) — task 1003315
|
|
27
|
+
//
|
|
28
|
+
// The account is YOURS: when this machine holds a GDS CLI session the me-scoped
|
|
29
|
+
// reads are fetched live at boot and layered over the fixtures, so the hall
|
|
30
|
+
// greets the builder previewing the change rather than whoever's snapshot is
|
|
31
|
+
// checked in (task 1003315). Signed out, offline, or --fixture-me → fixtures.
|
|
24
32
|
//
|
|
25
33
|
// Browse http://builders.localhost:<port>/ — Chromium resolves *.localhost,
|
|
26
34
|
// and the `builders.` host prefix exercises the real subdomain serving path.
|
|
@@ -41,9 +49,11 @@ function arg(name) {
|
|
|
41
49
|
}
|
|
42
50
|
const PORT = Number(arg('--port')) || 4600;
|
|
43
51
|
const EMPTY = process.argv.includes('--empty');
|
|
44
|
-
// --rank: view the hall as a different rank — the /me
|
|
52
|
+
// --rank: view the hall as a different rank — the served /me builder.rank is
|
|
45
53
|
// overridden in the response, so the Archon-gated pages (watch, harbor, gate)
|
|
46
|
-
// render their real content instead of the sealed notice.
|
|
54
|
+
// render their real content instead of the sealed notice. Since task 1003315 the
|
|
55
|
+
// served /me is normally your OWN account, so this is now only needed to view
|
|
56
|
+
// the hall as a rank you are not. Preview-only theatre:
|
|
47
57
|
// the real pages are server-gated (requireGovernmentPage / ADR 0016).
|
|
48
58
|
const RANK_OVERRIDE = arg('--rank');
|
|
49
59
|
const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
@@ -51,6 +61,10 @@ const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.t
|
|
|
51
61
|
// often lacks prod's instance modules (provisioning, dev-box), and their
|
|
52
62
|
// widgets can't be reviewed without it (task 2169).
|
|
53
63
|
const MODULES_ON = String(arg('--modules-on') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
64
|
+
// --fixture-me: pin the checked-in /me snapshot instead of reading the caller's
|
|
65
|
+
// own account live. The screenshot matrix in the README wants a stable subject,
|
|
66
|
+
// and it is the explicit way to preview the signed-out fallback (task 1003315).
|
|
67
|
+
const FIXTURE_ME = process.argv.includes('--fixture-me');
|
|
54
68
|
const BRAND_FILE = arg('--brand') || 'config/branding.neutral.json';
|
|
55
69
|
process.env.GDS_BRANDING_FILE = path.resolve(ROOT, BRAND_FILE);
|
|
56
70
|
|
|
@@ -65,6 +79,21 @@ catch (_) { express = require('express'); }
|
|
|
65
79
|
const { buildStampedHtml } = require(path.join(ROOT, 'src', 'asset-version.js'));
|
|
66
80
|
const { injectBrandingGlobal, mountInternalSurfaces } = require(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'));
|
|
67
81
|
const { clientModules } = require(path.join(ROOT, 'src', 'modules.js'));
|
|
82
|
+
const { overrideKey, resolveLiveIdentity } = require('./live-identity');
|
|
83
|
+
|
|
84
|
+
// The session is the ONE thing this harness reads from outside its own tree, and
|
|
85
|
+
// it reads it through the FAIL-OPEN helper on purpose: apiCall/cliClient exit the
|
|
86
|
+
// process on a 401, which would kill a preview over a cosmetic feature.
|
|
87
|
+
function readSession() {
|
|
88
|
+
try { return require(path.join(ROOT, 'scripts', 'gds', 'cli-lib')).readSessionToken(); }
|
|
89
|
+
catch (_) { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Live me-scoped payloads, keyed by overrideKey(); empty until boot resolves
|
|
93
|
+
// them, and empty forever when signed out / offline / --fixture-me.
|
|
94
|
+
let liveOverrides = new Map();
|
|
95
|
+
let liveBuilder = null;
|
|
96
|
+
let liveWhy = null;
|
|
68
97
|
|
|
69
98
|
// ---- fixtures ---------------------------------------------------------------
|
|
70
99
|
// File naming: the API path with slashes as "__" — /me/recent-ships →
|
|
@@ -151,7 +180,11 @@ app.use((req, res, next) => {
|
|
|
151
180
|
if (req.method !== 'GET') {
|
|
152
181
|
return res.status(501).json({ error: { code: 'preview_readonly', message: 'the preview harness serves fixtures; writes are not wired' } });
|
|
153
182
|
}
|
|
154
|
-
|
|
183
|
+
// Your own account wins over the checked-in snapshot; a me-scoped read that
|
|
184
|
+
// wasn't resolved live falls through to its fixture exactly as before.
|
|
185
|
+
const q = req.query || {};
|
|
186
|
+
const live = liveOverrides.get(overrideKey(apiPath, q)) ?? liveOverrides.get(apiPath);
|
|
187
|
+
const fix = live !== undefined ? live : fixtureFor(apiPath, q);
|
|
155
188
|
if (fix === undefined) {
|
|
156
189
|
console.log(` no fixture: ${apiPath} (add scripts/hall-preview/fixtures/${apiPath.replace(/\//g, '__')}.json)`);
|
|
157
190
|
return res.status(404).json({ error: { code: 'no_fixture', message: apiPath } });
|
|
@@ -197,10 +230,41 @@ app.get(/^\/([a-z-]+)$/, (req, res, next) => {
|
|
|
197
230
|
// the page routes we already served.
|
|
198
231
|
mountInternalSurfaces(app);
|
|
199
232
|
|
|
233
|
+
// Whose hall this is, said plainly in the banner. When it is NOT yours, that is
|
|
234
|
+
// the line that says so — the old harness printed nothing and the substitution
|
|
235
|
+
// was only visible as a stranger's name on the page (task 1003315).
|
|
236
|
+
function accountLine() {
|
|
237
|
+
if (liveBuilder) {
|
|
238
|
+
const who = liveBuilder.github_login ? `@${liveBuilder.github_login}` : liveBuilder.display_name;
|
|
239
|
+
return ` account: ${who} (${liveBuilder.rank}) — your own, live${RANK_OVERRIDE ? ` [rank forced to ${RANK_OVERRIDE}]` : ''}`;
|
|
240
|
+
}
|
|
241
|
+
const why = FIXTURE_ME ? '--fixture-me' : liveWhy;
|
|
242
|
+
// A published core carves fixtures/ out (publish-manifest.js) — there the
|
|
243
|
+
// fallback is not "someone else's account", it is NO account, and the hall
|
|
244
|
+
// will 404 its way through /me. Say which of the two this is.
|
|
245
|
+
const snap = (loadFixture('me') || {}).builder;
|
|
246
|
+
if (!snap) return ` account: NONE — no live read (${why}) and this checkout ships no me fixture`;
|
|
247
|
+
const who = snap.github_login ? `@${snap.github_login}` : (snap.display_name || 'a checked-in snapshot');
|
|
248
|
+
return ` account: ${who} (${RANK_OVERRIDE || snap.rank}) — NOT YOURS, the checked-in fixture: ${why}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
200
251
|
// Loopback only — this is a pre-auth preview of hall pages with fixture data;
|
|
201
252
|
// *.localhost resolves to 127.0.0.1 so nothing else is needed.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
253
|
+
(async () => {
|
|
254
|
+
if (!FIXTURE_ME) {
|
|
255
|
+
const res = await resolveLiveIdentity({ session: readSession() });
|
|
256
|
+
if (res.ok) {
|
|
257
|
+
liveOverrides = res.overrides;
|
|
258
|
+
liveBuilder = res.builder;
|
|
259
|
+
if (res.skipped.length) console.log(` (kept fixtures for: ${res.skipped.join(', ')})`);
|
|
260
|
+
} else {
|
|
261
|
+
liveWhy = res.why;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
app.listen(PORT, '127.0.0.1', () => {
|
|
265
|
+
console.log(`hall-preview: http://builders.localhost:${PORT}/`);
|
|
266
|
+
console.log(accountLine());
|
|
267
|
+
console.log(` brand: ${BRAND_FILE}${EMPTY ? ' (EMPTY fixtures)' : ''}${MODULES_OFF.length ? ` modules off: ${MODULES_OFF.join(',')}` : ''}`);
|
|
268
|
+
console.log(' pages:', Object.keys(stamped).map((f) => f.replace(/\.html$/, '')).sort().join(' '));
|
|
269
|
+
});
|
|
270
|
+
})();
|
package/src/module-api.js
CHANGED
|
@@ -1216,8 +1216,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1216
1216
|
// landed since 1.19.386 with no explicit bump. run 33121066592. (task 1002620)
|
|
1217
1217
|
// 1.19.388 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1218
1218
|
// landed since 1.19.387 with no explicit bump. run 33125589814. (task 1002620)
|
|
1219
|
+
// 1.19.389 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1220
|
+
// landed since 1.19.388 with no explicit bump. run 33126279278. (task 1002620)
|
|
1221
|
+
// 1.19.390 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1222
|
+
// landed since 1.19.389 with no explicit bump. run 33133575931. (task 1002620)
|
|
1219
1223
|
// ---------------------------------------------------------------------------
|
|
1220
|
-
const CORE_VERSION = '1.19.
|
|
1224
|
+
const CORE_VERSION = '1.19.390'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1221
1225
|
|
|
1222
1226
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1223
1227
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|