@bongos/core 1.19.384 → 1.19.386
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 +15 -15
- 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/docs-entropy.js +11 -1
- package/scripts/gds/ship-grade-steps.js +43 -2
- package/src/module-api.js +5 -1
- package/tests/docs_entropy.mjs +86 -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.386",
|
|
6
|
+
"core_contract": "1.19.386",
|
|
7
|
+
"source_commit": "c4899bbdaafe758d4521d6cb7944ad0117886573",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-27T21:
|
|
9
|
+
"built_at": "2026-08-27T21:58:01.005Z",
|
|
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": "856370a346d104a96b56e2fe9a3fefd50a50298a18e82c9afa13ef04387f6b7a",
|
|
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": "34407c6061bf61f8ec416b91f30da5ae1140c1e584e6721af64fbadadb60c9b0"
|
|
5121
5121
|
},
|
|
5122
5122
|
{
|
|
5123
5123
|
"path": "package.json",
|
|
5124
5124
|
"mode": "0000644",
|
|
5125
|
-
"sha256": "
|
|
5125
|
+
"sha256": "e49313249eef5acc6d3a2926aeb5725f0773d8d4300393a8ed57a3016a25a995"
|
|
5126
5126
|
},
|
|
5127
5127
|
{
|
|
5128
5128
|
"path": "public-docs/index.html",
|
|
@@ -5782,7 +5782,7 @@
|
|
|
5782
5782
|
{
|
|
5783
5783
|
"path": "scripts/gds/docs-entropy.js",
|
|
5784
5784
|
"mode": "0000644",
|
|
5785
|
-
"sha256": "
|
|
5785
|
+
"sha256": "e052ddf4d44c0d67512cb14e150cd309a9ab09414c557db8319a57fd8f0d6961"
|
|
5786
5786
|
},
|
|
5787
5787
|
{
|
|
5788
5788
|
"path": "scripts/gds/doctor.js",
|
|
@@ -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": "28a5f25668bc0297f421e200dbb658a5365db71874af4f8fd29c37016b4dcd29"
|
|
7036
7036
|
},
|
|
7037
7037
|
{
|
|
7038
7038
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7862,7 +7862,7 @@
|
|
|
7862
7862
|
{
|
|
7863
7863
|
"path": "tests/docs_entropy.mjs",
|
|
7864
7864
|
"mode": "0000644",
|
|
7865
|
-
"sha256": "
|
|
7865
|
+
"sha256": "c3ef388bb7baacd4da4319fed82ceb5687e0d444cacce2ee84f9061dccbd1018"
|
|
7866
7866
|
},
|
|
7867
7867
|
{
|
|
7868
7868
|
"path": "tests/docs_openapi_route.mjs",
|
|
@@ -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.386",
|
|
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.386",
|
|
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.386",
|
|
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",
|
|
@@ -89,7 +89,17 @@ function parseFile(absPath) {
|
|
|
89
89
|
return { headings: [], links: [], paragraphs: [] };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
// Split on CRLF **or** LF, and drop a leading BOM (task 1003314). A Windows
|
|
93
|
+
// checkout gets CRLF from git, and splitting on a bare newline then leaves a
|
|
94
|
+
// trailing carriage return on every line — which the heading regex below
|
|
95
|
+
// cannot match, because `.` excludes line terminators and `$` is not in
|
|
96
|
+
// multiline mode. The file parsed as having ZERO headings, so every
|
|
97
|
+
// [text](file.md#anchor) pointing into it was reported as a broken anchor:
|
|
98
|
+
// doc_broken_anchors read 21 on Windows and 6 in CI, and the quality ratchet
|
|
99
|
+
// a Windows builder ran was pure noise. Same failure family as task 1002526's
|
|
100
|
+
// `git ls-files` quoting bug, one layer in — a gate that reports confidently
|
|
101
|
+
// and wrongly on one platform.
|
|
102
|
+
const lines = raw.replace(/^\uFEFF/, '').split(/\r?\n/);
|
|
93
103
|
const headings = [];
|
|
94
104
|
const links = [];
|
|
95
105
|
const paragraphs = [];
|
|
@@ -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,12 @@ 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)
|
|
1213
|
+
// 1.19.386 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1214
|
+
// landed since 1.19.385 with no explicit bump. run 33120468176. (task 1002620)
|
|
1211
1215
|
// ---------------------------------------------------------------------------
|
|
1212
|
-
const CORE_VERSION = '1.19.
|
|
1216
|
+
const CORE_VERSION = '1.19.386'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1213
1217
|
|
|
1214
1218
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1215
1219
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/docs_entropy.mjs
CHANGED
|
@@ -12,12 +12,14 @@ import { test } from 'node:test';
|
|
|
12
12
|
import { execFileSync } from 'node:child_process';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
15
17
|
import path from 'node:path';
|
|
16
18
|
|
|
17
19
|
const require = createRequire(import.meta.url);
|
|
18
20
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
21
|
const SCANNER = path.join(ROOT, 'scripts', 'gds', 'docs-entropy.js');
|
|
20
|
-
const { collectMarkdownFiles } = require(SCANNER); // require.main-guarded → no scan on import
|
|
22
|
+
const { collectMarkdownFiles, parseFile } = require(SCANNER); // require.main-guarded → no scan on import
|
|
21
23
|
|
|
22
24
|
test('collectMarkdownFiles passes the glob as an argv array, never a shell string', () => {
|
|
23
25
|
let seen = null;
|
|
@@ -53,3 +55,86 @@ test('the real scan enumerates the repo (the regression: it reported on 0 files)
|
|
|
53
55
|
assert.ok(Number.isFinite(result.stale_links));
|
|
54
56
|
assert.ok(Number.isFinite(result.broken_anchors));
|
|
55
57
|
});
|
|
58
|
+
|
|
59
|
+
// --- CRLF: a Windows checkout must parse exactly like a Linux one (task 1003314) ---
|
|
60
|
+
//
|
|
61
|
+
// The bug: parseFile() split on a bare '\n', so on a Windows checkout (git hands
|
|
62
|
+
// out CRLF) every line kept a trailing '\r'. The heading regex /^(#{1,6})\s+(.+)$/
|
|
63
|
+
// cannot match that — '.' excludes line terminators and '$' is not in multiline
|
|
64
|
+
// mode — so EVERY CRLF file parsed as having zero headings, and every
|
|
65
|
+
// [text](file.md#anchor) pointing into one was reported broken. doc_broken_anchors
|
|
66
|
+
// read 21 on Windows against a baseline of 6, which made the quality ratchet
|
|
67
|
+
// unusable there: a builder could not tell a real regression from platform noise.
|
|
68
|
+
// Same family as the `git ls-files` quoting bug above — a gate that reports
|
|
69
|
+
// confidently and WRONGLY on one platform only.
|
|
70
|
+
|
|
71
|
+
const DOC_LINES = [
|
|
72
|
+
'# Title one',
|
|
73
|
+
'',
|
|
74
|
+
'Some prose with a [link](other.md#deep-anchor) in it.',
|
|
75
|
+
'',
|
|
76
|
+
'## 12. Session Update Protocol',
|
|
77
|
+
'',
|
|
78
|
+
'More prose.',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Write `text` to a temp .md and parse it — parseFile takes a path, not a string.
|
|
82
|
+
function parseString(text) {
|
|
83
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'docs-entropy-eol-'));
|
|
84
|
+
try {
|
|
85
|
+
const file = path.join(dir, 'sample.md');
|
|
86
|
+
fs.writeFileSync(file, text);
|
|
87
|
+
return parseFile(file);
|
|
88
|
+
} finally {
|
|
89
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
test('parseFile reads CRLF and LF identically — headings, slugs and links', () => {
|
|
94
|
+
const lf = parseString(DOC_LINES.join('\n'));
|
|
95
|
+
const crlf = parseString(DOC_LINES.join('\r\n'));
|
|
96
|
+
|
|
97
|
+
assert.equal(lf.headings.length, 2, 'fixture sanity: the LF form has two headings');
|
|
98
|
+
assert.deepEqual(
|
|
99
|
+
crlf.headings, lf.headings,
|
|
100
|
+
'a CRLF file must parse to the same headings as the LF one — before this fix it parsed to NONE',
|
|
101
|
+
);
|
|
102
|
+
assert.deepEqual(crlf.links.map((l) => l.href), lf.links.map((l) => l.href));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('parseFile slugs a CRLF heading without a trailing carriage return', () => {
|
|
106
|
+
const slugs = parseString(DOC_LINES.join('\r\n')).headings.map((h) => h.slug);
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
slugs, ['title-one', '12-session-update-protocol'],
|
|
109
|
+
'the real anchor CLAUDE.md cites — a stray \\r in the slug is what made it report as "missing"',
|
|
110
|
+
);
|
|
111
|
+
assert.ok(!slugs.some((s) => s.includes('\r')), 'no slug carries a carriage return');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('parseFile ignores a leading BOM, so a BOM file is not one heading short', () => {
|
|
115
|
+
// Same class, different byte: PowerShell writes UTF-8 WITH BOM by default
|
|
116
|
+
// (scripts/gds/CLAUDE.md warns about this for JSON bodies) and
|
|
117
|
+
// readFileSync('utf8') does not strip it — the BOM would sit before the first
|
|
118
|
+
// '#' and hide that heading.
|
|
119
|
+
const withBom = parseString('' + DOC_LINES.join('\r\n'));
|
|
120
|
+
assert.equal(withBom.headings.length, 2, 'the first heading survives a BOM');
|
|
121
|
+
assert.equal(withBom.headings[0].text, 'Title one');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the real scan on THIS checkout matches the CI baselines (the platform-parity proof)', () => {
|
|
125
|
+
// The point of the fix: the number a Windows builder sees is the number CI
|
|
126
|
+
// sees. Read the baseline rather than hardcoding it, so this stays honest as
|
|
127
|
+
// the ratchet legitimately tightens.
|
|
128
|
+
const stdout = execFileSync(process.execPath, [SCANNER, '--json'], {
|
|
129
|
+
cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
|
|
130
|
+
});
|
|
131
|
+
const result = JSON.parse(stdout);
|
|
132
|
+
const baselines = JSON.parse(
|
|
133
|
+
fs.readFileSync(path.join(ROOT, 'config', 'fitness-baselines.json'), 'utf8'),
|
|
134
|
+
).metrics;
|
|
135
|
+
assert.ok(
|
|
136
|
+
result.broken_anchors <= baselines.doc_broken_anchors,
|
|
137
|
+
`broken_anchors ${result.broken_anchors} exceeds the baseline ${baselines.doc_broken_anchors} — `
|
|
138
|
+
+ 'on a CRLF checkout this read 21, because every heading in every CRLF file went unparsed',
|
|
139
|
+
);
|
|
140
|
+
});
|
|
@@ -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`);
|