@bongos/core 1.19.384 → 1.19.385
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 +13 -13
- package/modules/grading/grader-workers/index.js +46 -3
- package/modules/grading/grader.js +25 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship-grade-steps.js +43 -2
- package/src/module-api.js +3 -1
- package/tests/grade_unavailable_display.mjs +48 -0
- package/tests/grader_panel.mjs +84 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.385",
|
|
6
|
+
"core_contract": "1.19.385",
|
|
7
|
+
"source_commit": "3ed24b93e1f32dddb11c2a7a8bfaa9dda77a6f31",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-27T21:
|
|
9
|
+
"built_at": "2026-08-27T21:33:52.536Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 338,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1963,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "405834d1bb0740aa2af5d7666798ac5219e895d514d397afb2c9c53953a431c8",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -3537,7 +3537,7 @@
|
|
|
3537
3537
|
{
|
|
3538
3538
|
"path": "modules/grading/grader-workers/index.js",
|
|
3539
3539
|
"mode": "0000644",
|
|
3540
|
-
"sha256": "
|
|
3540
|
+
"sha256": "186ebc1d6456f3d9d0435766744fd767005b667fc4f6020bb9041f6b66388c4b"
|
|
3541
3541
|
},
|
|
3542
3542
|
{
|
|
3543
3543
|
"path": "modules/grading/grader-workers/narc.js",
|
|
@@ -3567,7 +3567,7 @@
|
|
|
3567
3567
|
{
|
|
3568
3568
|
"path": "modules/grading/grader.js",
|
|
3569
3569
|
"mode": "0000644",
|
|
3570
|
-
"sha256": "
|
|
3570
|
+
"sha256": "53001adcb058742f39bdb475fca023809c239f50bf9dddc61ccef966d5d01d6a"
|
|
3571
3571
|
},
|
|
3572
3572
|
{
|
|
3573
3573
|
"path": "modules/grading/module.json",
|
|
@@ -5117,12 +5117,12 @@
|
|
|
5117
5117
|
{
|
|
5118
5118
|
"path": "package-lock.json",
|
|
5119
5119
|
"mode": "0000644",
|
|
5120
|
-
"sha256": "
|
|
5120
|
+
"sha256": "44a05f902a0d9063f2642c03a377c0c850dc4871e65c8b680c1190ca905bb3ec"
|
|
5121
5121
|
},
|
|
5122
5122
|
{
|
|
5123
5123
|
"path": "package.json",
|
|
5124
5124
|
"mode": "0000644",
|
|
5125
|
-
"sha256": "
|
|
5125
|
+
"sha256": "da469f97be509ad2739415aa339e06765bb4b3ea0c828620fb72391e76bb8862"
|
|
5126
5126
|
},
|
|
5127
5127
|
{
|
|
5128
5128
|
"path": "public-docs/index.html",
|
|
@@ -6472,7 +6472,7 @@
|
|
|
6472
6472
|
{
|
|
6473
6473
|
"path": "scripts/gds/ship-grade-steps.js",
|
|
6474
6474
|
"mode": "0000644",
|
|
6475
|
-
"sha256": "
|
|
6475
|
+
"sha256": "e68f4c2848cb59878cd0b96b48fdd075db4a035354d51afef52aa6225f7895e5"
|
|
6476
6476
|
},
|
|
6477
6477
|
{
|
|
6478
6478
|
"path": "scripts/gds/ship-grade.js",
|
|
@@ -7032,7 +7032,7 @@
|
|
|
7032
7032
|
{
|
|
7033
7033
|
"path": "src/module-api.js",
|
|
7034
7034
|
"mode": "0000644",
|
|
7035
|
-
"sha256": "
|
|
7035
|
+
"sha256": "63478a33b8d134113541ea814481eedb00ad89ac0b6777e87a1a6053b5718d72"
|
|
7036
7036
|
},
|
|
7037
7037
|
{
|
|
7038
7038
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8302,7 +8302,7 @@
|
|
|
8302
8302
|
{
|
|
8303
8303
|
"path": "tests/grade_unavailable_display.mjs",
|
|
8304
8304
|
"mode": "0000644",
|
|
8305
|
-
"sha256": "
|
|
8305
|
+
"sha256": "08e08714d025fe7c0f1177fbfa6724241ff519cd34848f953d5bc7cce6c142fa"
|
|
8306
8306
|
},
|
|
8307
8307
|
{
|
|
8308
8308
|
"path": "tests/grader.mjs",
|
|
@@ -8322,7 +8322,7 @@
|
|
|
8322
8322
|
{
|
|
8323
8323
|
"path": "tests/grader_panel.mjs",
|
|
8324
8324
|
"mode": "0000644",
|
|
8325
|
-
"sha256": "
|
|
8325
|
+
"sha256": "32dc0e219ed6aa7430c9ba9523faddb2ddc9602d939731788c660d9d79751d22"
|
|
8326
8326
|
},
|
|
8327
8327
|
{
|
|
8328
8328
|
"path": "tests/grader_prompt_fencing.mjs",
|
|
@@ -70,6 +70,35 @@ const WORKER_MAX_TIMEOUT_MS = {
|
|
|
70
70
|
efficiency: 360_000, // Read-heavy on a large diff: traces perf across every changed file
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
// task 1003313: the diagnostic a dark panel used to throw away.
|
|
74
|
+
//
|
|
75
|
+
// runSubagent attaches exit_code + stdout + stderr to every subprocess error,
|
|
76
|
+
// but the catch below kept only err.code — so a TOTAL outage stored four
|
|
77
|
+
// identical `SUBAGENT_NONZERO_EXIT` rows and nothing else, and two builders sat
|
|
78
|
+
// dark for days with a 0/10 card that could not name its own cause. At least two
|
|
79
|
+
// known environmental faults produce that exact signature (a logged-out `claude`
|
|
80
|
+
// CLI — preflight.js claudeAuthFinding; the Windows WindowsApps alias stub —
|
|
81
|
+
// learning 42) and the stored grade could not tell them apart.
|
|
82
|
+
//
|
|
83
|
+
// The tail is REDACTED and BOUNDED because it lands in a grade row that the hall
|
|
84
|
+
// renders: CLI stderr can carry an auth header or a key echoed back in an error.
|
|
85
|
+
const STDERR_TAIL_MAX = 400;
|
|
86
|
+
// eslint-disable-next-line no-control-regex -- stripping ANSI SGR from CLI stderr
|
|
87
|
+
const ANSI_RE = /\x1B\[[0-9;]*[A-Za-z]/g;
|
|
88
|
+
const SECRET_RES = [
|
|
89
|
+
/sk-ant-[A-Za-z0-9_-]+/g,
|
|
90
|
+
/(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
|
|
91
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
92
|
+
/\b[Bb]earer\s+[A-Za-z0-9._~+/-]{12,}=*/g,
|
|
93
|
+
];
|
|
94
|
+
function redactStderrTail(raw) {
|
|
95
|
+
let s = String(raw || '').replace(ANSI_RE, '').trim();
|
|
96
|
+
if (!s) return null;
|
|
97
|
+
for (const re of SECRET_RES) s = s.replace(re, '[redacted]');
|
|
98
|
+
s = s.slice(-STDERR_TAIL_MAX);
|
|
99
|
+
return s || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
73
102
|
// Resolve the timeout for a worker: explicit module override > KIND map >
|
|
74
103
|
// caller's runOpts > undefined (runSubagent applies its own default).
|
|
75
104
|
function timeoutForWorker(worker, runOpts) {
|
|
@@ -127,6 +156,9 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
127
156
|
let runMs = null;
|
|
128
157
|
let costUsd = null;
|
|
129
158
|
let subprocessError = null;
|
|
159
|
+
// task 1003313: the WHY behind subprocessError. Null on a clean run.
|
|
160
|
+
let exitCode = null;
|
|
161
|
+
let stderrTail = null;
|
|
130
162
|
const t0 = Date.now();
|
|
131
163
|
try {
|
|
132
164
|
const { stdout, run_ms, cost_usd } = await runSubagentFn({ prompt, opts: { ...runOpts, model, timeoutMs } });
|
|
@@ -139,6 +171,11 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
139
171
|
} catch (err) {
|
|
140
172
|
runMs = err.run_ms ?? (Date.now() - t0);
|
|
141
173
|
subprocessError = err.code || 'UNKNOWN';
|
|
174
|
+
exitCode = Number.isInteger(err.exit_code) ? err.exit_code : null;
|
|
175
|
+
// A CLI that dies before writing JSON often says why on stdout, not stderr
|
|
176
|
+
// (`claude` prints its "not logged in" line there) — fall back so the tail
|
|
177
|
+
// is never empty when the subprocess did explain itself.
|
|
178
|
+
stderrTail = redactStderrTail(err.stderr) ?? redactStderrTail(err.stdout);
|
|
142
179
|
}
|
|
143
180
|
const parsed = subprocessError
|
|
144
181
|
? null
|
|
@@ -148,10 +185,10 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
148
185
|
run_ms: runMs,
|
|
149
186
|
subprocess_error: subprocessError,
|
|
150
187
|
});
|
|
151
|
-
return { verdict, runMs, costUsd };
|
|
188
|
+
return { verdict, runMs, costUsd, exitCode, stderrTail };
|
|
152
189
|
}
|
|
153
190
|
|
|
154
|
-
let { verdict, runMs, costUsd } = await attempt();
|
|
191
|
+
let { verdict, runMs, costUsd, exitCode, stderrTail } = await attempt();
|
|
155
192
|
// #465: retry ONCE on a transient error (the freshest attempt wins, even if it
|
|
156
193
|
// also errored — one retry only). A real verdict or a SUBAGENT_TIMEOUT is
|
|
157
194
|
// never retried. NB (#486): the freshest attempt's cost wins; a transient
|
|
@@ -159,7 +196,7 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
159
196
|
let retried = false;
|
|
160
197
|
if (TRANSIENT_WORKER_ERRORS.has(verdict.error)) {
|
|
161
198
|
retried = true;
|
|
162
|
-
({ verdict, runMs, costUsd } = await attempt());
|
|
199
|
+
({ verdict, runMs, costUsd, exitCode, stderrTail } = await attempt());
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
return {
|
|
@@ -176,6 +213,10 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
176
213
|
cost_usd: costUsd, // #486: this worker's own subprocess API spend (USD)
|
|
177
214
|
retried,
|
|
178
215
|
error: verdict.error ?? null,
|
|
216
|
+
// task 1003313: on an errored worker, WHY. Omitted entirely on a clean run so
|
|
217
|
+
// a healthy grade row keeps its existing shape.
|
|
218
|
+
...(verdict.error && exitCode != null ? { exit_code: exitCode } : {}),
|
|
219
|
+
...(verdict.error && stderrTail ? { stderr_tail: stderrTail } : {}),
|
|
179
220
|
// Narc-specific (carried opaquely; aggregatePanel doesn't need to know):
|
|
180
221
|
...(verdict.ac_checklist ? { ac_checklist: verdict.ac_checklist } : {}),
|
|
181
222
|
};
|
|
@@ -188,6 +229,8 @@ module.exports = {
|
|
|
188
229
|
runWorker,
|
|
189
230
|
timeoutForWorker, // public surface; WORKER_MAX_TIMEOUT_MS stays module-internal
|
|
190
231
|
TRANSIENT_WORKER_ERRORS,
|
|
232
|
+
redactStderrTail, // task 1003313: exported so the redaction contract is pinned
|
|
233
|
+
STDERR_TAIL_MAX,
|
|
191
234
|
// re-export per-worker modules for direct access by tests
|
|
192
235
|
narc,
|
|
193
236
|
quality,
|
|
@@ -88,6 +88,27 @@ const { buildPrompt, extractJson } = require('./grader-prompt.js');
|
|
|
88
88
|
const { aggregateShots, changeMagnitude, hasPanelEvidence, isSubstantiveIssue, serverSideGuards, validateAndScore } = require('./grader-score.js');
|
|
89
89
|
const { buildContextBundle, diffAddedLinesByFile, extractAssistantResult, extractCostUsd, runOneShot, runSubagent, runSubagentCached } = require('./grader-subagent.js');
|
|
90
90
|
|
|
91
|
+
// task 1003313: render the WHY of a total outage into the verdict line.
|
|
92
|
+
//
|
|
93
|
+
// Exists because "GRADER UNAVAILABLE — all 4 workers failed to run" is not a
|
|
94
|
+
// diagnosis: a logged-out `claude` CLI, a Windows app-execution-alias stub, and a
|
|
95
|
+
// genuine API outage all produce it, and the builder sees no way to tell which.
|
|
96
|
+
// runWorker now carries exit_code + a redacted stderr_tail off each dead worker;
|
|
97
|
+
// this puts the shared exit code and the first distinct CLI error line into the
|
|
98
|
+
// text the builder actually reads. Silent when the workers carry no detail
|
|
99
|
+
// (a grade from a server predating the propagation).
|
|
100
|
+
const OUTAGE_TAIL_MAX = 300;
|
|
101
|
+
function describeOutage(gating) {
|
|
102
|
+
const dead = gating.filter((w) => w.error);
|
|
103
|
+
const codes = [...new Set(dead.map((w) => w.exit_code).filter((c) => c != null))];
|
|
104
|
+
const tails = [...new Set(dead.map((w) => w.stderr_tail).filter(Boolean))];
|
|
105
|
+
if (!codes.length && !tails.length) return '';
|
|
106
|
+
const parts = [];
|
|
107
|
+
if (codes.length) parts.push(`exit ${codes.join('/')}`);
|
|
108
|
+
if (tails.length) parts.push(`subprocess said: ${tails[0].slice(-OUTAGE_TAIL_MAX)}`);
|
|
109
|
+
return ` Diagnostic — ${parts.join('; ')}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
91
112
|
function aggregatePanel(workerResults, meta = {}) {
|
|
92
113
|
const N = workerResults.length;
|
|
93
114
|
|
|
@@ -232,6 +253,10 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
232
253
|
const erroredKinds = gating.filter((w) => w.error).map((w) => w.kind);
|
|
233
254
|
verdict =
|
|
234
255
|
`GRADER UNAVAILABLE — all ${gatingError}/${gating.length} gating worker(s) failed to run (${erroredKinds.join(', ')}); no quality signal was produced. This is an infra/outage condition, NOT a quality rejection — manual-confirm available.` +
|
|
256
|
+
// task 1003313: name the cause. Without this the card said only "failed to
|
|
257
|
+
// run" four times over, and two builders sat dark for days because nothing
|
|
258
|
+
// stored — or showed — the exit code and the CLI's own error line.
|
|
259
|
+
describeOutage(gating) +
|
|
235
260
|
advisoryNote;
|
|
236
261
|
panelOutcome = 'unavailable';
|
|
237
262
|
forwardedConfidence = FALLTHROUGH_CONFIDENCE;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.385",
|
|
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.385",
|
|
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.385",
|
|
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",
|
|
@@ -14,6 +14,10 @@ const os = require('node:os');
|
|
|
14
14
|
const { cliClient, rewardPolicy } = require('./cli-lib');
|
|
15
15
|
const { gitOk } = require('./ship-git.js');
|
|
16
16
|
const { resolveInstanceBase } = require('./ship-deploy-target.js');
|
|
17
|
+
// task 1003313: the shared onboarding/doctor preflight — reused (not duplicated)
|
|
18
|
+
// so the grader outage explains itself with the same `claude CLI` verdict doctor
|
|
19
|
+
// gives, and the two can never drift.
|
|
20
|
+
const { preflightChecks } = require('./preflight.js');
|
|
17
21
|
const { computeRawDiffHash, reconstructPanelIssues, runSubagentGrader, shouldReuseGrade } = require('./ship-grade.js');
|
|
18
22
|
|
|
19
23
|
// ---------- Shared grade-step helpers (used by shipMain AND regradeMain) ----------
|
|
@@ -223,7 +227,33 @@ async function gradeAndRecord({ taskId, task, summary, baseline, committedFiles,
|
|
|
223
227
|
// issues + next-step hints). Pure + exported so the #943 grader-unavailable vs
|
|
224
228
|
// FAIL branch is unit-tested without capturing stdout. printGradeOutcome just
|
|
225
229
|
// prints what this returns.
|
|
226
|
-
|
|
230
|
+
// task 1003313: the per-worker diagnostic lines for a total outage, plus the
|
|
231
|
+
// local `claude` CLI verdict. Pure over its inputs (cliFinding is INJECTED, never
|
|
232
|
+
// spawned here) so formatGradeOutcome stays unit-testable without a subprocess.
|
|
233
|
+
function formatOutageDiagnostic(g, cliFinding) {
|
|
234
|
+
const out = [];
|
|
235
|
+
const dead = (g.signals?.workers || []).filter((w) => w && w.error);
|
|
236
|
+
const detailed = dead.filter((w) => w.exit_code != null || w.stderr_tail);
|
|
237
|
+
if (detailed.length) {
|
|
238
|
+
out.push('');
|
|
239
|
+
out.push('What the grader subprocess actually did:');
|
|
240
|
+
for (const w of detailed) {
|
|
241
|
+
const code = w.exit_code != null ? `exit ${w.exit_code}` : w.error;
|
|
242
|
+
out.push(` ${String(w.kind || '?').padEnd(11)} ${code}${w.stderr_tail ? ` — ${w.stderr_tail.split('\n')[0]}` : ''}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (cliFinding && cliFinding.level !== 'ok') {
|
|
246
|
+
out.push('');
|
|
247
|
+
out.push(`Local \`claude\` CLI: ${cliFinding.detail}`);
|
|
248
|
+
if (cliFinding.fix) out.push(` Fix: ${cliFinding.fix}`);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// cliFinding: the preflight `claude CLI` row, injected by printGradeOutcome so a
|
|
254
|
+
// dark panel can say "your CLI is logged out" instead of leaving the builder to
|
|
255
|
+
// guess. Report-only — it never changes the outcome (ADR 0162: no new gates).
|
|
256
|
+
function formatGradeOutcome(g, taskId, cliFinding = null) {
|
|
227
257
|
const out = [];
|
|
228
258
|
// #943: a TOTAL grader outage (every gating worker failed to spawn / the
|
|
229
259
|
// grader was bypassed pre-spawn) is NOT a quality rejection — it's "the grader
|
|
@@ -236,6 +266,7 @@ function formatGradeOutcome(g, taskId) {
|
|
|
236
266
|
out.push('');
|
|
237
267
|
out.push(`Subagent grade: GRADER UNAVAILABLE${reason ? ` (${reason})` : ''} · no quality signal produced — NOT a quality rejection`);
|
|
238
268
|
if (g.notes_md) out.push(g.notes_md.split('\n').map((l) => ' ' + l).join('\n'));
|
|
269
|
+
out.push(...formatOutageDiagnostic(g, cliFinding));
|
|
239
270
|
out.push('');
|
|
240
271
|
out.push('The grader could not run (infra/outage/bypass) — there are no findings to fix. Task stays at completed.');
|
|
241
272
|
out.push('When the grader is reachable again, re-grade:');
|
|
@@ -308,7 +339,17 @@ function formatGradeOutcome(g, taskId) {
|
|
|
308
339
|
// last resort — see feedback_prefer_regrade_over_override). #943: a total grader
|
|
309
340
|
// outage prints as "GRADER UNAVAILABLE", not a 0/10 FAIL — see formatGradeOutcome.
|
|
310
341
|
function printGradeOutcome(g, taskId) {
|
|
311
|
-
|
|
342
|
+
// task 1003313: on a TOTAL outage only, ask the existing preflight whether the
|
|
343
|
+
// local `claude` CLI is launchable and logged in — the two faults that produce
|
|
344
|
+
// this exact all-workers-dead signature. Report-only and outage-only, so the
|
|
345
|
+
// happy path pays nothing and no ship is newly gated (ADR 0162).
|
|
346
|
+
let cliFinding = null;
|
|
347
|
+
if (g?.signals?.panel_outcome === 'unavailable') {
|
|
348
|
+
try {
|
|
349
|
+
cliFinding = preflightChecks().find((r) => r.title === 'claude CLI') || null;
|
|
350
|
+
} catch (_) { /* best-effort: a broken probe must never mask the grade output */ }
|
|
351
|
+
}
|
|
352
|
+
console.log(formatGradeOutcome(g, taskId, cliFinding));
|
|
312
353
|
}
|
|
313
354
|
|
|
314
355
|
// Print the credits line + multiplier breakdown + idea bonus + unlocked
|
package/src/module-api.js
CHANGED
|
@@ -1208,8 +1208,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1208
1208
|
// landed since 1.19.382 with no explicit bump. run 33101389971. (task 1002620)
|
|
1209
1209
|
// 1.19.384 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1210
1210
|
// landed since 1.19.383 with no explicit bump. run 33116184189. (task 1002620)
|
|
1211
|
+
// 1.19.385 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1212
|
+
// landed since 1.19.384 with no explicit bump. run 33118682829. (task 1002620)
|
|
1211
1213
|
// ---------------------------------------------------------------------------
|
|
1212
|
-
const CORE_VERSION = '1.19.
|
|
1214
|
+
const CORE_VERSION = '1.19.385'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1213
1215
|
|
|
1214
1216
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1215
1217
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -35,6 +35,54 @@ test('grader-unavailable grade → "GRADER UNAVAILABLE", NOT a 0/10 FAIL or "fix
|
|
|
35
35
|
assert.doesNotMatch(out, /blended .*\/10/, 'must NOT print a 0/10 quality score for a non-grade');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
// ---------- task 1003313: the outage card says WHY ----------
|
|
39
|
+
//
|
|
40
|
+
// Before this, a total outage printed "GRADER UNAVAILABLE" and stopped — the
|
|
41
|
+
// builder had no way to tell a logged-out CLI from a Windows alias stub from a
|
|
42
|
+
// real API outage, and two builders stayed dark for days. The diagnostic and the
|
|
43
|
+
// local `claude` CLI verdict are report-only: they never change the outcome.
|
|
44
|
+
|
|
45
|
+
const OUTAGE = {
|
|
46
|
+
passed: false, score: 0, threshold: 6, notes_md: 'GRADER UNAVAILABLE — all 4/4 gating worker(s) failed to run',
|
|
47
|
+
rubric_scores: {}, issues: [],
|
|
48
|
+
signals: {
|
|
49
|
+
panel_outcome: 'unavailable',
|
|
50
|
+
workers: ['narc', 'quality', 'hacker', 'efficiency'].map((kind) => ({
|
|
51
|
+
kind, error: 'SUBAGENT_NONZERO_EXIT', exit_code: 1, stderr_tail: 'Invalid API key · Run /login',
|
|
52
|
+
})),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
test('an outage card prints each dead worker with its exit code + CLI error (task 1003313)', () => {
|
|
57
|
+
const out = formatGradeOutcome(OUTAGE, 1003313);
|
|
58
|
+
assert.match(out, /GRADER UNAVAILABLE/);
|
|
59
|
+
assert.match(out, /What the grader subprocess actually did/);
|
|
60
|
+
assert.match(out, /narc\s+exit 1 — Invalid API key/, 'names the worker, its exit code, and what the CLI said');
|
|
61
|
+
assert.doesNotMatch(out, /Fix the issues above/, 'still not a quality rejection');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('an outage card relays the injected claude-CLI preflight finding + its fix (task 1003313)', () => {
|
|
65
|
+
const out = formatGradeOutcome(OUTAGE, 1003313, {
|
|
66
|
+
level: 'warn', title: 'claude CLI',
|
|
67
|
+
detail: 'launches (2.1.0) but is not logged in',
|
|
68
|
+
fix: 'Run `claude login`',
|
|
69
|
+
});
|
|
70
|
+
assert.match(out, /Local `claude` CLI: launches \(2\.1\.0\) but is not logged in/);
|
|
71
|
+
assert.match(out, /Fix: Run `claude login`/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('a HEALTHY claude-CLI finding adds no noise to the outage card (task 1003313)', () => {
|
|
75
|
+
const out = formatGradeOutcome(OUTAGE, 1003313, { level: 'ok', title: 'claude CLI', detail: 'logged in' });
|
|
76
|
+
assert.doesNotMatch(out, /Local `claude` CLI/, 'an ok finding is not worth a line');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('an outage from a pre-propagation server prints no empty diagnostic block (task 1003313)', () => {
|
|
80
|
+
const g = { ...OUTAGE, signals: { panel_outcome: 'unavailable', workers: [{ kind: 'narc', error: 'SUBAGENT_NONZERO_EXIT' }] } };
|
|
81
|
+
const out = formatGradeOutcome(g, 1003313);
|
|
82
|
+
assert.match(out, /GRADER UNAVAILABLE/);
|
|
83
|
+
assert.doesNotMatch(out, /What the grader subprocess actually did/);
|
|
84
|
+
});
|
|
85
|
+
|
|
38
86
|
test('a real FAIL still prints the FAIL block + fix-the-issues guidance (#943 regression guard)', () => {
|
|
39
87
|
const g = {
|
|
40
88
|
passed: false, score: 4, threshold: 6,
|
package/tests/grader_panel.mjs
CHANGED
|
@@ -246,6 +246,90 @@ await atest('graderUnavailableResult: server-compat shape (null axes, fallthroug
|
|
|
246
246
|
assert.equal(r.passed, false);
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
+
// ---------- task 1003313: a dark panel names its own cause ----------
|
|
250
|
+
//
|
|
251
|
+
// The regression these pin: runWorker used to keep only err.code and throw away
|
|
252
|
+
// err.exit_code / err.stderr, so a total outage stored four identical
|
|
253
|
+
// SUBAGENT_NONZERO_EXIT rows and two builders sat dark for days with a 0/10 card
|
|
254
|
+
// that could not say whether the CLI was logged out, aliased, or the API down.
|
|
255
|
+
|
|
256
|
+
const workersMod = require('../modules/grading/grader-workers/index.js');
|
|
257
|
+
const { runWorker, redactStderrTail, STDERR_TAIL_MAX } = workersMod;
|
|
258
|
+
|
|
259
|
+
// A worker module stub: buildPrompt/extractJson/computeVerdict, no spawning.
|
|
260
|
+
const STUB_WORKER = {
|
|
261
|
+
KIND: 'quality', NAME: 'Quality', SPECIALTY: 'stub',
|
|
262
|
+
buildPrompt: () => 'prompt',
|
|
263
|
+
extractJson: (s) => JSON.parse(s),
|
|
264
|
+
computeVerdict: (parsed, meta) => (meta.subprocess_error
|
|
265
|
+
? { verdict: 'fail', scores: {}, issues: [], notes_md: '', confidence: 0, error: meta.subprocess_error }
|
|
266
|
+
: { verdict: 'pass', scores: { code_quality: 9 }, issues: [], notes_md: '', confidence: 0.9, error: null }),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// The exact error runSubagent throws on a nonzero exit.
|
|
270
|
+
const nonzeroExit = ({ exitCode = 1, stderr = '', stdout = '' } = {}) =>
|
|
271
|
+
Object.assign(new Error(`subagent exited with code ${exitCode}`), {
|
|
272
|
+
code: 'SUBAGENT_NONZERO_EXIT', exit_code: exitCode, run_ms: 120, stdout, stderr,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await atest('runWorker: a dead subprocess carries exit_code + stderr_tail onto the worker row (task 1003313)', async () => {
|
|
276
|
+
const r = await runWorker(STUB_WORKER, {
|
|
277
|
+
task: {}, diff: '', changedFiles: [], diffStats: {}, model: 'm', runOpts: {},
|
|
278
|
+
runSubagentFn: async () => { throw nonzeroExit({ exitCode: 1, stderr: 'Invalid API key · Run /login' }); },
|
|
279
|
+
});
|
|
280
|
+
assert.equal(r.error, 'SUBAGENT_NONZERO_EXIT');
|
|
281
|
+
assert.equal(r.exit_code, 1, 'the exit code must survive the catch');
|
|
282
|
+
assert.match(r.stderr_tail, /Invalid API key/, "the CLI's own error line must survive the catch");
|
|
283
|
+
assert.equal(r.retried, true, 'a transient error is still retried once (#465)');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await atest('runWorker: a clean run adds neither exit_code nor stderr_tail (task 1003313)', async () => {
|
|
287
|
+
const r = await runWorker(STUB_WORKER, {
|
|
288
|
+
task: {}, diff: '', changedFiles: [], diffStats: {}, model: 'm', runOpts: {},
|
|
289
|
+
runSubagentFn: async () => ({ stdout: '{"code_quality":9}', run_ms: 10, cost_usd: 0.5 }),
|
|
290
|
+
});
|
|
291
|
+
assert.equal(r.verdict, 'pass');
|
|
292
|
+
assert.equal('exit_code' in r, false, 'a healthy grade row keeps its existing shape');
|
|
293
|
+
assert.equal('stderr_tail' in r, false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await atest('runWorker: stdout is the fallback when the CLI explained itself there (task 1003313)', async () => {
|
|
297
|
+
const r = await runWorker(STUB_WORKER, {
|
|
298
|
+
task: {}, diff: '', changedFiles: [], diffStats: {}, model: 'm', runOpts: {},
|
|
299
|
+
runSubagentFn: async () => { throw nonzeroExit({ exitCode: 1, stderr: '', stdout: 'Not logged in.' }); },
|
|
300
|
+
});
|
|
301
|
+
assert.match(r.stderr_tail, /Not logged in/);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('redactStderrTail: strips secrets, strips ANSI, bounds the length (task 1003313)', () => {
|
|
305
|
+
assert.match(redactStderrTail('auth failed for sk-ant-api03-AbCdEf_12345'), /\[redacted\]/);
|
|
306
|
+
assert.doesNotMatch(redactStderrTail('token ghp_abcdefghijklmnopqrstuvwxyz0123'), /ghp_/);
|
|
307
|
+
assert.doesNotMatch(redactStderrTail('header Bearer eyJhbGciOiJIUzI1NiJ9.abc'), /eyJhbGci/);
|
|
308
|
+
assert.equal(redactStderrTail('\u001B[31mred error\u001B[0m'), 'red error', 'ANSI colour codes are stripped');
|
|
309
|
+
assert.equal(redactStderrTail('x'.repeat(STDERR_TAIL_MAX + 200)).length, STDERR_TAIL_MAX);
|
|
310
|
+
assert.equal(redactStderrTail(' '), null, 'an empty tail is null, not an empty string');
|
|
311
|
+
assert.equal(redactStderrTail(undefined), null);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('aggregatePanel: the GRADER UNAVAILABLE verdict names exit code + CLI error (task 1003313)', () => {
|
|
315
|
+
const dead = (kind) => ({
|
|
316
|
+
kind, verdict: 'fail', error: 'SUBAGENT_NONZERO_EXIT', scores: {}, issues: [], confidence: 0,
|
|
317
|
+
exit_code: 1, stderr_tail: 'Invalid API key · Run /login',
|
|
318
|
+
});
|
|
319
|
+
const r = aggregatePanel([dead('narc'), dead('quality'), dead('hacker'), dead('efficiency')], {});
|
|
320
|
+
assert.equal(r.signals.panel_outcome, 'unavailable');
|
|
321
|
+
assert.match(r.notes_md, /GRADER UNAVAILABLE/);
|
|
322
|
+
assert.match(r.notes_md, /exit 1/, 'the verdict text must carry the exit code');
|
|
323
|
+
assert.match(r.notes_md, /Invalid API key/, 'the verdict text must carry the CLI error line');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('aggregatePanel: no diagnostic appended when the workers carry none (pre-propagation grades)', () => {
|
|
327
|
+
const dead = (kind) => ({ kind, verdict: 'fail', error: 'SUBAGENT_NONZERO_EXIT', scores: {}, issues: [], confidence: 0 });
|
|
328
|
+
const r = aggregatePanel([dead('narc'), dead('quality'), dead('hacker'), dead('efficiency')], {});
|
|
329
|
+
assert.equal(r.signals.panel_outcome, 'unavailable');
|
|
330
|
+
assert.doesNotMatch(r.notes_md, /Diagnostic —/);
|
|
331
|
+
});
|
|
332
|
+
|
|
249
333
|
console.log(`${asyncPassed} passed, ${asyncFailed} failed (async)`);
|
|
250
334
|
const totalFailed = failed + asyncFailed;
|
|
251
335
|
console.log(`\n${passed + asyncPassed} passed, ${totalFailed} failed`);
|