@bongos/core 1.19.919 → 1.19.921
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 +73 -53
- package/.claude/skills/grade-audit/SKILL.md +9 -0
- package/docs/adr/0320-an-empty-diff-is-not-an-ungraded-diff.md +159 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/grading/grader-no-artifact.js +347 -0
- package/modules/grading/grader.js +6 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +17 -1
- package/scripts/gds/ship-flow.js +54 -7
- package/scripts/gds/ship-grade-steps.js +43 -5
- package/scripts/gds/ship-grade.js +36 -0
- package/src/bongos/routes/me.js +9 -1
- package/src/module-api.js +1 -1
- package/tests/core_update_banner.mjs +28 -0
- package/tests/git_merge_regen.mjs +87 -3
- package/tests/grader_no_artifact.mjs +360 -0
- package/tests/grader_prompt_fencing.mjs +46 -0
- package/tests/nav_permission_atoms.mjs +110 -0
- package/tests/no_artifact_escalation.mjs +13 -3
- package/tests/skip_is_not_pass.mjs +91 -1
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
|
|
188
188
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
189
189
|
chrome exists before the page script runs. -->
|
|
190
|
-
<script src="/builders/shell.js?v=2026-
|
|
190
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
191
191
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
192
192
|
<script src="/builders/palette.js" defer></script>
|
|
193
193
|
<!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
|
|
59
59
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
60
60
|
|
|
61
|
-
<script src="/builders/shell.js?v=2026-
|
|
61
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
62
62
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
63
63
|
<script src="/builders/palette.js" defer></script>
|
|
64
64
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
70
70
|
chrome exists before the page script runs. -->
|
|
71
|
-
<script src="/builders/shell.js?v=2026-
|
|
71
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
72
72
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
73
73
|
<script src="/builders/palette.js" defer></script>
|
|
74
74
|
<!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
43
43
|
|
|
44
|
-
<script src="/builders/shell.js?v=2026-
|
|
44
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
45
45
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
46
46
|
<script src="/builders/palette.js" defer></script>
|
|
47
47
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
52
52
|
chrome exists before the page script runs. -->
|
|
53
|
-
<script src="/builders/shell.js?v=2026-
|
|
53
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
54
54
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
55
55
|
<script src="/builders/palette.js" defer></script>
|
|
56
56
|
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
|
|
100
100
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
101
101
|
chrome exists before the page script runs. -->
|
|
102
|
-
<script src="/builders/shell.js?v=2026-
|
|
102
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
103
103
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
104
104
|
<script src="/builders/palette.js" defer></script>
|
|
105
105
|
<!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
|
|
@@ -416,7 +416,7 @@
|
|
|
416
416
|
|
|
417
417
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
418
418
|
chrome exists before the page scripts run. -->
|
|
419
|
-
<script src="/builders/shell.js?v=2026-
|
|
419
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
420
420
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
421
421
|
<script src="/builders/palette.js" defer></script>
|
|
422
422
|
<!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
|
|
67
67
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
68
68
|
|
|
69
|
-
<script src="/builders/shell.js?v=2026-
|
|
69
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
70
70
|
<script src="/builders/palette.js" defer></script>
|
|
71
71
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
72
72
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
59
59
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
60
|
-
<script src="/builders/shell.js?v=2026-
|
|
60
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
61
61
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
62
62
|
<script src="/builders/palette.js" defer></script>
|
|
63
63
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
|
|
163
163
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
164
164
|
chrome exists before the page script runs. -->
|
|
165
|
-
<script src="/builders/shell.js?v=2026-
|
|
165
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
166
166
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
167
167
|
<script src="/builders/palette.js" defer></script>
|
|
168
168
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
|
|
152
152
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
153
153
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
154
|
-
<script src="/builders/shell.js?v=2026-
|
|
154
|
+
<script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
|
|
155
155
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
156
156
|
<script src="/builders/palette.js" defer></script>
|
|
157
157
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.921",
|
|
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.921",
|
|
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.921",
|
|
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",
|
|
@@ -231,9 +231,25 @@ const CLEAN_VERDICT_RE = /\b[1-9]\d* passed, 0 failed(?:, \d+ skipped)?\s*$/;
|
|
|
231
231
|
// line so a suite that prints skips without a summary suffix is still surfaced.
|
|
232
232
|
const SKIP_LINE_RE = /^\s*SKIP\b/;
|
|
233
233
|
|
|
234
|
+
// node:test suites print no SKIP line at all — they report `ℹ skipped N` in their own summary
|
|
235
|
+
// (task 1004112). Until this, every skip in a node:test suite was INVISIBLE to the roll-up whose
|
|
236
|
+
// entire purpose is to say "a skipped case is not a passing one": the suite printed `ok`, the
|
|
237
|
+
// roll-up counted nothing, and the unrun coverage vanished without a trace.
|
|
238
|
+
//
|
|
239
|
+
// Found while fixing this counter's own subject. Gating four unevaluable cases in
|
|
240
|
+
// tests/skip_is_not_pass.mjs turned four honest FAILURES into four invisible skips — the 1003993
|
|
241
|
+
// defect reappearing one layer up, in the file written to prevent it. A skip that nothing counts
|
|
242
|
+
// is a pass with extra steps.
|
|
243
|
+
const NODE_TEST_SKIP_RE = /^\s*(?:ℹ|#)\s*skipped\s+(\d+)\s*$/m;
|
|
244
|
+
|
|
234
245
|
function countSkips(stdout) {
|
|
235
246
|
if (!stdout) return 0;
|
|
236
|
-
|
|
247
|
+
const lines = stdout.split('\n').filter((l) => SKIP_LINE_RE.test(l)).length;
|
|
248
|
+
// The two formats are mutually exclusive in practice (a suite is either the project's own
|
|
249
|
+
// harness or node:test), but SUM rather than branch: a suite that grew both would otherwise
|
|
250
|
+
// report whichever we happened to check first, and under-reporting is the failure mode here.
|
|
251
|
+
const m = stdout.match(NODE_TEST_SKIP_RE);
|
|
252
|
+
return lines + (m ? Number(m[1]) : 0);
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
function isCleanTeardownAbort(stdout, stderr) {
|
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -74,6 +74,11 @@ async function shipMain() {
|
|
|
74
74
|
// task 1003950: set only when the empty-diff guard below classifies this ship as a
|
|
75
75
|
// deliberate no-artifact close. Gates the auto-escalation at the confirm wall.
|
|
76
76
|
let noArtifactClose = false;
|
|
77
|
+
// task 1004011: this ship has no committed diff, so the grade's subject is the
|
|
78
|
+
// builder's CLAIM (notes + value summary) rather than a diff. Distinct from
|
|
79
|
+
// noArtifactClose, which marks the older "reached the archon gate" case: a ship
|
|
80
|
+
// can now be no-artifact and still fully graded, which is the point.
|
|
81
|
+
let noArtifactGrade = false;
|
|
77
82
|
// task 1002605 (G-18): explicit opt-in to continue past a FAILED production
|
|
78
83
|
// smoke run. Without it a smoke FAIL now ABORTS before the claim resolves
|
|
79
84
|
// (claim intact); with it, the resolve proceeds and the task deliberately
|
|
@@ -267,16 +272,45 @@ async function shipMain() {
|
|
|
267
272
|
process.exit(1);
|
|
268
273
|
}
|
|
269
274
|
// verdict.reason === 'allow-empty': a deliberate no-artifact / DB-only ship.
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
275
|
+
//
|
|
276
|
+
// task 1004011 (ADR 0320): this used to force `skipGrade = true` — no grade at
|
|
277
|
+
// all, then the Archon-only `task.confirm.grade_bypass` gate. Both halves were
|
|
278
|
+
// wrong. An empty diff is not an UNGRADED diff, it is a diff the grader has
|
|
279
|
+
// nothing to say about, so it should not need the key that ratifies past a
|
|
280
|
+
// FAILING grade. And something here does deserve adversarial scrutiny: not the
|
|
281
|
+
// absent diff, but the CLAIM that nothing was needed. "The premise was already
|
|
282
|
+
// fixed", "this was a pure ops verification" are assertions the ledger records
|
|
283
|
+
// and, until now, nothing checked.
|
|
284
|
+
//
|
|
285
|
+
// So the grader is no longer skipped — it is re-pointed at the notes. A pass is
|
|
286
|
+
// a REAL passing grade and confirms through the ordinary rank-open path; a fail
|
|
287
|
+
// is a real fail, and the builder fixes the evidence rather than reaching for a
|
|
288
|
+
// permission. skipGrade stays false; only an explicit --skip-grade still skips.
|
|
289
|
+
// Set only when a grade will actually run. With --skip-grade (or the server-side
|
|
290
|
+
// grader_bypass, which forces skipGrade above) the `if (!skipGrade)` gate below
|
|
291
|
+
// skips grading entirely and this flag would be inert — but an inert flag makes a
|
|
292
|
+
// reader trace two of them to see why nothing happened, and invites a later
|
|
293
|
+
// refactor to treat it as "this ship graded its notes" when it graded nothing.
|
|
294
|
+
noArtifactGrade = !skipGrade;
|
|
273
295
|
// task 1003950: mark WHY the grader is being skipped. A --skip-grade ship past a
|
|
274
296
|
// failing grade and a genuinely artifact-free close both reach the same archon
|
|
275
297
|
// gate below, but only the second one may be escalated automatically — the first
|
|
276
298
|
// has a diff the grader DID read and reject, and auto-filing a blocker for it
|
|
277
299
|
// would be a builder routing their own rejected work to an override queue.
|
|
300
|
+
//
|
|
301
|
+
// Still set, and still needed: since task 1004011 this close reaches the archon
|
|
302
|
+
// gate only when the builder ALSO passed --skip-grade, or the server bypassed
|
|
303
|
+
// the grader for them. Those are the cases where the escalation split still
|
|
304
|
+
// decides which refusal is printed.
|
|
278
305
|
noArtifactClose = true;
|
|
279
|
-
console.log('--allow-empty: no committed diff (no-artifact / DB-only ship)
|
|
306
|
+
console.log('--allow-empty: no committed diff (no-artifact / DB-only ship).');
|
|
307
|
+
if (noArtifactGrade) {
|
|
308
|
+
console.log(' The grader is not skipped — it is re-pointed: your handoff notes and value summary are graded');
|
|
309
|
+
console.log(' against the task description, so the claim that nothing was needed is what gets checked (task 1004011).');
|
|
310
|
+
} else {
|
|
311
|
+
console.log(' The grader is skipped (--skip-grade / server bypass), so the claim is NOT checked — this close');
|
|
312
|
+
console.log(' routes to the archon confirm gate. Drop --skip-grade to have your notes graded instead.');
|
|
313
|
+
}
|
|
280
314
|
}
|
|
281
315
|
|
|
282
316
|
// ---- Module drift check (BV1.R53, task 1422) ----
|
|
@@ -411,10 +445,15 @@ async function shipMain() {
|
|
|
411
445
|
if (!skipGrade) {
|
|
412
446
|
const tFetch = await (await cliClient()).tasks.getTasksId({ id: taskId });
|
|
413
447
|
fullTaskForGrade = tFetch.ok ? tFetch.data.task : { id: taskId, title: ac.title };
|
|
414
|
-
console.log(
|
|
448
|
+
console.log(noArtifactGrade
|
|
449
|
+
? 'Starting the no-artifact grader over your notes (overlaps the smoke run — task 1002660)...'
|
|
450
|
+
: 'Starting the quality grader panel (overlaps the smoke run — task 1002660)...');
|
|
415
451
|
gradeCompute = computeGradeResult({
|
|
416
452
|
taskId, task: fullTaskForGrade, summary,
|
|
417
453
|
baseline: graderBaseline, committedFiles,
|
|
454
|
+
// task 1004011: present ⇒ grade the claim, not the (empty) diff. The notes
|
|
455
|
+
// are the evidence, so they have to travel with it.
|
|
456
|
+
noArtifact: noArtifactGrade ? { notes } : null,
|
|
418
457
|
});
|
|
419
458
|
gradeCompute.catch(() => {}); // an abort path must not die on an unhandled rejection
|
|
420
459
|
}
|
|
@@ -613,12 +652,16 @@ async function shipMain() {
|
|
|
613
652
|
gradePassed = true; // by builder fiat, not by grader
|
|
614
653
|
} else if (passed && taskStatus === 'completed') {
|
|
615
654
|
console.log('');
|
|
616
|
-
console.log(
|
|
655
|
+
console.log(noArtifactGrade
|
|
656
|
+
? 'Running the no-artifact grader (subject: your claim that nothing was needed)...'
|
|
657
|
+
: 'Running subagent quality grader...');
|
|
617
658
|
|
|
618
659
|
// #513 + #536: the grader reads COMMITTED history (baseline...HEAD), not
|
|
619
660
|
// the working tree. The empty-committed-diff guard now runs earlier — BEFORE
|
|
620
661
|
// the claim is resolved (see above) — so by the time we reach here the diff
|
|
621
|
-
// is guaranteed non-empty on the grader path
|
|
662
|
+
// is guaranteed non-empty on the grader path, EXCEPT on the task 1004011
|
|
663
|
+
// no-artifact species, where it is guaranteed empty and the notes are the
|
|
664
|
+
// subject instead. Reuse the baseline + file list
|
|
622
665
|
// computed there so we don't diff twice. #678: gradeAndRecord POSTs the
|
|
623
666
|
// result and logs the panel cost — the same unit the --regrade path reuses.
|
|
624
667
|
// task 1002660: the panel computation was started BEFORE the smoke gate
|
|
@@ -631,6 +674,10 @@ async function shipMain() {
|
|
|
631
674
|
baseline: graderBaseline,
|
|
632
675
|
committedFiles,
|
|
633
676
|
precomputed: gradeCompute,
|
|
677
|
+
// No `noArtifact` here on purpose: `precomputed` is ALWAYS set on this path, and
|
|
678
|
+
// gradeAndRecord only falls back to computing inline when it is absent. Passing it would be
|
|
679
|
+
// unreachable by construction — defensive symmetry that reads like a live argument. The
|
|
680
|
+
// species reaches the grader through gradeCompute above, which was built with it.
|
|
634
681
|
});
|
|
635
682
|
|
|
636
683
|
if (!gradePost.ok) {
|
|
@@ -19,7 +19,7 @@ const { plainCardLines } = require('./plain-cards.js');
|
|
|
19
19
|
// so the grader outage explains itself with the same `claude CLI` verdict doctor
|
|
20
20
|
// gives, and the two can never drift.
|
|
21
21
|
const { preflightChecks } = require('./preflight.js');
|
|
22
|
-
const { computeRawDiffHash, reconstructPanelIssues, runSubagentGrader, shouldReuseGrade } = require('./ship-grade.js');
|
|
22
|
+
const { computeRawDiffHash, reconstructPanelIssues, runNoArtifactGrader, runSubagentGrader, shouldReuseGrade } = require('./ship-grade.js');
|
|
23
23
|
|
|
24
24
|
// ---------- Shared grade-step helpers (used by shipMain AND regradeMain) ----------
|
|
25
25
|
//
|
|
@@ -168,7 +168,41 @@ function buildPriorRoundContext({ storedGrade, responseText = '', interimCommits
|
|
|
168
168
|
// writes, so shipMain can start it before the smoke gate and overlap the
|
|
169
169
|
// multi-minute panel with smoke + the claim resolve. Returns
|
|
170
170
|
// { kind: 'reuse', currentHash, reusedResult } | { kind: 'fresh', currentHash, gradeResult }.
|
|
171
|
-
|
|
171
|
+
// `runNoArtifact` / `runDiffGrader` are injection seams, defaulted to the real
|
|
172
|
+
// runners. The routing decision and the diff-hash policy below are the parts worth
|
|
173
|
+
// testing, and BOTH real runners spawn a model — a test that had to spawn one to
|
|
174
|
+
// reach them would be slow, billable and non-deterministic, so it would not be
|
|
175
|
+
// written, so they would not be covered. Both seams exist because the negative case
|
|
176
|
+
// matters as much as the positive one: "an ordinary ship must NOT reach the
|
|
177
|
+
// no-artifact grader" is only assertable if the ordinary path can be reached
|
|
178
|
+
// without paying for a panel. Production passes neither.
|
|
179
|
+
async function computeGradeResult({ taskId, task, summary, baseline, committedFiles, priorRound = null, noArtifact = null, runNoArtifact = runNoArtifactGrader, runDiffGrader = runSubagentGrader }) {
|
|
180
|
+
// task 1004011: the no-artifact species. There is no diff, so every step below
|
|
181
|
+
// this is inapplicable rather than merely empty — the hash is of nothing, the
|
|
182
|
+
// reuse lookup would match any other empty-diff grade on this task, and the
|
|
183
|
+
// panel has no subject. Return early with the claim graded instead.
|
|
184
|
+
//
|
|
185
|
+
// `noArtifact` carries the notes, because they are the EVIDENCE here and this
|
|
186
|
+
// function is otherwise handed only the diff-derived inputs.
|
|
187
|
+
//
|
|
188
|
+
// `priorRound` is DELIBERATELY not threaded through. It is the delta-aware re-grade
|
|
189
|
+
// context (task 1002662): the prior round's findings, the builder's response, and
|
|
190
|
+
// the INTERIM DIFF between the previously graded HEAD and now. On a no-artifact
|
|
191
|
+
// ship there is no interim diff by construction, and --regrade refuses an empty
|
|
192
|
+
// diff anyway (task 1003950), so there is no path that reaches here with a
|
|
193
|
+
// meaningful prior round. Passing it would look like remediation context while
|
|
194
|
+
// carrying nothing that applies. Named here so the omission reads as a decision.
|
|
195
|
+
if (noArtifact) {
|
|
196
|
+
const gradeResult = await runNoArtifact({
|
|
197
|
+
task, valueSummary: summary, notes: noArtifact.notes,
|
|
198
|
+
});
|
|
199
|
+
// currentHash stays null on purpose: diff_hash is the reuse key for a grade
|
|
200
|
+
// over a diff, and storing a hash of the empty diff would let the NEXT
|
|
201
|
+
// empty-diff ship of this task reuse THIS grade — a different claim,
|
|
202
|
+
// different notes, silently inheriting a pass it never earned.
|
|
203
|
+
return { kind: 'fresh', currentHash: null, gradeResult };
|
|
204
|
+
}
|
|
205
|
+
|
|
172
206
|
// Compute the fingerprint of the raw (untruncated) diff so we can compare
|
|
173
207
|
// against any stored grade, independent of the display-truncation cap.
|
|
174
208
|
const currentHash = computeRawDiffHash(baseline);
|
|
@@ -205,7 +239,7 @@ async function computeGradeResult({ taskId, task, summary, baseline, committedFi
|
|
|
205
239
|
}
|
|
206
240
|
}
|
|
207
241
|
|
|
208
|
-
const gradeResult = await
|
|
242
|
+
const gradeResult = await runDiffGrader({
|
|
209
243
|
task,
|
|
210
244
|
valueSummary: summary,
|
|
211
245
|
baseline,
|
|
@@ -225,9 +259,13 @@ async function computeGradeResult({ taskId, task, summary, baseline, committedFi
|
|
|
225
259
|
// (a promise from computeGradeResult, task 1002660) lets the ship path hand in
|
|
226
260
|
// the overlapped computation; the --regrade path passes nothing and computes
|
|
227
261
|
// inline — behavior identical to the pre-split gradeAndRecord either way.
|
|
228
|
-
async function gradeAndRecord({ taskId, task, summary, baseline, committedFiles, priorRound = null, precomputed = null }) {
|
|
262
|
+
async function gradeAndRecord({ taskId, task, summary, baseline, committedFiles, priorRound = null, precomputed = null, noArtifact = null }) {
|
|
263
|
+
// task 1004011: forwarded so the two entry points cannot diverge. The ship path
|
|
264
|
+
// hands in `precomputed` (the overlapped promise) and never reaches the inline
|
|
265
|
+
// call; anything computing inline must be able to reach the no-artifact species
|
|
266
|
+
// too, or the next caller silently grades an empty diff with the panel.
|
|
229
267
|
const computed = await (precomputed
|
|
230
|
-
|| computeGradeResult({ taskId, task, summary, baseline, committedFiles, priorRound }));
|
|
268
|
+
|| computeGradeResult({ taskId, task, summary, baseline, committedFiles, priorRound, noArtifact }));
|
|
231
269
|
|
|
232
270
|
if (computed.kind === 'reuse') {
|
|
233
271
|
const payload = { ...buildGradePayload(computed.reusedResult, committedFiles), diff_hash: computed.currentHash };
|
|
@@ -344,6 +344,41 @@ async function runSubagentGrader({ task, valueSummary, baseline, changedFiles: p
|
|
|
344
344
|
return result;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
// task 1004011: the no-artifact twin of runSubagentGrader. Same station in the
|
|
348
|
+
// ship, different subject — there is no diff, so the thing graded is the builder's
|
|
349
|
+
// CLAIM that none was needed (modules/grading/grader-no-artifact.js, ADR 0320).
|
|
350
|
+
//
|
|
351
|
+
// WHY a separate runner rather than a flag on the one above: every input that one
|
|
352
|
+
// assembles is diff-derived (changed files, the unified diff, the shortstat, the
|
|
353
|
+
// generated-provenance check, the deterministic route-rank and permission-path
|
|
354
|
+
// pre-passes over the changed files). On an empty diff each of those is an empty
|
|
355
|
+
// set, and an empty set fed to a check that reports "no hits" is a check that
|
|
356
|
+
// passes vacuously — the exact failure mode task 1002646's Guard 1b exists to
|
|
357
|
+
// catch server-side. Threading `if (empty)` through all of them would be a wider
|
|
358
|
+
// change with more places to get that wrong than a sibling that never computes
|
|
359
|
+
// them at all.
|
|
360
|
+
async function runNoArtifactGrader({ task, valueSummary, notes, runOpts: extraOpts = {} }) {
|
|
361
|
+
const builderModel = resolveBuilderModel();
|
|
362
|
+
const graderModel = grader.selectGraderModel({ builderModel });
|
|
363
|
+
const runOpts = {
|
|
364
|
+
timeoutMs: GRADER_TIMEOUT_MS,
|
|
365
|
+
cwd: process.cwd(), // grade from the FEATURE worktree — the Read tool is the only evidence check here
|
|
366
|
+
...extraOpts,
|
|
367
|
+
};
|
|
368
|
+
console.log(` subagent grader ${grader.VERSION}: NO-ARTIFACT grade (empty committed diff) — one generalist shot over the handoff notes + value summary, model=${graderModel}`);
|
|
369
|
+
console.log(` subject: the CLAIM that nothing was needed, judged against the task description — not a diff, and not the absence of one`);
|
|
370
|
+
console.log(` gate: unchanged — functional_fidelity>=${grader.THRESHOLD} AND code_quality>=${grader.THRESHOLD} AND confidence>=0.4, re-asked for this species (is it genuinely artifact-free / is the evidence checkable)`);
|
|
371
|
+
const t0 = Date.now();
|
|
372
|
+
const result = await grader.gradeNoArtifact({ task, valueSummary, notes, builderModel, runOpts });
|
|
373
|
+
result.signals = {
|
|
374
|
+
...(result.signals || {}),
|
|
375
|
+
wall_ms: Date.now() - t0,
|
|
376
|
+
changed_files_count: 0,
|
|
377
|
+
graded_head_sha: gitOk('git rev-parse HEAD') || null,
|
|
378
|
+
};
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
|
|
347
382
|
function runSmoke() {
|
|
348
383
|
// `cwd: REPO_ROOT` + a relative script path is robust to spaces or other
|
|
349
384
|
// shell metacharacters that may appear in the absolute repo path on some
|
|
@@ -376,6 +411,7 @@ module.exports = {
|
|
|
376
411
|
diffHash,
|
|
377
412
|
reconstructPanelIssues,
|
|
378
413
|
resolveBaseRef,
|
|
414
|
+
runNoArtifactGrader,
|
|
379
415
|
runSmoke,
|
|
380
416
|
runSmokeAsync,
|
|
381
417
|
runSubagentGrader,
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -119,7 +119,15 @@ const discordIsLinked = (builderId) =>
|
|
|
119
119
|
// COSMETIC ONLY (ADR 0016). This decides who is OFFERED a link. The page gate
|
|
120
120
|
// (auth.js PAGE_GATES) and each data route's requirePermission are the wall, and
|
|
121
121
|
// they run regardless of what this says.
|
|
122
|
-
|
|
122
|
+
// `core.pin.move` (task 1004118). The core-update banner in shell.js gates on it,
|
|
123
|
+
// and task 1004045 shipped that gate WITHOUT adding the atom here — so the value was
|
|
124
|
+
// `undefined` for every builder, the guard always bailed, and the banner could not
|
|
125
|
+
// render for anyone, on any instance. The authority was never the problem: the same
|
|
126
|
+
// account that saw no banner is answered 200 by a `core.pin.move`-gated route. Only
|
|
127
|
+
// this projection omitted it. Adding an atom here is the deliberate act this comment
|
|
128
|
+
// already described; tests/nav_permission_atoms.mjs now fails if shell.js gates on
|
|
129
|
+
// one that is missing, so the act cannot be skipped again.
|
|
130
|
+
const NAV_PERMISSION_ATOMS = Object.freeze(['board.vote.cast', 'core.pin.move']);
|
|
123
131
|
|
|
124
132
|
// navPermissionsFor(builderId) → { '<atom>': boolean } over NAV_PERMISSION_ATOMS,
|
|
125
133
|
// or null when authority could not be established (government off, port missing,
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.921'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -306,3 +306,31 @@ test('the banner is asked for ONCE, not on every access settle', async () => {
|
|
|
306
306
|
const first = b.requested.filter((r) => r.endsWith('/core-update')).length;
|
|
307
307
|
assert.equal(first, 1);
|
|
308
308
|
});
|
|
309
|
+
|
|
310
|
+
// ── the chain, with the map the SERVER would really send (task 1004118) ───────
|
|
311
|
+
//
|
|
312
|
+
// Every case above hands applyAccess a permissions map written here. That is exactly
|
|
313
|
+
// how task 1004045's banner shipped dead: it gated on `core.pin.move`, /me resolved
|
|
314
|
+
// only NAV_PERMISSION_ATOMS, the atom was not in it, and the guard bailed for every
|
|
315
|
+
// builder on every instance — while this suite stayed green throughout.
|
|
316
|
+
//
|
|
317
|
+
// So this case does NOT write the map. It DERIVES it from me.js's own declaration,
|
|
318
|
+
// the way the server does, and grants everything in it. If the atom the banner needs
|
|
319
|
+
// is not part of that projection, there is nothing here to grant and the banner
|
|
320
|
+
// cannot render — which is the real bug, reproduced as a failure.
|
|
321
|
+
test('the banner renders from the projection /me ACTUALLY sends, not one written by this test', async () => {
|
|
322
|
+
const meSrc = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/me.js'), 'utf8');
|
|
323
|
+
const m = meSrc.match(/NAV_PERMISSION_ATOMS = Object\.freeze\(\[([^\]]*)\]\)/);
|
|
324
|
+
assert.ok(m, 'NAV_PERMISSION_ATOMS is readable');
|
|
325
|
+
const atoms = m[1].split(',').map((x) => x.trim().replace(/^'|'$/g, '')).filter(Boolean);
|
|
326
|
+
// A builder who holds everything the projection can report — the most generous real
|
|
327
|
+
// answer the server is capable of giving.
|
|
328
|
+
const permissions = Object.fromEntries(atoms.map((a) => [a, true]));
|
|
329
|
+
const b = boot({ permissions, update: BEHIND });
|
|
330
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
331
|
+
assert.ok(
|
|
332
|
+
b.banner(),
|
|
333
|
+
'the banner did not render for a builder holding EVERY atom /me can report — so the atom it ' +
|
|
334
|
+
'gates on is outside NAV_PERMISSION_ATOMS and the feature is dead for everyone (task 1004118)',
|
|
335
|
+
);
|
|
336
|
+
});
|
|
@@ -57,6 +57,38 @@ const AC = require('../scripts/gds/gen-api-client.js'); // task 1003537 — the
|
|
|
57
57
|
|
|
58
58
|
const MARKERS = /^(<{7}|={7}|>{7})/m;
|
|
59
59
|
|
|
60
|
+
// driverConfigValue — the ONE spelling of `merge.otb-regen.driver` these fixtures install
|
|
61
|
+
// (task 1004112). Three call sites built it by hand, which is how half a fix stayed half a fix.
|
|
62
|
+
//
|
|
63
|
+
// Two separate ways this string is mangled before node sees it, and `git config` is responsible
|
|
64
|
+
// for only one — the value is a COMMAND LINE, parsed by a shell:
|
|
65
|
+
//
|
|
66
|
+
// 1. BACKSLASHES. `git config` strips them from the value, so a win32 path.join() path arrives
|
|
67
|
+
// as `C:Users…` — drive-relative, which node resolves against the merging repo's cwd, i.e.
|
|
68
|
+
// the scratch dir. That is the `Cannot find module 'C:\…\Temp\otb-gitmerge-*\Users…'` shape
|
|
69
|
+
// in docs/recipes/windows-builders.md. Fixed by task 1003546's forward slashes, kept here.
|
|
70
|
+
//
|
|
71
|
+
// 2. SPACES — NOT previously handled, and the reason this fixture reds on some Windows boxes and
|
|
72
|
+
// passes on others. Unquoted, `node C:/Users/Some Name/repo/scripts/…` splits at the space
|
|
73
|
+
// and node is handed a truncated path. Fully DETERMINISTIC for any builder whose checkout
|
|
74
|
+
// path contains a space, and invisible to every builder whose path does not — which is why
|
|
75
|
+
// windows-builders.md recorded the symptom as "seen once, then not reproducing in any of
|
|
76
|
+
// three trees" and warned against baselining it. Three trees on three space-free paths agree
|
|
77
|
+
// with each other and say nothing about a fourth.
|
|
78
|
+
// Verified both ways in a scratch repo: unquoted → merge exit 1, `Auto-merging …`,
|
|
79
|
+
// MODULE_NOT_FOUND; quoted → exit 0 and the driver heals the file.
|
|
80
|
+
//
|
|
81
|
+
// The quotes are the fix. The forward slashes stay, because git config still eats backslashes
|
|
82
|
+
// inside them.
|
|
83
|
+
function driverConfigValue(driverPath) {
|
|
84
|
+
// The quoting is only as good as what it cannot be closed by. These paths are always
|
|
85
|
+
// locally-generated os.tmpdir() fixtures, never attacker-influenced — but a helper whose whole
|
|
86
|
+
// job is "survive a hostile-looking path" should not itself break on one, and the next caller
|
|
87
|
+
// will not re-derive that the input is trusted. `"` is the one character that ends the quote.
|
|
88
|
+
const quoted = driverPath.replace(/\\/g, '/').replace(/"/g, '\\"');
|
|
89
|
+
return `node "${quoted}" %O %A %B %P`;
|
|
90
|
+
}
|
|
91
|
+
|
|
60
92
|
// ── 1) docs/repo-map.md regenerates clean, ignoring the conflicted sides ────────
|
|
61
93
|
{
|
|
62
94
|
const r = regen.regenerateResolved('docs/repo-map.md', {
|
|
@@ -215,7 +247,7 @@ function withBlockBody(src, body) {
|
|
|
215
247
|
// Forward slashes: `git config` strips backslashes from the value, so a win32
|
|
216
248
|
// path.join() driver path reaches node with its separators gone and the driver
|
|
217
249
|
// never loads — this fixture was silently dead on Windows (task 1003546).
|
|
218
|
-
g('config', 'merge.otb-regen.driver',
|
|
250
|
+
g('config', 'merge.otb-regen.driver', driverConfigValue(driver));
|
|
219
251
|
fs.writeFileSync(path.join(tmp, '.gitattributes'), 'docs/repo-map.md merge=otb-regen\n');
|
|
220
252
|
fs.mkdirSync(path.join(tmp, 'docs'));
|
|
221
253
|
writeMap('base\n'); g('add', '-A'); g('commit', '-qm', 'base');
|
|
@@ -237,6 +269,58 @@ function withBlockBody(src, body) {
|
|
|
237
269
|
}
|
|
238
270
|
}
|
|
239
271
|
|
|
272
|
+
// ── 6b) the driver config survives a path with SPACES (task 1004112) ──────────────
|
|
273
|
+
//
|
|
274
|
+
// Case 6 above only proves the driver loads from THIS builder's checkout path. On a path with no
|
|
275
|
+
// space it passes whether or not the value is quoted — which is exactly how the quoting stayed
|
|
276
|
+
// missing while three trees agreed the fixture was fine.
|
|
277
|
+
//
|
|
278
|
+
// So assert the property directly, on a path this box may not have: point the driver at a
|
|
279
|
+
// directory whose name contains a space and run a real merge through it. The scratch repo is ours,
|
|
280
|
+
// so the space is guaranteed wherever the repo itself lives.
|
|
281
|
+
//
|
|
282
|
+
// NB this is a bare block, like every other case in this file — written as a `function` it would
|
|
283
|
+
// be defined, never called, and permanently green over nothing.
|
|
284
|
+
{
|
|
285
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-gitmerge-spaced-'));
|
|
286
|
+
try {
|
|
287
|
+
const spaced = path.join(tmp, 'a dir with spaces');
|
|
288
|
+
fs.mkdirSync(spaced);
|
|
289
|
+
// A stand-in driver: this case is about the ARGV plumbing reaching a driver at all, not about
|
|
290
|
+
// what the real one decides. Using the real one would re-test case 6.
|
|
291
|
+
const drv = path.join(spaced, 'driver.js');
|
|
292
|
+
fs.writeFileSync(drv, 'require("fs").writeFileSync(process.argv[3], "HEALED\\n");\n');
|
|
293
|
+
|
|
294
|
+
const work = path.join(tmp, 'repo');
|
|
295
|
+
fs.mkdirSync(work);
|
|
296
|
+
const g = (...a) => spawnSync('git', a, { cwd: work, encoding: 'utf8' });
|
|
297
|
+
g('init', '-q');
|
|
298
|
+
g('config', 'user.email', 't@t'); g('config', 'user.name', 't');
|
|
299
|
+
g('config', 'core.autocrlf', 'false');
|
|
300
|
+
g('config', 'merge.otb-regen.name', 'regen');
|
|
301
|
+
g('config', 'merge.otb-regen.driver', driverConfigValue(drv));
|
|
302
|
+
fs.writeFileSync(path.join(work, '.gitattributes'), 'f.md merge=otb-regen\n');
|
|
303
|
+
|
|
304
|
+
const write = (s) => fs.writeFileSync(path.join(work, 'f.md'), s);
|
|
305
|
+
write('base\n'); g('add', '-A'); g('commit', '-qm', 'base');
|
|
306
|
+
g('checkout', '-qb', 'feature');
|
|
307
|
+
write('FEATURE\n'); g('add', '-A'); g('commit', '-qm', 'f');
|
|
308
|
+
g('checkout', '-q', 'master');
|
|
309
|
+
if (g('rev-parse', '--verify', 'master').status !== 0) g('checkout', '-q', 'main');
|
|
310
|
+
write('MASTER\n'); g('add', '-A'); g('commit', '-qm', 'm');
|
|
311
|
+
|
|
312
|
+
const merge = g('merge', '--no-edit', 'feature');
|
|
313
|
+
assert.equal(merge.status, 0,
|
|
314
|
+
'a driver path containing a space must still resolve — unquoted, the shell splits it and node '
|
|
315
|
+
+ 'is handed a truncated path (MODULE_NOT_FOUND), which is deterministic for any builder whose '
|
|
316
|
+
+ `checkout path has a space. got ${merge.status} :: ${merge.stdout}${merge.stderr}`);
|
|
317
|
+
assert.equal(fs.readFileSync(path.join(work, 'f.md'), 'utf8'), 'HEALED\n',
|
|
318
|
+
'the driver must have actually RUN, not merely failed to break the merge');
|
|
319
|
+
} finally {
|
|
320
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
240
324
|
// ── 7) .gitattributes' merge=otb-regen lines and REGENERABLE agree (task 1003543) ─
|
|
241
325
|
// The whole scheme is two hand-maintained lists naming the same files (one in
|
|
242
326
|
// .gitattributes, one in REGENERABLE) — nothing checked that they still matched, so
|
|
@@ -320,7 +404,7 @@ function withBlockBody(src, body) {
|
|
|
320
404
|
// Forward slashes: `git config` strips backslashes from the value, so a win32
|
|
321
405
|
// path.join() driver path reaches node with its separators gone and the driver
|
|
322
406
|
// never loads — this fixture was silently dead on Windows (task 1003546).
|
|
323
|
-
g('config', 'merge.otb-regen.driver',
|
|
407
|
+
g('config', 'merge.otb-regen.driver', driverConfigValue(driver));
|
|
324
408
|
fs.writeFileSync(path.join(tmp, '.gitattributes'), 'docs/copy-registry.json merge=otb-regen\n');
|
|
325
409
|
fs.mkdirSync(path.join(tmp, 'docs'));
|
|
326
410
|
writeReg('{"schema":0}\n'); g('add', '-A'); g('commit', '-qm', 'base');
|
|
@@ -386,7 +470,7 @@ function withBlockBody(src, body) {
|
|
|
386
470
|
g('config', 'core.autocrlf', 'false');
|
|
387
471
|
g('config', 'merge.otb-regen.name', 'regen');
|
|
388
472
|
g('config', 'merge.otb-regen.driver',
|
|
389
|
-
|
|
473
|
+
driverConfigValue(path.join(work, 'scripts/gds/git-merge-regen.js')));
|
|
390
474
|
|
|
391
475
|
if (!fs.existsSync(scannedFile())) skip = `${SCANNED} is gone — repoint this fixture at another scanned surface`;
|
|
392
476
|
else if (fs.readFileSync(scannedFile(), 'utf8').split('\n').length < 420) skip = `${SCANNED} is too short for two distant edits`;
|