@bongos/core 1.19.1032 → 1.19.1034

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.
Files changed (151) hide show
  1. package/.bongos-core.json +322 -117
  2. package/.claude/skills/builder-start/SKILL.md +1 -1
  3. package/.claude/skills/grade-audit/SKILL.md +1 -1
  4. package/.claude/skills/grade-recover/SKILL.md +2 -1
  5. package/.claude/skills/grader-health/SKILL.md +5 -1
  6. package/.claude/skills/merge-mode/SKILL.md +1 -1
  7. package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
  8. package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
  9. package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
  10. package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
  11. package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
  12. package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
  13. package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
  14. package/docs/adr/README.md +4 -4
  15. package/docs/api/openapi.json +1 -1
  16. package/docs/architecture.md +4 -3
  17. package/docs/canonical-permissions.md +2 -2
  18. package/docs/copy-inventory.md +34 -30
  19. package/docs/copy-registry.json +69 -33
  20. package/docs/file-map.md +4 -3
  21. package/docs/module-api-changelog.md +4 -0
  22. package/docs/recipes/autobongos-windows-host.md +3 -1
  23. package/docs/recipes/local-dev.md +45 -3
  24. package/docs/recipes/packaging-the-core.md +3 -1
  25. package/docs/recipes/redteam-patrol.md +2 -2
  26. package/docs/recipes/upgrading-the-core.md +1 -1
  27. package/modules/autonomy/cadence.js +52 -0
  28. package/modules/autonomy/gauge.js +57 -9
  29. package/modules/government/protected-surfaces.json +1 -0
  30. package/modules/grading/grade-engine.js +267 -0
  31. package/modules/grading/grader-subagent.js +4 -3
  32. package/modules/grading/grader-workers/index.js +67 -11
  33. package/modules/grading/grader-workers/worker-verdict.js +32 -2
  34. package/modules/grading/grader.js +109 -88
  35. package/modules/hall-ui/public/deploy.js +35 -0
  36. package/modules/hall-ui/public/project-settings.css +7 -0
  37. package/modules/hall-ui/records/builders-roster.md +10 -0
  38. package/modules/hall-ui/records/catalog.md +10 -0
  39. package/modules/hall-ui/records/collaboration.md +14 -0
  40. package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
  41. package/modules/hall-ui/records/oversight-family.md +19 -0
  42. package/modules/hall-ui/records/panel-family.md +19 -0
  43. package/modules/hall-ui/records/reading-rooms.md +12 -0
  44. package/modules/hall-ui/records/record-family.md +13 -0
  45. package/modules/hall-ui/records/reference-room.md +9 -0
  46. package/modules/hall-ui/records/roster-and-ledger.md +9 -0
  47. package/modules/hall-ui/records/sky.md +23 -0
  48. package/modules/lifecycle/dependency-advisory.js +2 -1
  49. package/modules/lifecycle/routes/tasks.js +19 -4
  50. package/modules/lifecycle/ship-preflight.js +91 -0
  51. package/modules/provisioning/control-plane-guard.js +14 -13
  52. package/modules/provisioning/core-upgrade.js +30 -17
  53. package/modules/provisioning/public-refusal.js +44 -0
  54. package/modules/provisioning/routes/disconnect.js +2 -1
  55. package/modules/provisioning/routes/env-manifest.js +5 -4
  56. package/modules/provisioning/routes/provisioning.js +16 -15
  57. package/modules/provisioning/routes/repo-private.js +2 -1
  58. package/package-lock.json +2 -2
  59. package/package.json +1 -1
  60. package/release-notes.json +7481 -0
  61. package/scripts/gds/address-park-sweep.sh +3 -1
  62. package/scripts/gds/autobongos-run.js +91 -3
  63. package/scripts/gds/backfill-cost.js +20 -9
  64. package/scripts/gds/baseline-staleness.js +1 -1
  65. package/scripts/gds/claim.js +2 -2
  66. package/scripts/gds/credential-presence.js +281 -0
  67. package/scripts/gds/db-backup-nightly.sh +9 -4
  68. package/scripts/gds/db-backup-offsite.sh +4 -2
  69. package/scripts/gds/deploy-main-audit-gate.sh +11 -4
  70. package/scripts/gds/dev-lib.js +212 -5
  71. package/scripts/gds/dev.js +102 -50
  72. package/scripts/gds/doctor.js +3 -2
  73. package/scripts/gds/grade-correlation-audit.js +280 -0
  74. package/scripts/gds/grade-replay.js +237 -0
  75. package/scripts/gds/hooks/pre-push.js +7 -3
  76. package/scripts/gds/main-audit.js +19 -10
  77. package/scripts/gds/memory-map.js +3 -1
  78. package/scripts/gds/onboarding-config.js +3 -2
  79. package/scripts/gds/package-core.js +72 -1
  80. package/scripts/gds/plain-cards.js +4 -0
  81. package/scripts/gds/preflight.js +12 -4
  82. package/scripts/gds/provision-core-upgrade.js +282 -80
  83. package/scripts/gds/provision-repo-auth.js +15 -0
  84. package/scripts/gds/provision.js +1 -1
  85. package/scripts/gds/publish-credential-check.js +3 -0
  86. package/scripts/gds/push-main.js +14 -3
  87. package/scripts/gds/push-path-brief.js +3 -2
  88. package/scripts/gds/redteam-patrol.js +4 -4
  89. package/scripts/gds/release-notes.js +271 -0
  90. package/scripts/gds/run-routine.js +18 -5
  91. package/scripts/gds/run-unit-tests.js +14 -3
  92. package/scripts/gds/seed-board-room-tasks.js +1 -1
  93. package/scripts/gds/session-cost.js +12 -3
  94. package/scripts/gds/ship-deploy-target.js +11 -6
  95. package/scripts/gds/ship-flow.js +5 -3
  96. package/scripts/gds/ship-grade-steps.js +33 -0
  97. package/scripts/gds/ship-land.js +241 -16
  98. package/scripts/gds/ship-merge.js +3 -2
  99. package/scripts/gds/ship-preflight-steps.js +86 -11
  100. package/scripts/gds/ship.js +1 -1
  101. package/scripts/gds/stray-unit-sweep.sh +3 -1
  102. package/scripts/gds/upgrade-migrate.js +67 -0
  103. package/scripts/gds/upgrade.js +9 -7
  104. package/scripts/hall-preview/README.md +20 -3
  105. package/scripts/hall-preview/live-identity.js +1 -1
  106. package/scripts/hall-preview/refresh-fixtures.js +71 -11
  107. package/scripts/hall-preview/scrub-identity.js +296 -0
  108. package/src/bongos/routes/security.js +3 -3
  109. package/src/build-info.js +4 -2
  110. package/src/module-api.js +1 -1
  111. package/tests/adr_0089_scope.mjs +70 -0
  112. package/tests/adr_0162_citation.mjs +184 -0
  113. package/tests/autobongos_cadence.mjs +74 -0
  114. package/tests/autobongos_loop.mjs +114 -0
  115. package/tests/autonomy_gauge.mjs +56 -0
  116. package/tests/consumer_layout_boot.mjs +363 -31
  117. package/tests/core_upgrade_door.mjs +40 -6
  118. package/tests/core_upgrade_runner.mjs +329 -5
  119. package/tests/credential_presence.mjs +153 -0
  120. package/tests/db_backup_nightly.mjs +21 -3
  121. package/tests/db_backup_offsite.mjs +81 -0
  122. package/tests/deploy_audit_gate.mjs +5 -2
  123. package/tests/deploy_page_projects.mjs +63 -2
  124. package/tests/dev_lib.mjs +2 -1
  125. package/tests/dev_windows.mjs +324 -0
  126. package/tests/env_knobs_neutral_prefix.mjs +203 -0
  127. package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
  128. package/tests/government_protected_surfaces.mjs +10 -0
  129. package/tests/grade_correlation_audit.mjs +159 -0
  130. package/tests/grade_engine.mjs +249 -0
  131. package/tests/grade_engine_replay.mjs +99 -0
  132. package/tests/grader_prompt_window_bound.mjs +132 -0
  133. package/tests/grader_unbacked_score.mjs +194 -0
  134. package/tests/hall_preview_scrub.mjs +214 -0
  135. package/tests/hall_records.mjs +85 -0
  136. package/tests/helpers.mjs +25 -0
  137. package/tests/instance_db_migrated_on_code_move.mjs +8 -1
  138. package/tests/integration_parked_ratchet.mjs +24 -1
  139. package/tests/plain_cards.mjs +1 -1
  140. package/tests/provisioning_public_refusal.mjs +195 -0
  141. package/tests/publish_status_branch.mjs +128 -0
  142. package/tests/redteam_patrol.mjs +1 -1
  143. package/tests/release_notes.mjs +327 -0
  144. package/tests/shell_stat_portability.mjs +49 -0
  145. package/tests/ship_land_conflict.mjs +181 -0
  146. package/tests/ship_land_red_pr.mjs +236 -0
  147. package/tests/ship_preflight.mjs +226 -3
  148. package/tests/start_env_guard.mjs +16 -0
  149. package/tests/sweep_app_account.mjs +15 -2
  150. package/tests/upgrade.mjs +20 -1
  151. package/tests/upgrade_migrate.mjs +195 -0
@@ -0,0 +1,249 @@
1
+ // tests/grade_engine.mjs — the central grading engine (task 1002839, ADR 0089 §1).
2
+ //
3
+ // Pins the four things the extraction promised: (1) the pipeline is NAMED and
4
+ // runs in the ADR's order; (2) there is a registry with two doors — the grading
5
+ // module's own work-types and any other module's, through the seam port
6
+ // grader.workType.<key> — and the code panel is registered in it as the default;
7
+ // (3) the deterministic pre-passes and the veto belong to the ENGINE, so a
8
+ // work-type whose own aggregate would pass a protected-path fail still fails;
9
+ // (4) the code panel graded through the engine is wired exactly as gradeSubagent
10
+ // was — same judges, models, timeouts, prompts and record — including through the
11
+ // real default model call (a fake `claude` binary; POSIX only).
12
+ //
13
+ // Run: node --test tests/grade_engine.mjs
14
+
15
+ import { test } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import { createRequire } from 'node:module';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const grader = require('../modules/grading/grader.js');
24
+ const engine = grader.gradeEngine;
25
+ const api = require('../src/module-api');
26
+ const llmCache = require('../src/bongos/llm-cache.js');
27
+
28
+ const TASK = { id: 4242, title: 'Fixture task', description: 'Add helper X. Acceptance: X exported.', kind: 'feature', discipline: 'engineer', version_id: 'BONGOS-V2', est_minutes: 60 };
29
+ const DIFF = 'diff --git a/modules/example/a.js b/modules/example/a.js\n--- a/modules/example/a.js\n+++ b/modules/example/a.js\n@@ -1 +1 @@\n-const a = 1;\n+const a = 2; // changed value here\n';
30
+ const DS = { files_changed: 1, lines_added: 1, lines_removed: 1 };
31
+
32
+ // Which judge a prompt belongs to — every worker prompt opens with its identity.
33
+ const KIND_MARKS = [['**Narc**', 'narc'], ['**Quality**', 'quality'], ['**Hacker**', 'hacker'], ['**Efficiency Monger**', 'efficiency'], ['**Reader**', 'reader']];
34
+ const kindOf = (prompt) => (KIND_MARKS.find(([m]) => prompt.slice(0, 400).includes(m)) || [null, 'unknown'])[1];
35
+ const PASSING = {
36
+ narc: { functional_fidelity: 9, confidence: 0.8, issues: [], ac_checklist: [{ ac: 'X exported', status: 'satisfied' }] },
37
+ quality: { code_quality: 8, economy: 8, confidence: 0.8, issues: [] },
38
+ hacker: { security_score: 9, confidence: 0.8, issues: [] },
39
+ efficiency: { efficiency_score: 9, confidence: 0.8, issues: [] },
40
+ };
41
+ const envelope = (payload) => JSON.stringify({ type: 'result', result: JSON.stringify(payload), total_cost_usd: 0.01 });
42
+
43
+ // An injected model call that records what each judge was asked.
44
+ function recordingRunner(replies = PASSING) {
45
+ const calls = [];
46
+ const run = async ({ prompt, opts }) => {
47
+ const kind = kindOf(prompt);
48
+ calls.push({ kind, prompt, opts });
49
+ return { stdout: envelope(replies[kind] || {}), stderr: '', run_ms: 1, exit_code: 0, cost_usd: 0.01 };
50
+ };
51
+ return { run, calls };
52
+ }
53
+
54
+ // A work-type with every hook, whose aggregate PASSES no matter what it is handed —
55
+ // the shape a careless or hostile module could register.
56
+ function alwaysPassWorkType(seen = {}) {
57
+ return {
58
+ key: 'always-pass',
59
+ resolveJudges: () => ['judge-a'],
60
+ modelFor: () => 'model-x',
61
+ runJudge: async (judge, ctx) => { seen.ctx = ctx; return { kind: judge, verdict: 'pass', scores: {}, issues: [], confidence: 1, model: 'model-x' }; },
62
+ aggregate: (results) => { seen.results = results; return { passed: true, score: 10, signals: { panel_outcome: 'pass', confidence: 1 }, notes_md: 'always pass', issues: [] }; },
63
+ unavailable: ({ reason }) => ({ passed: false, signals: { panel_outcome: 'unavailable', reason } }),
64
+ };
65
+ }
66
+
67
+ test('the pipeline is named, in ADR 0089 §1 order', () => {
68
+ assert.deepEqual(engine.PIPELINE_STAGES, ['select', 'pre_pass', 'evidence', 'fan_out', 'aggregate', 'gate', 'persist']);
69
+ });
70
+
71
+ test('the code panel is registered as work-type #1 and is the default', () => {
72
+ assert.ok(engine.listWorkTypes().includes('code'));
73
+ const code = engine.resolveWorkType({ workType: 'code' });
74
+ assert.equal(code.key, 'code');
75
+ assert.equal(code.aggregate, grader.aggregatePanel, 'the code work-type aggregates with the v4 panel gate');
76
+ assert.equal(code.unavailable, grader.graderUnavailableResult);
77
+ assert.equal(code.cacheTaskType, 'grade_worker', 'kept at the pre-engine value so cached worker replies stay hits');
78
+ // Everything that is not a registered key lands on the default rather than
79
+ // throwing inside the grade step (the task 1002659 posture).
80
+ assert.equal(engine.resolveWorkType({}), code);
81
+ assert.equal(engine.resolveWorkType({ workType: 'no-such-type' }), code);
82
+ assert.equal(engine.resolveWorkType({ task: { discipline: 'engineer', kind: 'feature' } }), code);
83
+ });
84
+
85
+ test('registerWorkType refuses a malformed or conflicting work-type at registration', () => {
86
+ assert.throws(() => engine.registerWorkType({ ...alwaysPassWorkType(), key: 'Bad Key' }), /must match/);
87
+ const { aggregate, ...noAggregate } = alwaysPassWorkType();
88
+ assert.throws(() => engine.registerWorkType({ ...noAggregate, key: 'half-built' }), /missing hook\(s\): aggregate/);
89
+ assert.throws(() => engine.registerWorkType({ ...alwaysPassWorkType(), key: 'bad-optional', gatherEvidence: 'not a function' }), /gatherEvidence/);
90
+ const code = engine.resolveWorkType({ workType: 'code' });
91
+ assert.equal(engine.registerWorkType(code), code, 'the same definition again is a no-op');
92
+ assert.throws(() => engine.registerWorkType({ ...code }), /already registered/, 'a second definition for a key is a wiring bug');
93
+ });
94
+
95
+ test('another module contributes a work-type through the seam port, and a broken one degrades to the default', () => {
96
+ const def = { ...alwaysPassWorkType(), key: 'test-seam-art' };
97
+ api.registerProvider(engine.workTypePort('test-seam-art'), def);
98
+ assert.equal(engine.resolveWorkType({ task: { discipline: 'test-seam-art' } }), def, 'resolved by the task discipline');
99
+ assert.ok(engine.listWorkTypes().includes('test-seam-art'));
100
+
101
+ api.registerProvider(engine.workTypePort('test-seam-broken'), { key: 'test-seam-broken', aggregate: () => ({}) });
102
+ const logged = [];
103
+ const orig = console.error;
104
+ console.error = (...a) => { logged.push(a.join(' ')); };
105
+ try {
106
+ assert.equal(engine.resolveWorkType({ workType: 'test-seam-broken' }).key, 'code');
107
+ } finally { console.error = orig; }
108
+ assert.match(logged.join('\n'), /ignoring invalid work-type on port grader\.workType\.test-seam-broken/);
109
+ assert.ok(!engine.listWorkTypes().includes('test-seam-broken'));
110
+
111
+ // Tests reset the seam registry; the grading module's own default must survive it.
112
+ require('../src/module-seams')._reset();
113
+ assert.equal(engine.resolveWorkType({}).key, 'code', 'a seam reset cannot lose the default work-type');
114
+ assert.deepEqual(engine.listWorkTypes(), ['code']);
115
+ });
116
+
117
+ test('the ENGINE owns the pre-passes: a work-type that never names them still gets them, in order', async () => {
118
+ const seen = {};
119
+ await engine.runGrade({
120
+ workType: alwaysPassWorkType(seen), task: TASK, diff: DIFF, diffStats: DS,
121
+ changedFiles: ['src/bongos/routes/x.js', 'src/bongos/auth.js'], builderRank: 'thetes',
122
+ });
123
+ assert.deepEqual(seen.results.map((r) => r.kind), ['judge-a', 'route-rank', 'permission-path'],
124
+ 'deterministic pseudo-judges join after the model judges, route-rank first');
125
+ assert.ok(seen.ctx.deterministicFindings.length > 0, 'the judges are handed the pre-pass findings');
126
+ });
127
+
128
+ test('the gate: no verdict upgrades a deterministic fail — whatever the work-type aggregate said', async () => {
129
+ const r = await engine.runGrade({
130
+ workType: alwaysPassWorkType(), task: TASK, diff: DIFF, diffStats: DS,
131
+ changedFiles: ['src/bongos/auth.js'], builderRank: 'xenos',
132
+ });
133
+ assert.equal(r.passed, false);
134
+ assert.equal(r.signals.panel_outcome, 'fail');
135
+ assert.equal(r.signals.confidence, 0, 'forwarded confidence 0 → the server gate fails it too');
136
+ assert.deepEqual(r.signals.deterministic_veto, ['permission-path']);
137
+ assert.match(r.notes_md, /DETERMINISTIC VETO/);
138
+
139
+ // The veto enforces the deterministic checks and nothing else: with no
140
+ // protected path touched, the work-type's own pass stands.
141
+ const clean = await engine.runGrade({
142
+ workType: alwaysPassWorkType(), task: TASK, diff: DIFF, diffStats: DS,
143
+ changedFiles: ['modules/example/a.js'], builderRank: 'xenos',
144
+ });
145
+ assert.equal(clean.passed, true);
146
+ assert.equal(clean.signals.deterministic_veto, undefined);
147
+ });
148
+
149
+ test('replay rebuilds the pre-passes from the recorded pseudo-judges, so its gate judges what the live gate judged', () => {
150
+ const recorded = [
151
+ { kind: 'judge-a', verdict: 'pass', scores: {}, issues: [], model: 'model-x' },
152
+ { kind: 'permission-path', verdict: 'fail', scores: {}, issues: [{ severity: 'blocker', file: 'src/bongos/auth.js', summary: 'x' }], model: 'deterministic' },
153
+ ];
154
+ const r = engine.replayRecorded({ signals: { workers: recorded }, workType: alwaysPassWorkType() });
155
+ assert.equal(r.passed, false);
156
+ assert.deepEqual(r.signals.deterministic_veto, ['permission-path']);
157
+ // A judge merely NAMED like a pre-pass, but produced by a model, is not one.
158
+ const forged = [{ ...recorded[1], model: 'claude-sonnet-5' }];
159
+ assert.equal(engine.replayRecorded({ signals: { workers: forged }, workType: alwaysPassWorkType() }).passed, true);
160
+ });
161
+
162
+ test('the gate returns an already-failed aggregate UNTOUCHED — why it is a no-op for the code panel', () => {
163
+ const failed = { passed: false, signals: { panel_outcome: 'fail', confidence: 0 }, notes_md: 'x' };
164
+ const prePass = { findings: [], workerResult: { kind: 'permission-path', verdict: 'fail', model: 'deterministic' } };
165
+ assert.equal(engine.applyDeterministicGate(failed, [prePass]), failed);
166
+ const passed = { passed: true, signals: { panel_outcome: 'pass' } };
167
+ assert.equal(engine.applyDeterministicGate(passed, []), passed, 'nothing to enforce → the same object');
168
+ });
169
+
170
+ test('the code panel through the engine: same judges, models, timeouts, prompts and record as gradeSubagent', async () => {
171
+ const { run, calls } = recordingRunner();
172
+ const prior = { round: 2, findings_md: '- [major] x', response_md: 'fixed in abc', interim_md: '1 file' };
173
+ const r = await engine.runGrade({
174
+ workType: 'code', task: TASK, valueSummary: 'Adds X.', diff: DIFF, diffStats: DS,
175
+ changedFiles: ['src/bongos/auth.js'], builderRank: 'xenos', priorRound: prior,
176
+ runOpts: { cwd: process.cwd(), timeoutMs: 1234 },
177
+ }, { runSubagent: run });
178
+
179
+ assert.deepEqual(calls.map((c) => c.kind).sort(), ['efficiency', 'hacker', 'narc', 'quality']);
180
+ for (const c of calls) {
181
+ assert.equal(c.opts.model, grader.workerModelFor(c.kind, grader.selectGraderModel({}), undefined));
182
+ assert.equal(c.opts.timeoutMs, 360000, `${c.kind}: the per-worker budget overrides runOpts.timeoutMs`);
183
+ assert.equal(c.opts.cwd, process.cwd());
184
+ assert.match(c.prompt, /## Prior panel round \(this is re-grade round 2\)/, `${c.kind} sees the prior round`);
185
+ assert.match(c.prompt, /## Post-change file contents \(1 of 1 changed files, bundled\)/, `${c.kind} sees the bundle`);
186
+ }
187
+ const hacker = calls.find((c) => c.kind === 'hacker');
188
+ assert.match(hacker.prompt, /## Deterministic route-rank pre-check/, 'the Hacker is handed the permission-path findings');
189
+
190
+ assert.deepEqual(r.signals.workers.map((w) => w.kind), ['narc', 'quality', 'hacker', 'efficiency', 'permission-path']);
191
+ assert.equal(r.signals.panel_outcome, 'fail', 'a sub-Metic protected-path edit fails the code panel on its own gate');
192
+ assert.equal(r.signals.deterministic_veto, undefined, 'aggregatePanel already failed it — the engine veto had nothing to do');
193
+ const keys = Object.keys(r.signals);
194
+ assert.deepEqual(keys.slice(-4), ['builder_model_assumed', 'grader_model', 'total_ms', 'context_bundle'],
195
+ 'the run meta lands last, exactly where aggregatePanel put it when handed the meta directly');
196
+ assert.equal(r.signals.builder_model_assumed, grader.ASSUMED_BUILDER_MODEL);
197
+ assert.equal(r.signals.context_bundle.read_root, path.resolve(process.cwd()));
198
+ assert.deepEqual(r.signals.context_bundle.bundled.map((b) => b.file), ['src/bongos/auth.js']);
199
+ assert.equal(r.panel_cost_usd, 0.04);
200
+ });
201
+
202
+ test('bypass short-circuits BEFORE any judge is asked (#943)', async () => {
203
+ const { run, calls } = recordingRunner();
204
+ const r = await engine.runGrade({ workType: 'code', task: TASK, diff: DIFF, changedFiles: ['a.js'], judges: ['narc', 'no-such-worker'], runOpts: { bypass: true } }, { runSubagent: run });
205
+ assert.equal(calls.length, 0);
206
+ assert.equal(r.signals.panel_outcome, 'unavailable');
207
+ assert.equal(r.signals.grader_unavailable_reason, 'bypass_requested');
208
+ });
209
+
210
+ test('gradeSubagent grades through the engine\'s DEFAULT model call — a fake claude binary end to end',
211
+ { skip: process.platform === 'win32' && 'the fake CLI is a shebang script; spawn runs it through cmd.exe on win32' },
212
+ async () => {
213
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grade-engine-'));
214
+ const bin = path.join(dir, 'claude');
215
+ fs.writeFileSync(bin, `#!/usr/bin/env node
216
+ let s = '';
217
+ process.stdin.on('data', (d) => { s += d; }).on('end', () => {
218
+ const replies = ${JSON.stringify(PASSING)};
219
+ const kinds = ${JSON.stringify(KIND_MARKS)};
220
+ const hit = kinds.find(([m]) => s.slice(0, 400).includes(m));
221
+ const argv = process.argv.slice(2);
222
+ const payload = { ...(hit ? replies[hit[1]] : {}), notes_md: 'model=' + argv[argv.indexOf('--model') + 1] };
223
+ process.stdout.write(JSON.stringify({ type: 'result', result: JSON.stringify(payload), total_cost_usd: 0.02 }));
224
+ });
225
+ `);
226
+ fs.chmodSync(bin, 0o755);
227
+ // The cache is exercised for its KEY (task_type) but never touches a database.
228
+ const origWithCache = llmCache.withCache;
229
+ const origSpec = llmCache.makeCacheSpec;
230
+ const taskTypes = [];
231
+ llmCache.makeCacheSpec = (args) => { taskTypes.push(args.task_type); return origSpec(args); };
232
+ llmCache.withCache = async (spec, producer) => ({ ...(await producer()), cached: false });
233
+ try {
234
+ const r = await grader.gradeSubagent({
235
+ task: TASK, valueSummary: 'Adds X.', diff: DIFF, diffStats: DS, changedFiles: ['modules/example/a.js'],
236
+ runOpts: { claudeBin: bin, cwd: process.cwd() },
237
+ });
238
+ assert.equal(r.signals.panel_outcome, 'pass', r.notes_md);
239
+ assert.equal(r.passed, true);
240
+ assert.deepEqual(r.signals.workers.map((w) => w.kind), ['narc', 'quality', 'hacker', 'efficiency']);
241
+ for (const w of r.signals.workers) assert.match(w.notes_md, new RegExp(`model=${w.model}$`), 'the spawned CLI got the worker model');
242
+ assert.deepEqual(taskTypes, ['grade_worker', 'grade_worker', 'grade_worker', 'grade_worker']);
243
+ assert.equal(r.panel_cost_usd, 0.08);
244
+ } finally {
245
+ llmCache.withCache = origWithCache;
246
+ llmCache.makeCacheSpec = origSpec;
247
+ fs.rmSync(dir, { recursive: true, force: true });
248
+ }
249
+ });
@@ -0,0 +1,99 @@
1
+ // tests/grade_engine_replay.mjs — the differential replay over RECORDED grades
2
+ // (task 1002839, ADR 0089 §1).
3
+ //
4
+ // The corpus (tests/fixtures/grade-replay/recorded-panel-grades.json) is real:
5
+ // production grade rows pulled by `scripts/gds/grade-replay.js --live
6
+ // --write-fixture`, one per panel shape, with review prose hashed. Each row
7
+ // carries the judges' persisted verdicts and the decision the server stored.
8
+ // Replaying the verdicts through the grade engine's aggregate + gate stages must
9
+ // reproduce that decision exactly — that is what "the extraction changed no
10
+ // grade" means, measured on grades that actually happened rather than asserted.
11
+ //
12
+ // A future change to aggregation or the gate (task 1002841's normalization is
13
+ // the next) that moves any of these recorded decisions fails here, and says
14
+ // which task's grade moved and on which field.
15
+ //
16
+ // Run: node --test tests/grade_engine_replay.mjs
17
+
18
+ import { test } from 'node:test';
19
+ import assert from 'node:assert/strict';
20
+ import { createRequire } from 'node:module';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const grader = require('../modules/grading/grader.js');
24
+ const replay = require('../scripts/gds/grade-replay.js');
25
+ const { grades: corpus } = require('./fixtures/grade-replay/recorded-panel-grades.json');
26
+
27
+ const clone = (v) => JSON.parse(JSON.stringify(v));
28
+
29
+ test('the corpus is real and varied, not a pile of all-pass panels', () => {
30
+ assert.ok(corpus.length >= 20, `expected a corpus of 20+ recorded grades, got ${corpus.length}`);
31
+ const outcomes = new Set(corpus.map((g) => g.recorded.panel_outcome));
32
+ for (const o of ['pass', 'concerns', 'fail', 'unavailable']) assert.ok(outcomes.has(o), `no recorded '${o}' grade in the corpus`);
33
+ const workers = corpus.flatMap((g) => g.workers);
34
+ assert.ok(workers.some((w) => w.kind === 'route-rank'), 'a deterministic pseudo-judge');
35
+ assert.ok(workers.some((w) => w.kind === 'reader'), 'the non-gating reader');
36
+ assert.ok(workers.some((w) => w.retried), 'a retried worker');
37
+ assert.ok(workers.some((w) => w.error), 'an errored worker');
38
+ assert.equal(new Set(corpus.map(replay.shapeKey)).size, corpus.length, 'one row per panel shape');
39
+ });
40
+
41
+ test('every recorded grade replays to the decision production stored', () => {
42
+ for (const row of corpus) {
43
+ const { mismatches } = replay.replayRow(row);
44
+ assert.deepEqual(mismatches, [], `task ${row.task_id} (${row.graded_at}) replays differently: ${JSON.stringify(mismatches)}`);
45
+ }
46
+ });
47
+
48
+ test('the engine gate changes nothing for the code panel on any recorded grade', () => {
49
+ for (const row of corpus) {
50
+ const viaEngine = grader.gradeEngine.replayRecorded({ signals: { workers: row.workers } });
51
+ assert.deepEqual(viaEngine, grader.aggregatePanel(row.workers, {}), `task ${row.task_id}`);
52
+ }
53
+ });
54
+
55
+ test('the replay is not vacuous: a changed record, or a changed verdict, is flagged', () => {
56
+ const passing = corpus.find((g) => g.recorded.panel_outcome === 'pass' && g.workers.some((w) => w.kind === 'quality'));
57
+ const edited = clone(passing);
58
+ edited.recorded.panel_outcome = 'concerns';
59
+ assert.deepEqual(replay.replayRow(edited).mismatches.map((m) => m.field), ['panel_outcome']);
60
+
61
+ const flipped = clone(passing);
62
+ const q = flipped.workers.find((w) => w.kind === 'quality');
63
+ q.verdict = 'fail';
64
+ q.issues = [{ severity: 'major', file: 'h:x', summary: 'h:y' }];
65
+ const fields = replay.replayRow(flipped).mismatches.map((m) => m.field);
66
+ for (const f of ['panel_outcome', 'pass_count', 'fail_count', 'confidence']) assert.ok(fields.includes(f), `${f} not flagged: ${fields}`);
67
+ });
68
+
69
+ test('compareDecision: a stored 0 against an axis the panel left null is the old server coercion — nothing else is', () => {
70
+ const rec = { panel_outcome: 'unavailable', rubric: { code_quality: 0, functional_fidelity: 7 } };
71
+ const rep = { panel_outcome: 'unavailable', rubric: { code_quality: null, functional_fidelity: null } };
72
+ assert.deepEqual(replay.compareDecision(rec, rep), [{ field: 'rubric.functional_fidelity', recorded: 7, replayed: null }]);
73
+ assert.deepEqual(replay.compareDecision({ confidence: undefined, rubric: {} }, { confidence: 0.3, rubric: {} }), [],
74
+ 'a confidence the server guards rewrote is recorded as undefined and not compared');
75
+ });
76
+
77
+ test('trimRecordedGrade keeps what aggregate + gate read and hashes the prose', () => {
78
+ const grade = {
79
+ rubric_scores: { functional_fidelity: '9', code_quality: 8 },
80
+ signals: {
81
+ panel_outcome: 'pass', pass_count: 1, worker_count: 1, panel_cost_usd: '0.5', confidence: 0.8,
82
+ server_guard: { downgraded_to: null },
83
+ workers: [{ kind: 'quality', verdict: 'pass', error: null, scores: { code_quality: 8 }, confidence: 0.8, model: 'm', cost_usd: 0.5,
84
+ notes_md: 'long review prose', name: 'Quality', issues: [{ severity: 'minor', file: 'a.js', summary: ' the same finding ' }] }],
85
+ },
86
+ };
87
+ const row = replay.trimRecordedGrade({ task_id: 7, graded_at: 'T', grade });
88
+ assert.equal(row.task_id, '7');
89
+ assert.equal(row.recorded.rubric.functional_fidelity, 9);
90
+ assert.equal(row.recorded.panel_cost_usd, 0.5);
91
+ assert.equal(row.workers[0].notes_md, undefined, 'prose the gate never reads is dropped');
92
+ const [issue] = row.workers[0].issues;
93
+ assert.match(issue.summary, /^h:[0-9a-f]{10}$/);
94
+ assert.equal(issue.summary, replay.trimRecordedGrade({ task_id: 7, grade: { ...grade, signals: { ...grade.signals, workers: [{ ...grade.signals.workers[0], issues: [{ ...grade.signals.workers[0].issues[0], summary: 'the same finding' }] }] } } }).workers[0].issues[0].summary,
95
+ 'hashing the TRIMMED summary keeps the de-duplication key exact');
96
+ assert.equal(replay.trimRecordedGrade({ task_id: 8, grade: { signals: { workers: [] } } }), null, 'not a panel grade');
97
+ const guarded = replay.trimRecordedGrade({ task_id: 9, grade: { ...grade, signals: { ...grade.signals, server_guard: { downgraded_to: 'concerns' } } } });
98
+ assert.equal(guarded.recorded.confidence, undefined, "a confidence the server guards rewrote is theirs, not the panel's — not recorded");
99
+ });
@@ -0,0 +1,132 @@
1
+ // tests/grader_prompt_window_bound.mjs
2
+ //
3
+ // Task 1003648 — a worker prompt too big for its model's window degrades instead
4
+ // of failing outright, and a real overflow is recognised as one.
5
+ //
6
+ // TWO DEFECTS, both in runWorker:
7
+ // 1. The overflow tag never fired. Task 1003318 matched
8
+ // /prompt is too long: the request is .../ — a colon — but the line the
9
+ // `claude` CLI really prints (stored verbatim on task 1003298's grade row) is
10
+ // "Prompt is too long · the request is ~251248 tokens (limit 200000) ...".
11
+ // So a real overflow was filed as SUBAGENT_NONZERO_EXIT and retried as if
12
+ // it were transient. The existing test was green because its fixture used
13
+ // the paraphrase. REAL_REFUSAL below is the real sentence.
14
+ // 2. Nothing smaller was ever tried. The post-change file bundle (up to 120k
15
+ // chars) is the one part of the prompt that can go without losing evidence —
16
+ // the worker can Read those files — so an overflow now retries ONCE without
17
+ // it, and only then stands as PROMPT_TOO_LONG (an explicit lens-unavailable).
18
+ //
19
+ // Run: node tests/grader_prompt_window_bound.mjs
20
+
21
+ import { strict as assert } from 'node:assert';
22
+ import { createRequire } from 'node:module';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const { runWorker, quality, TRANSIENT_WORKER_ERRORS } = require('../modules/grading/grader-workers/index.js');
26
+
27
+ let passed = 0;
28
+ let failed = 0;
29
+ async function test(name, fn) {
30
+ try { await fn(); passed++; console.log(` ok ${name}`); }
31
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
32
+ }
33
+
34
+ // The CLI's own refusal sentence, exactly as stored in
35
+ // signals.workers[quality].stderr_tail on task 1003298's grade (2026-08-27).
36
+ const REAL_REFUSAL = 'Prompt is too long · the request is ~251248 tokens (limit 200000) but this conversation is only ~100861 tokens — the rest is system prompt, tool definitions, and attachment content. A single-exchange conversation cannot be compacted; reduce attached files/tools or start with less context.';
37
+
38
+ // How runSubagent rejects when the CLI exits nonzero: the JSON result envelope is
39
+ // on stdout (--output-format json) and stderr is empty — the 1003298 shape.
40
+ const refusal = () => Object.assign(new Error('subagent exited with code 1'), {
41
+ code: 'SUBAGENT_NONZERO_EXIT', exit_code: 1, run_ms: 1087, stderr: '',
42
+ stdout: JSON.stringify({ type: 'result', subtype: 'success', is_error: true, result: REAL_REFUSAL, duration_ms: 1087 }),
43
+ });
44
+ const GOOD = JSON.stringify({ code_quality: 9, economy: 9, confidence: 0.9, issues: [], notes_md: 'clean' });
45
+
46
+ const BUNDLE_MARKER = 'BUNDLED_CONTENT_OF_a_js_7f3e';
47
+ const DIFF_MARKER = '+const shipped = true; // DIFF_MARKER_91c2';
48
+ const bundle = (files) => ({ files, skipped: [], total_chars: files.reduce((n, f) => n + f.content.length, 0) });
49
+ const ONE_FILE = [{ file: 'a.js', content: `// ${BUNDLE_MARKER}\n${'x'.repeat(5000)}`, truncated: false, chars_total: 5010 }];
50
+
51
+ // The real Quality worker (real prompt scaffold, parser and verdict) with only the
52
+ // subprocess replaced. `outcomes` is consumed one per spawn; every prompt is kept.
53
+ async function run({ contextBundle, outcomes }) {
54
+ const prompts = [];
55
+ const r = await runWorker(quality, {
56
+ task: { id: 1003648, title: 't', description: 'd', kind: 'bug', discipline: 'engineer' },
57
+ valueSummary: 'v', diff: DIFF_MARKER, changedFiles: ['a.js'], diffStats: {},
58
+ model: 'claude-haiku-4-5-20251001', runOpts: {}, contextBundle,
59
+ runSubagentFn: async ({ prompt }) => {
60
+ prompts.push(prompt);
61
+ const next = outcomes.shift();
62
+ if (next === 'refuse') throw refusal();
63
+ return { stdout: next, run_ms: 5, cost_usd: 0.01 };
64
+ },
65
+ });
66
+ return { r, prompts };
67
+ }
68
+
69
+ // ---- 1. the real sentence is recognised (defect 1) ------------------------
70
+
71
+ await test('the CLI\'s real refusal sentence is tagged PROMPT_TOO_LONG, and not retried as transient', async () => {
72
+ const { r, prompts } = await run({ contextBundle: bundle([]), outcomes: ['refuse', GOOD] });
73
+ assert.equal(r.error, 'PROMPT_TOO_LONG', 'the real " · " form must classify, not only a paraphrased colon');
74
+ assert.equal(prompts.length, 1, 'a window overflow is not transient — no task-465 retry of the same prompt');
75
+ assert.equal(r.retried, false);
76
+ assert.equal(TRANSIENT_WORKER_ERRORS.has('PROMPT_TOO_LONG'), false);
77
+ assert.equal('prompt_bounded' in r, false, 'with no bundle there was nothing smaller to try');
78
+ });
79
+
80
+ await test('the colon paraphrase still classifies too', async () => {
81
+ const r = await runWorker(quality, {
82
+ task: {}, diff: 'x', changedFiles: [], model: 'm', runOpts: {},
83
+ runSubagentFn: async () => { throw Object.assign(new Error('exit 1'), { code: 'SUBAGENT_NONZERO_EXIT', exit_code: 1, stderr: 'Prompt is too long: the request is ~9 tokens (limit 8)', stdout: '' }); },
84
+ });
85
+ assert.equal(r.error, 'PROMPT_TOO_LONG');
86
+ });
87
+
88
+ // ---- 2. the overflow degrades: one retry without the bundle (defect 2) -----
89
+
90
+ await test('an overflow with a bundle retries ONCE without it — same diff — and grades normally', async () => {
91
+ const { r, prompts } = await run({ contextBundle: bundle(ONE_FILE), outcomes: ['refuse', GOOD] });
92
+ assert.equal(prompts.length, 2);
93
+ assert.ok(prompts[0].includes(BUNDLE_MARKER), 'the first prompt carried the bundle');
94
+ assert.ok(!prompts[1].includes(BUNDLE_MARKER), 'the retry drops the bundled file contents');
95
+ assert.ok(prompts[1].includes('DIFF_MARKER_91c2'), 'the retry still carries the whole diff');
96
+ assert.ok(prompts[1].length < prompts[0].length);
97
+ assert.equal(r.error, null);
98
+ assert.equal(r.verdict, 'pass');
99
+ assert.deepEqual(r.prompt_bounded, {
100
+ reason: 'PROMPT_TOO_LONG', bundle_dropped: true, bundle_files: 1,
101
+ prompt_chars: prompts[0].length, retry_prompt_chars: prompts[1].length,
102
+ request_tokens: 251248, window_tokens: 200000, retry_error: null,
103
+ });
104
+ });
105
+
106
+ await test('if the smaller prompt overflows too, the lens is explicitly unavailable — two spawns, no score', async () => {
107
+ const { r, prompts } = await run({ contextBundle: bundle(ONE_FILE), outcomes: ['refuse', 'refuse', GOOD] });
108
+ assert.equal(prompts.length, 2, 'one degraded retry, never a loop');
109
+ assert.equal(r.error, 'PROMPT_TOO_LONG');
110
+ assert.deepEqual(r.scores, {}, 'no axis is fabricated for a worker that never ran');
111
+ assert.equal(r.prompt_bounded.retry_error, 'PROMPT_TOO_LONG');
112
+ });
113
+
114
+ await test('an ordinary run keeps the bundle and its exact row shape', async () => {
115
+ const { r, prompts } = await run({ contextBundle: bundle(ONE_FILE), outcomes: [GOOD] });
116
+ assert.equal(prompts.length, 1);
117
+ assert.ok(prompts[0].includes(BUNDLE_MARKER));
118
+ assert.equal(r.verdict, 'pass');
119
+ assert.equal('prompt_bounded' in r, false);
120
+ });
121
+
122
+ await test('a transient error is still retried once with the SAME prompt (the task 465 retry, unchanged)', async () => {
123
+ const { r, prompts } = await run({ contextBundle: bundle(ONE_FILE), outcomes: ['not json at all', GOOD] });
124
+ assert.equal(prompts.length, 2);
125
+ assert.equal(prompts[0], prompts[1], 'a parse_error retry re-sends the same prompt, bundle included');
126
+ assert.equal(r.retried, true);
127
+ assert.equal(r.verdict, 'pass');
128
+ assert.equal('prompt_bounded' in r, false);
129
+ });
130
+
131
+ console.log(`\n${passed} passed, ${failed} failed`);
132
+ process.exit(failed === 0 ? 0 : 1);