@bongos/core 1.19.527 → 1.19.528
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 +20 -15
- package/docs/module-api-changelog.md +2 -0
- package/modules/grading/grader-subagent.js +20 -1
- package/modules/grading/grader.js +24 -1
- package/modules/lifecycle/cascade.js +86 -4
- package/modules/lifecycle/tests/cascade.mjs +63 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/grader_root_outage.mjs +109 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
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.528",
|
|
6
|
+
"core_contract": "1.19.528",
|
|
7
|
+
"source_commit": "f6d2603506a4eaa27fbddbddc75a6119738b71a6",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-04T02:27:38.586Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 434,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1976,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2434,
|
|
20
|
+
"tree_sha256": "965d514d2cf50b0b282abb1e8171fb9a9d78b10f11f043086fa2f2af7083ef76",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2612,7 +2612,7 @@
|
|
|
2612
2612
|
{
|
|
2613
2613
|
"path": "docs/module-api-changelog.md",
|
|
2614
2614
|
"mode": "0000644",
|
|
2615
|
-
"sha256": "
|
|
2615
|
+
"sha256": "4f91a1b18b26d90010d2f78214ed38f9c2f469e53a04e08e8b398bbd753998a9"
|
|
2616
2616
|
},
|
|
2617
2617
|
{
|
|
2618
2618
|
"path": "docs/modules-contract.md",
|
|
@@ -4417,7 +4417,7 @@
|
|
|
4417
4417
|
{
|
|
4418
4418
|
"path": "modules/grading/grader-subagent.js",
|
|
4419
4419
|
"mode": "0000644",
|
|
4420
|
-
"sha256": "
|
|
4420
|
+
"sha256": "8a100cca2391e018cf257e178c253cb21f514a07abda1a8f07da754b76aa83ad"
|
|
4421
4421
|
},
|
|
4422
4422
|
{
|
|
4423
4423
|
"path": "modules/grading/grader-workers/efficiency.js",
|
|
@@ -4472,7 +4472,7 @@
|
|
|
4472
4472
|
{
|
|
4473
4473
|
"path": "modules/grading/grader.js",
|
|
4474
4474
|
"mode": "0000644",
|
|
4475
|
-
"sha256": "
|
|
4475
|
+
"sha256": "4c664f5080513169c35369e5406f32efa64ce24259fefc47f527a94ff8001b1f"
|
|
4476
4476
|
},
|
|
4477
4477
|
{
|
|
4478
4478
|
"path": "modules/grading/module.json",
|
|
@@ -5257,7 +5257,7 @@
|
|
|
5257
5257
|
{
|
|
5258
5258
|
"path": "modules/lifecycle/cascade.js",
|
|
5259
5259
|
"mode": "0000644",
|
|
5260
|
-
"sha256": "
|
|
5260
|
+
"sha256": "2192c61c606a37b4782db0646242ac969bcd318d4305b1247adf028b14f2e484"
|
|
5261
5261
|
},
|
|
5262
5262
|
{
|
|
5263
5263
|
"path": "modules/lifecycle/claim-eligibility.js",
|
|
@@ -5627,7 +5627,7 @@
|
|
|
5627
5627
|
{
|
|
5628
5628
|
"path": "modules/lifecycle/tests/cascade.mjs",
|
|
5629
5629
|
"mode": "0000644",
|
|
5630
|
-
"sha256": "
|
|
5630
|
+
"sha256": "af00ee4629002c8df2448e2146a24108407a36b062859ee1afee293aa60cb209"
|
|
5631
5631
|
},
|
|
5632
5632
|
{
|
|
5633
5633
|
"path": "modules/lifecycle/version-close.js",
|
|
@@ -7322,12 +7322,12 @@
|
|
|
7322
7322
|
{
|
|
7323
7323
|
"path": "package-lock.json",
|
|
7324
7324
|
"mode": "0000644",
|
|
7325
|
-
"sha256": "
|
|
7325
|
+
"sha256": "dceea98bb14779f8b25de5d336677be85b46644d37eab685a2846de68f3bbc99"
|
|
7326
7326
|
},
|
|
7327
7327
|
{
|
|
7328
7328
|
"path": "package.json",
|
|
7329
7329
|
"mode": "0000644",
|
|
7330
|
-
"sha256": "
|
|
7330
|
+
"sha256": "a551e83f0029826d20ab7a280ca4cda7da3aeb98f06d064b1592e84b278c7882"
|
|
7331
7331
|
},
|
|
7332
7332
|
{
|
|
7333
7333
|
"path": "public-docs/index.html",
|
|
@@ -9007,7 +9007,7 @@
|
|
|
9007
9007
|
{
|
|
9008
9008
|
"path": "src/module-api.js",
|
|
9009
9009
|
"mode": "0000644",
|
|
9010
|
-
"sha256": "
|
|
9010
|
+
"sha256": "a530f0345672c3ba330cec9e0056eccf02041ef54f932d6f9959bd24d5fc6178"
|
|
9011
9011
|
},
|
|
9012
9012
|
{
|
|
9013
9013
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10389,6 +10389,11 @@
|
|
|
10389
10389
|
"mode": "0000644",
|
|
10390
10390
|
"sha256": "50a785eb6e2c71d5a043c2ec2c37a835777da6397dac3ffc2542acdde14b57a8"
|
|
10391
10391
|
},
|
|
10392
|
+
{
|
|
10393
|
+
"path": "tests/grader_root_outage.mjs",
|
|
10394
|
+
"mode": "0000644",
|
|
10395
|
+
"sha256": "409b21de4ac729cf90c792be28fcc358349e80dca67e47255a0105377d2cfcf8"
|
|
10396
|
+
},
|
|
10392
10397
|
{
|
|
10393
10398
|
"path": "tests/grader_severity_gate.mjs",
|
|
10394
10399
|
"mode": "0000644",
|
|
@@ -1491,5 +1491,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1491
1491
|
landed since 1.19.525 with no explicit bump. run 33824700579. (task 1002620)
|
|
1492
1492
|
1.19.527 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1493
1493
|
landed since 1.19.526 with no explicit bump. run 33827382028. (task 1002620)
|
|
1494
|
+
1.19.528 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1495
|
+
landed since 1.19.527 with no explicit bump. run 33829339717. (task 1002620)
|
|
1494
1496
|
---------------------------------------------------------------------------
|
|
1495
1497
|
```
|
|
@@ -23,6 +23,25 @@ function runSubagent({ prompt, opts = {} }) {
|
|
|
23
23
|
// isn't named `claude` on PATH — covers any OS, not just Windows.
|
|
24
24
|
const bin = opts.claudeBin ?? process.env.OTB_CLAUDE_BIN ?? process.env.CLAUDE_BIN ?? 'claude';
|
|
25
25
|
|
|
26
|
+
// THE PERMISSION MODE IS ROOT-DEPENDENT (task 1003574). `bypassPermissions` maps
|
|
27
|
+
// to --dangerously-skip-permissions, and the CLI REFUSES that when euid is 0:
|
|
28
|
+
// "--dangerously-skip-permissions cannot be used with root/sudo privileges"
|
|
29
|
+
// It exits non-zero before doing any work, so under root EVERY worker returns
|
|
30
|
+
// SUBAGENT_NONZERO_EXIT and the whole panel dies. A root container is an ordinary
|
|
31
|
+
// CI/sandbox shape, not an exotic one — this was hit on a real ship (task 1003280).
|
|
32
|
+
//
|
|
33
|
+
// `acceptEdits` is the fallback because it is the closest mode the CLI accepts as
|
|
34
|
+
// root, and it grants this worker NOTHING extra: the tool list is `Read` alone, so
|
|
35
|
+
// there are no edits to accept. Verified as root that a worker under acceptEdits
|
|
36
|
+
// still reads files inside its cwd (which is all a grader worker ever does) while
|
|
37
|
+
// a path outside the project is refused without hanging — in -p there is no prompt
|
|
38
|
+
// to answer, so a denial returns rather than blocking.
|
|
39
|
+
//
|
|
40
|
+
// Non-root keeps bypassPermissions exactly as before: this narrows the mode only
|
|
41
|
+
// where the wider one cannot run at all.
|
|
42
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
43
|
+
const permissionMode = isRoot ? 'acceptEdits' : 'bypassPermissions';
|
|
44
|
+
|
|
26
45
|
const args = [
|
|
27
46
|
'-p',
|
|
28
47
|
'--print',
|
|
@@ -30,7 +49,7 @@ function runSubagent({ prompt, opts = {} }) {
|
|
|
30
49
|
'--output-format', 'json',
|
|
31
50
|
'--tools', 'Read',
|
|
32
51
|
'--disable-slash-commands',
|
|
33
|
-
'--permission-mode',
|
|
52
|
+
'--permission-mode', permissionMode,
|
|
34
53
|
];
|
|
35
54
|
|
|
36
55
|
const startedAt = Date.now();
|
|
@@ -183,6 +183,11 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
183
183
|
// If the panel were somehow advisory-only, fall back to gating on everyone so
|
|
184
184
|
// a lone Narc still counts (defensive — the default panel always has 3+
|
|
185
185
|
// gating workers).
|
|
186
|
+
// A pseudo-worker produced by a deterministic check rather than an LLM subagent
|
|
187
|
+
// (route-rank, permission-path — both stamp model:'deterministic'). It cannot be
|
|
188
|
+
// taken out by a grader outage, so it is not evidence either way about whether the
|
|
189
|
+
// panel ran. Task 1003574.
|
|
190
|
+
const isDeterministic = (w) => w && w.model === 'deterministic';
|
|
186
191
|
const nonAdvisory = workerResults.filter((w) => !isAdvisory(w));
|
|
187
192
|
const gating = nonAdvisory.length ? nonAdvisory : workerResults;
|
|
188
193
|
const gatingFail = gating.filter((w) => w.verdict === 'fail' && !isMissingSignal(w)).length;
|
|
@@ -202,7 +207,25 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
202
207
|
// is an infra/outage condition, NOT a 0/10 quality rejection. Distinguished
|
|
203
208
|
// from the partial-error case (some gating workers errored, others reported)
|
|
204
209
|
// so callers (ship.js) can present "grader unavailable" instead of a FAIL.
|
|
205
|
-
|
|
210
|
+
//
|
|
211
|
+
// TASK 1003574 — WHY THIS COUNTS ONLY THE SUBAGENT WORKERS. When #943 was written
|
|
212
|
+
// every gating worker was an LLM subagent, so "all of them are missing signal" and
|
|
213
|
+
// "the grader could not run" were the same sentence. They stopped being the same
|
|
214
|
+
// the moment the DETERMINISTIC pseudo-workers (route-rank, permission-path) were
|
|
215
|
+
// appended to workerResults: those never spawn a subprocess, so an outage cannot
|
|
216
|
+
// touch them, and a passing route-rank made `gating.every(isMissingSignal)`
|
|
217
|
+
// permanently false. The effect was that a TOTAL subagent outage could no longer
|
|
218
|
+
// reach this branch at all — it fell through to securityLensErrored and was
|
|
219
|
+
// reported as `FAIL · 0/10` with zero issues and every rubric axis null, which is
|
|
220
|
+
// indistinguishable from a real rejection and routes the builder to a re-grade
|
|
221
|
+
// that cannot clear it. Observed end-to-end on task 1003280.
|
|
222
|
+
//
|
|
223
|
+
// Excluding the deterministic checks here does NOT weaken the gate: a
|
|
224
|
+
// deterministic worker that FAILS is a real finding and is caught by `gatingFail`,
|
|
225
|
+
// which is the first branch and takes precedence over this one. All this says is
|
|
226
|
+
// that a deterministic check PASSING is not evidence the grader ran.
|
|
227
|
+
const subagentGating = gating.filter((w) => !isDeterministic(w));
|
|
228
|
+
const gatingErrorAll = subagentGating.length > 0 && subagentGating.every((w) => isMissingSignal(w));
|
|
206
229
|
// Advisory-worker outcomes — never a hard block. Uses isAdvisory so a Narc
|
|
207
230
|
// that DID raise a blocker is treated as gating (in `nonAdvisory`), not here.
|
|
208
231
|
const advisoryFail = workerResults.some((w) => isAdvisory(w) && w.verdict === 'fail' && !isMissingSignal(w));
|
|
@@ -77,10 +77,85 @@
|
|
|
77
77
|
// a throw is caught and treated as "no match", because a broken
|
|
78
78
|
// predicate must not stop the other rules.
|
|
79
79
|
// title(task) / body(task) the generated task's title and description.
|
|
80
|
-
// kind optional task kind;
|
|
80
|
+
// kind optional task kind; must be one the classifier knows
|
|
81
|
+
// (task-classifier.VALID_KINDS). Defaults to 'unclassified' —
|
|
82
|
+
// which is right for generated work anyway, because a request the
|
|
83
|
+
// board has not triaged has, precisely, no classification yet.
|
|
81
84
|
//
|
|
82
|
-
//
|
|
83
|
-
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// The first production rule (R04 of goal 1000074, task 1003115).
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
//
|
|
89
|
+
// THE ARTIST REVIEW. "Every shipped work with copy or visuals becomes a Review
|
|
90
|
+
// task for an artist, in two steps" (the criterion artist-review-task-on-ship).
|
|
91
|
+
// This is the rule that makes that true, and it is the whole of R04's build: the
|
|
92
|
+
// mechanism (ADR 0242) and its four safety properties already exist and are
|
|
93
|
+
// tested; R04 writes the one declaration.
|
|
94
|
+
//
|
|
95
|
+
// TWO GUARDS THAT KEEP IT FROM MISFIRING, both pinned in the tests:
|
|
96
|
+
// • It excludes `source === CASCADE_SOURCE`. Without that, the artist review
|
|
97
|
+
// task it generates — discipline 'artist', so it matches its own predicate —
|
|
98
|
+
// would on ship generate a review OF the review, and so on. The exclusion
|
|
99
|
+
// makes the generation a single hop, never a chain.
|
|
100
|
+
// • "Copy or visuals" is a NARROW, explicit predicate (hasCopyOrVisualWork),
|
|
101
|
+
// not "every ship". A cascade that fired on everything would bury the artist,
|
|
102
|
+
// and the criterion's "dismissable rather than nagging" would be a lie.
|
|
103
|
+
//
|
|
104
|
+
// It does NOT block anything (ADR 0241 draws the line: an artist may hold a
|
|
105
|
+
// DEPLOY, decided per project, never a builder's ship; that gate is a separate
|
|
106
|
+
// build). The generated task is a `backlog` request that triage may dismiss.
|
|
107
|
+
|
|
108
|
+
// The modules whose shipped work puts user-facing copy or visuals in front of a
|
|
109
|
+
// person. Kept as an explicit set rather than inferred, because "is this
|
|
110
|
+
// copy/visual work" is a judgement the rule should state out loud and a test
|
|
111
|
+
// should be able to pin — an inferred predicate is the kind that quietly widens.
|
|
112
|
+
const COPY_VISUAL_MODULES = Object.freeze([
|
|
113
|
+
'copy-desk', // the copy registry + proposals (goal 1000074's own spine)
|
|
114
|
+
'hall-ui', // the builders' hall — the most-seen surface
|
|
115
|
+
'art-pipeline', // generated visual assets
|
|
116
|
+
'status-ui', // the public status dashboard
|
|
117
|
+
'ui-design', // the design-system surfaces (ADR 0081)
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
// Does this shipped task's work put copy or visuals in front of a person? Pure
|
|
121
|
+
// and total — the runner treats a throw as "no match", but this must not throw.
|
|
122
|
+
function hasCopyOrVisualWork(task) {
|
|
123
|
+
if (!task) return false;
|
|
124
|
+
if (task.source === 'copy-proposal') return true; // a copy change that landed via the copy desk (ADR 0233)
|
|
125
|
+
if (task.discipline === 'artist') return true; // work an artist did is, by definition, on their surface
|
|
126
|
+
if (task.module_key && COPY_VISUAL_MODULES.includes(task.module_key)) return true;
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The two-step artist review body. STEP 1 coaches the grader (the artist trains
|
|
131
|
+
// the grading system, not only the artefact); STEP 2 remakes the copy/visuals
|
|
132
|
+
// with both routes explicitly offered — R04's brief, verbatim in intent.
|
|
133
|
+
function artistReviewBody(task) {
|
|
134
|
+
return [
|
|
135
|
+
`This is an ARTIST REVIEW, generated automatically because task ${task.id} put copy or visuals in front of a person. It is a REQUEST, not a gate — it blocks nothing that shipped, and triage may dismiss it.`,
|
|
136
|
+
'',
|
|
137
|
+
`STEP 1 — REVIEW THE GRADER, not only the work. Read how the grader judged task ${task.id}'s copy and visuals, and coach it: where was it right, and where did it miss what an artist would catch — slop, an off-voice line, a claim that does not land, a visual that reads as generated? The artist trains the grading system, not just the artefact, so this step is the one that compounds.`,
|
|
138
|
+
'',
|
|
139
|
+
'STEP 2 — REMAKE OR TWEAK the copy/visuals if they need it. Your choice of route, both offered on purpose: OUTSIDE the AI chat (edit the surface directly and land it through the copy desk or the normal pipeline), or WITH it (draft in chat, then land). Either way it lands as a claimed, graded, shipped task like everything else — there is no live CMS (goal 1000074\'s hard rule).',
|
|
140
|
+
'',
|
|
141
|
+
'If the copy and visuals are fine and the grader judged them well, DISMISS this in triage. That is a real, expected outcome — not a failure.',
|
|
142
|
+
].join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Production rules. The artist review is the first (R04); add another by
|
|
146
|
+
// appending an entry, never by touching the runner (ADR 0242).
|
|
147
|
+
const CASCADES = Object.freeze([
|
|
148
|
+
Object.freeze({
|
|
149
|
+
key: 'artist-review-on-ship',
|
|
150
|
+
discipline: 'artist',
|
|
151
|
+
kind: 'unclassified',
|
|
152
|
+
// The anti-recursion guard (source !== CASCADE_SOURCE) is FIRST so a
|
|
153
|
+
// cascade-generated task is rejected before any other test runs.
|
|
154
|
+
when: (t) => t.source !== CASCADE_SOURCE && hasCopyOrVisualWork(t),
|
|
155
|
+
title: (t) => `Artist review: the copy and visuals that shipped in task ${t.id}`,
|
|
156
|
+
body: artistReviewBody,
|
|
157
|
+
}),
|
|
158
|
+
]);
|
|
84
159
|
|
|
85
160
|
// The (source, source_ref) pair the generated task is keyed on. `source` is a
|
|
86
161
|
// constant so every cascade-generated row is findable as a class; the ref pins
|
|
@@ -171,7 +246,11 @@ function cascadeTaskSpec(rule, task) {
|
|
|
171
246
|
title: String(rule.title(task) || '').trim(),
|
|
172
247
|
description: `${preamble}\n\n${body}`,
|
|
173
248
|
discipline: rule.discipline,
|
|
174
|
-
|
|
249
|
+
// 'unclassified', not 'chore' (task 1003115): the cascade calls db.createTask
|
|
250
|
+
// directly, bypassing the route's kind validation, and 'chore' is not in
|
|
251
|
+
// task-classifier.VALID_KINDS — it would have written an invalid kind. A
|
|
252
|
+
// generated request the board has not triaged is genuinely unclassified.
|
|
253
|
+
kind: rule.kind || 'unclassified',
|
|
175
254
|
status: 'backlog',
|
|
176
255
|
source: CASCADE_SOURCE,
|
|
177
256
|
sourceRef: cascadeSourceRef(rule.key, task.id),
|
|
@@ -229,6 +308,9 @@ async function runCascadesForShip(shipped, deps = {}) {
|
|
|
229
308
|
module.exports = {
|
|
230
309
|
CASCADES,
|
|
231
310
|
CASCADE_SOURCE,
|
|
311
|
+
COPY_VISUAL_MODULES,
|
|
312
|
+
hasCopyOrVisualWork,
|
|
313
|
+
artistReviewBody,
|
|
232
314
|
cascadeSourceRef,
|
|
233
315
|
assertRuleShape,
|
|
234
316
|
validateCascades,
|
|
@@ -185,12 +185,69 @@ await test('the production table is valid, and a duplicate key is refused', () =
|
|
|
185
185
|
assert.ok(shape.some((p) => /body/.test(p)));
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
// --- the first production rule: the artist review (R04, task 1003115) --------
|
|
189
|
+
|
|
190
|
+
const shippedCopy = (over = {}) => ({
|
|
191
|
+
id: '7001', version_id: 'BONGOS-V1', goal_id: '1000074',
|
|
192
|
+
title: 'Reword the hall empty states', discipline: 'engineer',
|
|
193
|
+
kind: 'feature', status: 'shipped', module_key: 'hall-ui', source: 'manual',
|
|
194
|
+
...over,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await test('the artist review is the one production rule, keyed and disciplined', () => {
|
|
198
|
+
assert.equal(cascade.CASCADES.length, 1);
|
|
199
|
+
const r = cascade.CASCADES[0];
|
|
200
|
+
assert.equal(r.key, 'artist-review-on-ship');
|
|
201
|
+
assert.equal(r.discipline, 'artist', 'the review is FOR the artist, whatever role shipped the work');
|
|
202
|
+
assert.equal(r.kind, 'unclassified', 'a valid kind — not the invalid "chore" the spec used to default to');
|
|
203
|
+
assert.deepEqual(cascade.validateCascades(), [], 'and the shipped table is well-formed');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await test('a copy/visual ship generates the two-step artist review', () => {
|
|
207
|
+
const { matched } = cascade.cascadesFor(shippedCopy());
|
|
208
|
+
assert.equal(matched.length, 1);
|
|
209
|
+
const spec = matched[0].spec;
|
|
210
|
+
assert.equal(spec.discipline, 'artist');
|
|
211
|
+
assert.equal(spec.kind, 'unclassified');
|
|
212
|
+
assert.equal(spec.status, 'backlog', 'a request that triage may dismiss — not claimable, not nagging');
|
|
213
|
+
assert.match(spec.title, /7001/);
|
|
214
|
+
// Both steps, and both routes of step 2, are in the body.
|
|
215
|
+
assert.match(spec.description, /STEP 1 — REVIEW THE GRADER/);
|
|
216
|
+
assert.match(spec.description, /STEP 2 — REMAKE OR TWEAK/);
|
|
217
|
+
assert.match(spec.description, /OUTSIDE the AI chat/);
|
|
218
|
+
assert.match(spec.description, /WITH it/);
|
|
219
|
+
assert.match(spec.description, /DISMISS this in triage/);
|
|
220
|
+
assert.match(spec.description, /blocks nothing that shipped/, 'it says out loud that it is not a gate');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await test('"copy or visuals" is a NARROW predicate — three ways in, and a default of no', () => {
|
|
224
|
+
const H = cascade.hasCopyOrVisualWork;
|
|
225
|
+
assert.equal(H(shippedCopy({ module_key: 'copy-desk' })), true);
|
|
226
|
+
assert.equal(H(shippedCopy({ module_key: 'art-pipeline' })), true);
|
|
227
|
+
assert.equal(H(shippedCopy({ module_key: null, source: 'copy-proposal' })), true, 'a landed copy proposal');
|
|
228
|
+
assert.equal(H(shippedCopy({ module_key: null, source: 'manual', discipline: 'artist' })), true, 'work an artist did');
|
|
229
|
+
// The default is NO — a backend/infra ship is not an artist review.
|
|
230
|
+
assert.equal(H(shippedCopy({ module_key: 'lifecycle', discipline: 'engineer', source: 'manual' })), false);
|
|
231
|
+
assert.equal(H(shippedCopy({ module_key: null, discipline: 'engineer', source: 'manual' })), false);
|
|
232
|
+
assert.equal(H(null), false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await test('THE ANTI-RECURSION GUARD: an artist review shipping does NOT generate a review of itself', () => {
|
|
236
|
+
// The generated review is discipline:'artist' + source:'cascade'. Its own
|
|
237
|
+
// predicate (discipline artist) would match — so without the source guard it
|
|
238
|
+
// would cascade forever. This is the test that proves it is a single hop.
|
|
239
|
+
const reviewTaskThatShipped = shippedCopy({
|
|
240
|
+
discipline: 'artist', source: cascade.CASCADE_SOURCE, module_key: null, id: '7100',
|
|
241
|
+
});
|
|
242
|
+
const { matched } = cascade.cascadesFor(reviewTaskThatShipped);
|
|
243
|
+
assert.equal(matched.length, 0, 'a cascade-generated task never triggers another cascade');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await test('the review inherits the shipped task goal and version, not goal 1000074 by fiat', () => {
|
|
247
|
+
// A copy ship in some other goal gets its review filed next to that work.
|
|
248
|
+
const { matched } = cascade.cascadesFor(shippedCopy({ goal_id: '1000046', version_id: 'BONGOS-V1' }));
|
|
249
|
+
assert.equal(matched[0].spec.goalId, 1000046);
|
|
250
|
+
assert.equal(matched[0].spec.versionId, 'BONGOS-V1');
|
|
194
251
|
});
|
|
195
252
|
|
|
196
253
|
console.log(`\n${passed} passed, ${failed} failed`);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.528",
|
|
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.528",
|
|
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.528",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.528'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// tests/grader_root_outage.mjs — a grader that could not START must say so, and it
|
|
2
|
+
// must be able to start as root (task 1003574, goal 1000024).
|
|
3
|
+
//
|
|
4
|
+
// THE BUG THIS PINS. #943 added a 'unavailable' panel outcome for a total grader
|
|
5
|
+
// outage, so a builder sees "grader unavailable" instead of a FAIL nobody produced.
|
|
6
|
+
// It worked when every gating worker was an LLM subagent. Then the DETERMINISTIC
|
|
7
|
+
// pseudo-workers (route-rank, permission-path) were appended to the panel — they
|
|
8
|
+
// never spawn a subprocess, so an outage cannot touch them, and one of them passing
|
|
9
|
+
// made `gating.every(isMissingSignal)` permanently false. The outage branch became
|
|
10
|
+
// unreachable: a total subagent outage fell through to the securityLensErrored branch
|
|
11
|
+
// and was recorded as `FAIL · 0/10` with zero issues and every rubric axis null.
|
|
12
|
+
//
|
|
13
|
+
// That shape is indistinguishable from a real rejection in a terminal, and it routes
|
|
14
|
+
// a builder to /grade-recover Branch A — whose prescribed re-grade cannot clear an
|
|
15
|
+
// outage that is deterministic. Observed end-to-end on task 1003280, whose ship was
|
|
16
|
+
// blocked twice by a panel that never ran.
|
|
17
|
+
//
|
|
18
|
+
// Run: node --test tests/grader_root_outage.mjs
|
|
19
|
+
import { strict as assert } from 'node:assert';
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const grader = require(path.join(ROOT, 'modules', 'grading', 'grader.js'));
|
|
29
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
|
|
30
|
+
|
|
31
|
+
// An LLM worker whose subprocess died — no signal at all.
|
|
32
|
+
const errored = (kind) => ({
|
|
33
|
+
kind, name: kind, specialty: 'x', verdict: 'error', scores: {}, issues: [],
|
|
34
|
+
notes_md: '', confidence: 0, model: 'claude-sonnet-5', run_ms: 10,
|
|
35
|
+
error: 'SUBAGENT_NONZERO_EXIT',
|
|
36
|
+
});
|
|
37
|
+
// The deterministic pseudo-worker, exactly as route-rank-check.js stamps it.
|
|
38
|
+
const deterministicPass = (kind = 'route-rank') => ({
|
|
39
|
+
kind, name: kind, specialty: 'x', verdict: 'pass', scores: {}, issues: [],
|
|
40
|
+
notes_md: `${kind} pass`, confidence: 1, model: 'deterministic', run_ms: 0, error: null,
|
|
41
|
+
});
|
|
42
|
+
const llmPass = (kind) => ({
|
|
43
|
+
kind, name: kind, specialty: 'x', verdict: 'pass', scores: { code_quality: 8 }, issues: [],
|
|
44
|
+
notes_md: '', confidence: 0.8, model: 'claude-sonnet-5', run_ms: 10, error: null,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('the exact task-1003280 shape: every subagent dead, route-rank passing → unavailable, not fail', () => {
|
|
48
|
+
const r = grader.aggregatePanel([
|
|
49
|
+
errored('narc'), errored('quality'), errored('hacker'), errored('efficiency'),
|
|
50
|
+
deterministicPass('route-rank'),
|
|
51
|
+
]);
|
|
52
|
+
assert.equal(r.signals.panel_outcome, 'unavailable',
|
|
53
|
+
'a passing deterministic check is not evidence the panel ran');
|
|
54
|
+
assert.equal(r.passed, false, 'unavailable never auto-passes — it routes to manual-confirm');
|
|
55
|
+
assert.match(r.notes_md, /GRADER UNAVAILABLE/);
|
|
56
|
+
// The tell that made this so hard to read in a terminal: no findings at all.
|
|
57
|
+
assert.equal((r.issues || []).length, 0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('two deterministic pseudo-workers do not resurrect the fail either', () => {
|
|
61
|
+
const r = grader.aggregatePanel([
|
|
62
|
+
errored('narc'), errored('quality'), errored('hacker'), errored('efficiency'),
|
|
63
|
+
deterministicPass('route-rank'), deterministicPass('permission-path'),
|
|
64
|
+
]);
|
|
65
|
+
assert.equal(r.signals.panel_outcome, 'unavailable');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a DETERMINISTIC FAIL still hard-blocks — the fix does not soften the gate', () => {
|
|
69
|
+
const detFail = { ...deterministicPass('route-rank'), verdict: 'fail',
|
|
70
|
+
issues: [{ severity: 'blocker', file: 'r.js', summary: 'ungated write route' }] };
|
|
71
|
+
const r = grader.aggregatePanel([
|
|
72
|
+
errored('narc'), errored('quality'), errored('hacker'), errored('efficiency'), detFail,
|
|
73
|
+
]);
|
|
74
|
+
assert.equal(r.signals.panel_outcome, 'fail',
|
|
75
|
+
'gatingFail is the first branch and must still win over the outage branch');
|
|
76
|
+
assert.equal(r.passed, false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a real security-lens outage with the rest of the panel alive still hard-blocks (#574 intact)', () => {
|
|
80
|
+
const r = grader.aggregatePanel([
|
|
81
|
+
llmPass('narc'), llmPass('quality'), errored('hacker'), llmPass('efficiency'),
|
|
82
|
+
deterministicPass('route-rank'),
|
|
83
|
+
]);
|
|
84
|
+
assert.equal(r.signals.panel_outcome, 'fail', 'a missing Hacker among live workers is still a block');
|
|
85
|
+
assert.match(r.notes_md, /SECURITY LENS MISSING/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a healthy panel is unaffected', () => {
|
|
89
|
+
const r = grader.aggregatePanel([
|
|
90
|
+
llmPass('narc'), llmPass('quality'), llmPass('hacker'), llmPass('efficiency'),
|
|
91
|
+
deterministicPass('route-rank'),
|
|
92
|
+
]);
|
|
93
|
+
assert.equal(r.signals.panel_outcome, 'pass');
|
|
94
|
+
assert.equal(r.passed, true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---- the spawn half -------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
test('the worker spawn picks a permission mode the CLI accepts as root', () => {
|
|
100
|
+
const src = read('modules', 'grading', 'grader-subagent.js');
|
|
101
|
+
// The refusal is euid-based, so the choice must be too.
|
|
102
|
+
assert.match(src, /process\.getuid\(\) === 0/);
|
|
103
|
+
assert.match(src, /isRoot \? 'acceptEdits' : 'bypassPermissions'/);
|
|
104
|
+
// The flag must be passed as the computed value, never the hardcoded wide one.
|
|
105
|
+
assert.match(src, /'--permission-mode', permissionMode/);
|
|
106
|
+
assert.doesNotMatch(src, /'--permission-mode', 'bypassPermissions'/);
|
|
107
|
+
// The tool list is what makes acceptEdits cost nothing: there are no edits.
|
|
108
|
+
assert.match(src, /'--tools', 'Read'/);
|
|
109
|
+
});
|