@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,280 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // scripts/gds/grade-correlation-audit.js — do the grader's panel workers fail
5
+ // DIFFERENTLY, or are we paying four times for two votes? (task 1002843,
6
+ // ADR 0089 §5 — the structural answer to "Nine Judges, Two Effective Votes").
7
+ //
8
+ // WHY. Aggregation cannot manufacture independence a panel does not have. ADR
9
+ // 0089 therefore makes panel composition a measured choice, and nothing measured
10
+ // it: /grade-audit and /grader-health are qualitative sweeps with no inter-judge
11
+ // statistics. This reads PERSISTED grades — never re-grades, never spawns a
12
+ // model, never writes anything — and reports:
13
+ // (a) pairwise agreement between the panel workers on the same task: raw
14
+ // agreement, Cohen's kappa (agreement corrected for chance) and the phi
15
+ // correlation of their objections;
16
+ // (b) each worker's Cohen's kappa against the OWNER'S decisions;
17
+ // (c) which workers look redundant with another and which carry independent
18
+ // signal, plus the panel's effective number of independent votes.
19
+ //
20
+ // A VOTE is binary: a worker OBJECTS (verdict concerns or fail) or CLEARS (pass).
21
+ // A worker that errored without a finding produced no signal and casts no vote
22
+ // on that task — the same missing-signal rule the gate applies (#473). The
23
+ // deterministic pseudo-judges and the non-gating reader are not voters.
24
+ //
25
+ // THE OWNER'S DECISIONS — the only labels used, per the task:
26
+ // accept an approved override request, or a manual confirm of work whose
27
+ // stored grade did not pass (task confirmed/shipped anyway);
28
+ // reject a denied override request.
29
+ // A passing grade that auto-landed is NOT a label: that is the grader agreeing
30
+ // with itself, and counting it would inflate every worker's kappa. The labelled
31
+ // set is therefore a selected one — tasks the panel did not pass, plus the ones
32
+ // someone argued with — and the report says so.
33
+ //
34
+ // THE MINIMUM-N GUARD. Every statistic is REFUSED, with the reason, below
35
+ // MIN_N observations or when either side has fewer than MIN_PER_CLASS of a
36
+ // class (a kappa over a constant rater is undefined, and one over five labels is
37
+ // noise). --min-n may raise the bar, never lower it. Report-only: no gate reads
38
+ // this, and nothing changes status (ADR 0162).
39
+ //
40
+ // Usage:
41
+ // node scripts/gds/grade-correlation-audit.js [--recent N] [--min-n N] [--json]
42
+ // pull the N most recently finished tasks' grades (default 400) + the
43
+ // decided override requests (Metic+ read) through the API
44
+ // ... --save <file> also write the pulled dataset, to re-run offline
45
+ // ... --file <file> audit a saved dataset instead of the live API
46
+
47
+ const fs = require('node:fs');
48
+ const { arg, hasFlag, cliClient } = require('./cli-lib');
49
+ const { fetchRecentGrades, trimRecordedGrade } = require('./grade-replay');
50
+
51
+ const PANEL = ['narc', 'quality', 'hacker', 'efficiency'];
52
+ const MIN_N = 30;
53
+ const MIN_PER_CLASS = 5;
54
+ const REDUNDANT_KAPPA = 0.8;
55
+ const INDEPENDENT_KAPPA = 0.4;
56
+
57
+ // A worker's vote on one grade: 1 objects, 0 clears, null no signal.
58
+ function voteOf(w) {
59
+ if (!w || typeof w !== 'object') return null;
60
+ const issues = Array.isArray(w.issues) ? w.issues : [];
61
+ const substantive = issues.some((i) => i && (i.severity === 'blocker' || i.severity === 'major'));
62
+ if (w.error && !substantive) return null;
63
+ if (w.verdict === 'pass') return 0;
64
+ if (w.verdict === 'concerns' || w.verdict === 'fail') return 1;
65
+ return null;
66
+ }
67
+
68
+ // { kind -> vote } for the panel workers of one grade.
69
+ function panelVotes(workers) {
70
+ const out = {};
71
+ for (const w of Array.isArray(workers) ? workers : []) {
72
+ if (!w || !PANEL.includes(w.kind)) continue;
73
+ const v = voteOf(w);
74
+ if (v != null) out[w.kind] = v;
75
+ }
76
+ return out;
77
+ }
78
+
79
+ // Why a binary series cannot carry a statistic, or null when it can.
80
+ function refusal(pairs, minN) {
81
+ if (pairs.length < minN) return `n=${pairs.length} is below the minimum of ${minN}`;
82
+ for (const side of [0, 1]) {
83
+ const ones = pairs.filter((p) => p[side] === 1).length;
84
+ const zeros = pairs.length - ones;
85
+ if (Math.min(ones, zeros) < MIN_PER_CLASS) {
86
+ return `${side === 0 ? 'the first' : 'the second'} rater has only ${Math.min(ones, zeros)} of one class (need ${MIN_PER_CLASS}+ of each)`;
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+
92
+ // Cohen's kappa + raw agreement + phi for [[a, b], ...] binary pairs. Pure; the
93
+ // caller has already applied the minimum-N guard.
94
+ function agreementStats(pairs) {
95
+ const n = pairs.length;
96
+ let n11 = 0; let n10 = 0; let n01 = 0; let n00 = 0;
97
+ for (const [a, b] of pairs) {
98
+ if (a === 1 && b === 1) n11++; else if (a === 1) n10++; else if (b === 1) n01++; else n00++;
99
+ }
100
+ const po = (n11 + n00) / n;
101
+ const pa = (n11 + n10) / n;
102
+ const pb = (n11 + n01) / n;
103
+ const pe = pa * pb + (1 - pa) * (1 - pb);
104
+ const kappa = pe === 1 ? null : (po - pe) / (1 - pe);
105
+ const den = Math.sqrt((n11 + n10) * (n01 + n00) * (n11 + n01) * (n10 + n00));
106
+ const phi = den === 0 ? null : (n11 * n00 - n10 * n01) / den;
107
+ return { n, agreement: po, kappa, phi, table: { both_object: n11, only_first: n10, only_second: n01, both_clear: n00 } };
108
+ }
109
+
110
+ function guardedStats(pairs, minN) {
111
+ const why = refusal(pairs, minN);
112
+ return why ? { n: pairs.length, refused: why } : agreementStats(pairs);
113
+ }
114
+
115
+ // Effective number of independent votes for k raters whose mean pairwise
116
+ // correlation is rho (the design-effect formula). A negative mean is clamped to
117
+ // 0 — this is a ceiling on independence, not a claim of anti-correlation.
118
+ function effectiveVotes(k, rho) {
119
+ const r = Math.max(0, rho);
120
+ return k / (1 + (k - 1) * r);
121
+ }
122
+
123
+ // The owner's decision per task id, from the override ledger and the tasks'
124
+ // fates. Returns Map(task_id -> { label: 'accept'|'reject', source }). An
125
+ // approval outranks a denial on the same task: it was the later decision.
126
+ function ownerLabels({ grades, overrides }) {
127
+ const labels = new Map();
128
+ for (const o of overrides || []) {
129
+ if (o.status === 'approved') labels.set(String(o.task_id), { label: 'accept', source: 'override_approved' });
130
+ else if (o.status === 'denied' && !labels.has(String(o.task_id))) labels.set(String(o.task_id), { label: 'reject', source: 'override_denied' });
131
+ }
132
+ for (const g of grades || []) {
133
+ const id = String(g.task_id);
134
+ if (labels.has(id)) continue;
135
+ const landed = g.task_status === 'confirmed' || g.task_status === 'shipped';
136
+ if (landed && g.passed === false) labels.set(id, { label: 'accept', source: 'manual_confirm_after_fail' });
137
+ }
138
+ return labels;
139
+ }
140
+
141
+ function classify(kappa) {
142
+ if (kappa == null) return 'undetermined';
143
+ if (kappa >= REDUNDANT_KAPPA) return 'redundant';
144
+ if (kappa >= INDEPENDENT_KAPPA) return 'overlapping';
145
+ return 'independent';
146
+ }
147
+
148
+ // One stored grade → the slim row the audit reads (review prose hashed, as in
149
+ // the replay corpus): { task_id, task_status, passed, workers }, or null when
150
+ // it is not a panel grade.
151
+ function toAuditRow({ task_id, task_status, grade }) {
152
+ const row = trimRecordedGrade({ task_id, grade });
153
+ return row ? { task_id: row.task_id, task_status: task_status || null, passed: !!(grade && grade.passed === true), workers: row.workers } : null;
154
+ }
155
+
156
+ // The whole report, pure over a dataset { grades: [toAuditRow…], overrides: [{task_id, status}] }.
157
+ function audit(dataset, { minN = MIN_N } = {}) {
158
+ const floor = Math.max(MIN_N, Number(minN) || MIN_N);
159
+ const panels = (dataset.grades || [])
160
+ .map((g) => ({ task_id: String(g.task_id), votes: panelVotes(g.workers) }))
161
+ .filter((p) => Object.keys(p.votes).length > 0);
162
+ const labels = ownerLabels(dataset);
163
+ // 1 when the worker's vote disagrees with the owner (objected to accepted
164
+ // work, or cleared rejected work); only on labelled tasks it voted on.
165
+ const errorOf = (p, kind) => (labels.has(p.task_id) && kind in p.votes
166
+ ? Number(p.votes[kind] !== (labels.get(p.task_id).label === 'reject' ? 1 : 0))
167
+ : null);
168
+
169
+ const pairwise = [];
170
+ for (let i = 0; i < PANEL.length; i++) {
171
+ for (let j = i + 1; j < PANEL.length; j++) {
172
+ const a = PANEL[i]; const b = PANEL[j];
173
+ const pairs = panels.filter((p) => a in p.votes && b in p.votes).map((p) => [p.votes[a], p.votes[b]]);
174
+ const stats = guardedStats(pairs, floor);
175
+ // Error correlation: do the two get the SAME tasks wrong against the owner?
176
+ const errPairs = panels.map((p) => [errorOf(p, a), errorOf(p, b)]).filter(([x, y]) => x != null && y != null);
177
+ pairwise.push({ a, b, ...stats, reading: stats.refused ? 'undetermined' : classify(stats.kappa), errors: guardedStats(errPairs, floor) });
178
+ }
179
+ }
180
+ // The panel's effective vote count is a claim about ALL of it, so it needs
181
+ // every pair — one computable pair extrapolated to four workers would be the
182
+ // over-reading this guard exists to stop.
183
+ const phis = pairwise.filter((p) => !p.refused && p.phi != null).map((p) => p.phi);
184
+ const meanPhi = phis.length === pairwise.length ? phis.reduce((s, x) => s + x, 0) / phis.length : null;
185
+
186
+ const byLabel = { accept: 0, reject: 0 };
187
+ for (const v of labels.values()) byLabel[v.label]++;
188
+ const perWorker = PANEL.map((kind) => {
189
+ const pairs = panels
190
+ .filter((p) => labels.has(p.task_id) && kind in p.votes)
191
+ .map((p) => [p.votes[kind], labels.get(p.task_id).label === 'reject' ? 1 : 0]);
192
+ return { kind, ...guardedStats(pairs, floor) };
193
+ });
194
+
195
+ return {
196
+ min_n: floor,
197
+ panels: panels.length,
198
+ pairwise,
199
+ mean_pairwise_phi: meanPhi,
200
+ effective_votes: meanPhi == null ? null : effectiveVotes(PANEL.length, meanPhi),
201
+ labels: { total: labels.size, ...byLabel },
202
+ vs_owner: perWorker,
203
+ };
204
+ }
205
+
206
+ // ---------- the CLI ----------
207
+
208
+ async function fetchDataset(recent) {
209
+ const client = await cliClient();
210
+ const grades = (await fetchRecentGrades({ recent, client })).map(toAuditRow).filter(Boolean);
211
+ const overrides = [];
212
+ for (const status of ['approved', 'denied']) {
213
+ const r = await client.overrideRequests.getOverrideRequests({ query: { status } });
214
+ if (!r.ok) throw new Error(`GET /override-requests?status=${status} → HTTP ${r.status} (a Metic+ read)`);
215
+ for (const o of (r.data && r.data.requests) || []) overrides.push({ task_id: String(o.task_id), status: o.status, decided_at: o.decided_at || null });
216
+ }
217
+ return { pulled_at: new Date().toISOString(), grades, overrides };
218
+ }
219
+
220
+ const pct = (x) => `${(x * 100).toFixed(1)}%`;
221
+ const num = (x) => (x == null ? 'n/a' : x.toFixed(2));
222
+ const statLine = (s) => (s.refused ? `REFUSED — ${s.refused}` : `n=${s.n} agree ${pct(s.agreement)} kappa ${num(s.kappa)} phi ${num(s.phi)}`);
223
+
224
+ function printReport(r) {
225
+ const out = [];
226
+ out.push(`grade correlation audit — ${r.panels} recorded panel grade(s), minimum n = ${r.min_n}`);
227
+ out.push('A vote is OBJECTS (concerns/fail) or CLEARS (pass); an errored worker with no finding casts none.');
228
+ out.push('');
229
+ out.push('(a) Do the workers fail differently? — agreement between each pair on the same task');
230
+ for (const p of r.pairwise) {
231
+ out.push(` ${`${p.a} × ${p.b}`.padEnd(22)} ${statLine(p)}${p.refused ? '' : ` → ${p.reading}`}`);
232
+ out.push(` ${''.padEnd(22)} same tasks wrong vs the owner: ${statLine(p.errors)}`);
233
+ }
234
+ const cleared = r.pairwise.filter((p) => !p.refused).length;
235
+ out.push(r.effective_votes == null
236
+ ? ` effective independent votes: not computable — needs all ${r.pairwise.length} pairs, ${cleared} cleared the minimum`
237
+ : ` mean pairwise phi ${num(r.mean_pairwise_phi)} → about ${r.effective_votes.toFixed(1)} effective independent vote(s) out of 4`);
238
+ out.push('');
239
+ out.push(`(b) Each worker against the owner's decisions — ${r.labels.total} labelled task(s): ${r.labels.accept} accepted, ${r.labels.reject} rejected`);
240
+ for (const w of r.vs_owner) out.push(` ${w.kind.padEnd(22)} ${statLine(w)}`);
241
+ out.push('');
242
+ const redundant = r.pairwise.filter((p) => p.reading === 'redundant').map((p) => `${p.a}+${p.b}`);
243
+ const independent = r.pairwise.filter((p) => p.reading === 'independent').map((p) => `${p.a}+${p.b}`);
244
+ out.push('(c) Reading');
245
+ out.push(` redundant pairs (kappa >= 0.8): ${redundant.join(', ') || 'none'}`);
246
+ out.push(` independent pairs (kappa < 0.4): ${independent.join(', ') || 'none'}`);
247
+ out.push('');
248
+ out.push('Caveats: labels exist only where a person decided — override requests and manual confirms of work the');
249
+ out.push('panel did not pass — so they are a selected set, not a sample. A grade that auto-landed on a pass is');
250
+ out.push('never a label (that is the grader agreeing with itself). Only each task\'s LATEST grade keeps per-worker');
251
+ out.push('verdicts (grade_attempts stores pass/score only), so a re-graded task counts once, as its final round.');
252
+ console.log(out.join('\n'));
253
+ }
254
+
255
+ async function main() {
256
+ const file = arg('--file');
257
+ const dataset = file
258
+ ? JSON.parse(fs.readFileSync(file, 'utf8'))
259
+ : await fetchDataset(Number(arg('--recent')) || 400);
260
+ const save = arg('--save');
261
+ if (save && !file) fs.writeFileSync(save, `${JSON.stringify(dataset)}\n`);
262
+ const report = audit(dataset, { minN: Number(arg('--min-n')) || MIN_N });
263
+ if (hasFlag('--json')) console.log(JSON.stringify(report, null, 2));
264
+ else printReport(report);
265
+ }
266
+
267
+ module.exports = {
268
+ MIN_N,
269
+ agreementStats,
270
+ audit,
271
+ effectiveVotes,
272
+ ownerLabels,
273
+ panelVotes,
274
+ toAuditRow,
275
+ voteOf,
276
+ };
277
+
278
+ if (require.main === module) {
279
+ main().catch((err) => { console.error(`grade-correlation-audit: ${err.message}`); process.exit(2); });
280
+ }
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // scripts/gds/grade-replay.js — the differential replay over RECORDED grades
5
+ // (task 1002839, ADR 0089 §1). Read-only: it never grades, never spawns a model
6
+ // and never writes a row.
7
+ //
8
+ // WHY. How a grade is aggregated and gated IS the live ship gate, so a change to
9
+ // either is only behaviour-preserving if real recorded grades still come out the
10
+ // way production recorded them. Every persisted panel grade carries its judges'
11
+ // verdicts (signals.workers, the deterministic pseudo-judges included), so the
12
+ // grade engine's aggregate + gate stages can be re-run over them at zero cost and
13
+ // compared field by field with what the server stored.
14
+ //
15
+ // WHAT IS COMPARED — the decision, not the prose: panel_outcome, the four verdict
16
+ // counts, the worker count, the panel's own cost, the forwarded confidence (skipped
17
+ // when the server-side guards downgraded it — that number is theirs, not the
18
+ // panel's), and the five rubric axes the server re-records.
19
+ //
20
+ // Usage:
21
+ // node scripts/gds/grade-replay.js replay the committed corpus
22
+ // node scripts/gds/grade-replay.js --file <corpus> replay another corpus file
23
+ // node scripts/gds/grade-replay.js --live [--recent N]
24
+ // pull the N most recently finished tasks' grades from the API (default 150)
25
+ // ... --write-fixture <path> write the AGREEING replays, trimmed (issue text and
26
+ // stderr hashed), one per panel shape — how the test corpus is made
27
+ // ... --json machine-readable summary
28
+ // Exit 0 when every replayed grade agrees, 1 when any does not. A report, not a
29
+ // gate: nothing reads the exit code but the person who ran it.
30
+
31
+ const fs = require('node:fs');
32
+ const path = require('node:path');
33
+ const crypto = require('node:crypto');
34
+ const { arg, hasFlag, cliClient } = require('./cli-lib');
35
+ // Through the module facade: requiring grader.js is what registers the code panel
36
+ // as the engine's default work-type.
37
+ const engine = require('../../modules/grading/grader').gradeEngine;
38
+
39
+ const DEFAULT_CORPUS = path.join(__dirname, '..', '..', 'tests', 'fixtures', 'grade-replay', 'recorded-panel-grades.json');
40
+ const COUNT_FIELDS = ['pass_count', 'concerns_count', 'fail_count', 'error_count', 'worker_count'];
41
+ const RUBRIC_AXES = ['functional_fidelity', 'code_quality', 'economy', 'security_score', 'efficiency_score'];
42
+ const PER_SHAPE = 1;
43
+
44
+ const num = (v) => (v == null || v === '' ? null : Number(v));
45
+
46
+ // The decision a stored grade row records. The rubric axes are the server's
47
+ // re-derivation (validateAndScore) of the panel's medians — equal to them for
48
+ // every grade since the null-axis fixes (tasks 1002653 / 1003318).
49
+ function recordedDecision(grade) {
50
+ const s = (grade && grade.signals) || {};
51
+ const guard = s.server_guard || {};
52
+ const out = { panel_outcome: s.panel_outcome ?? null };
53
+ for (const k of COUNT_FIELDS) out[k] = s[k] ?? null;
54
+ out.panel_cost_usd = num(s.panel_cost_usd);
55
+ out.confidence = guard.downgraded_to ? undefined : num(s.confidence);
56
+ out.rubric = {};
57
+ for (const a of RUBRIC_AXES) out.rubric[a] = num(grade && grade.rubric_scores ? grade.rubric_scores[a] : null);
58
+ return out;
59
+ }
60
+
61
+ // The same decision, read off a replayed grade-result.
62
+ function replayedDecision(result) {
63
+ const s = (result && result.signals) || {};
64
+ const out = { panel_outcome: s.panel_outcome ?? null };
65
+ for (const k of COUNT_FIELDS) out[k] = s[k] ?? null;
66
+ out.panel_cost_usd = num(s.panel_cost_usd);
67
+ out.confidence = num(s.confidence);
68
+ out.rubric = {};
69
+ for (const a of RUBRIC_AXES) out.rubric[a] = num(result && result.rubric_scores ? result.rubric_scores[a] : null);
70
+ return out;
71
+ }
72
+
73
+ const same = (a, b) => a === b || (typeof a === 'number' && typeof b === 'number' && Math.abs(a - b) < 1e-9);
74
+
75
+ // Every field on which a replay disagrees with the record, as
76
+ // { field, recorded, replayed }. An `undefined` recorded value is not compared.
77
+ //
78
+ // One pairing is NOT a disagreement: a rubric axis the panel left null (no judge
79
+ // reported it) that the row stores as 0. That 0 is the server's, not the panel's —
80
+ // validateAndScore coerced a missing axis with Number(null) === 0 until tasks
81
+ // 1002653 / 1003318 made it record null. A real 0/10 median replays as 0, so the
82
+ // pairing can only mean that coercion; any other value against a null still counts.
83
+ function compareDecision(recorded, replayed) {
84
+ const out = [];
85
+ for (const k of ['panel_outcome', ...COUNT_FIELDS, 'panel_cost_usd', 'confidence']) {
86
+ if (recorded[k] === undefined) continue;
87
+ if (!same(recorded[k], replayed[k])) out.push({ field: k, recorded: recorded[k], replayed: replayed[k] });
88
+ }
89
+ for (const a of RUBRIC_AXES) {
90
+ const r = (recorded.rubric || {})[a];
91
+ const p = (replayed.rubric || {})[a];
92
+ if (r === undefined || same(r, p) || (r === 0 && p === null)) continue;
93
+ out.push({ field: `rubric.${a}`, recorded: r, replayed: p });
94
+ }
95
+ return out;
96
+ }
97
+
98
+ const shortHash = (s) => `h:${crypto.createHash('sha256').update(String(s)).digest('hex').slice(0, 10)}`;
99
+
100
+ // A worker, reduced to what aggregate + gate read. Issue summaries, their file
101
+ // paths and stderr are hashed: the gate reads a summary and a file only to
102
+ // de-duplicate (the hash of the trimmed text preserves exactly that), and a
103
+ // fixture should not carry verbatim review prose.
104
+ function trimWorker(w) {
105
+ const t = {
106
+ kind: w.kind,
107
+ verdict: w.verdict,
108
+ error: w.error ?? null,
109
+ scores: w.scores || {},
110
+ confidence: w.confidence ?? null,
111
+ model: w.model ?? null,
112
+ cost_usd: w.cost_usd ?? null,
113
+ retried: !!w.retried,
114
+ issues: (Array.isArray(w.issues) ? w.issues : []).filter((i) => i && typeof i === 'object').map((i) => ({
115
+ severity: i.severity ?? null,
116
+ file: i.file == null ? null : shortHash(i.file),
117
+ summary: shortHash(String(i.summary ?? '').trim()),
118
+ })),
119
+ };
120
+ if (w.exit_code != null) t.exit_code = w.exit_code;
121
+ if (w.stderr_tail) t.stderr_tail = shortHash(w.stderr_tail);
122
+ return t;
123
+ }
124
+
125
+ // One recorded grade → a corpus row, or null when it is not a panel grade.
126
+ function trimRecordedGrade({ task_id, graded_at = null, grade }) {
127
+ const s = (grade && grade.signals) || {};
128
+ if (!Array.isArray(s.workers) || s.workers.length === 0) return null;
129
+ return { task_id: String(task_id), graded_at, recorded: recordedDecision(grade), workers: s.workers.map(trimWorker) };
130
+ }
131
+
132
+ // The panel's shape — outcome plus each judge's kind/verdict/error/retry — so
133
+ // the fixture keeps variety instead of 200 copies of an all-pass panel.
134
+ function shapeKey(row) {
135
+ return [row.recorded.panel_outcome, ...row.workers.map((w) => `${w.kind}:${w.verdict}${w.error ? `!${w.error}` : ''}${w.retried ? '~r' : ''}`)].join('|');
136
+ }
137
+
138
+ // Replay one corpus row (or one raw recorded grade) through the engine.
139
+ function replayRow(row) {
140
+ const result = engine.replayRecorded({ signals: { workers: row.workers } });
141
+ return { result, mismatches: compareDecision(row.recorded, replayedDecision(result)) };
142
+ }
143
+
144
+ // The most recently finished tasks' STORED grades, raw and newest first:
145
+ // [{ task_id, task_status, at, grade }]. Read-only GETs. Shared with the
146
+ // correlation audit (scripts/gds/grade-correlation-audit.js), which needs the
147
+ // task's fate beside its grade. The per-task reads run a few at a time — one
148
+ // after another made a 400-task pull take minutes for no reason.
149
+ const FETCH_CONCURRENCY = 6;
150
+ async function fetchRecentGrades({ recent = 150, statuses = ['shipped', 'completed', 'confirmed'], client = null } = {}) {
151
+ const api = client || await cliClient();
152
+ const tasks = [];
153
+ for (const status of statuses) {
154
+ const r = await api.tasks.getTasks({ query: { status, limit: 1000 } });
155
+ if (!r.ok) throw new Error(`GET /tasks?status=${status} → HTTP ${r.status}`);
156
+ for (const t of r.data.tasks || []) tasks.push({ id: String(t.id), status: t.status || status, at: t.shipped_at || t.updated_at || '' });
157
+ }
158
+ tasks.sort((a, b) => String(b.at).localeCompare(String(a.at)));
159
+ const picked = tasks.slice(0, recent);
160
+ const out = new Array(picked.length).fill(null);
161
+ for (let i = 0; i < picked.length; i += FETCH_CONCURRENCY) {
162
+ await Promise.all(picked.slice(i, i + FETCH_CONCURRENCY).map(async (t, j) => {
163
+ const r = await api.tasks.getTasksId({ id: t.id, query: { include: 'grade' } });
164
+ if (r.ok && r.data && r.data.grade) out[i + j] = { task_id: t.id, task_status: t.status, at: t.at, grade: r.data.grade };
165
+ }));
166
+ }
167
+ return out.filter(Boolean);
168
+ }
169
+
170
+ async function fetchLive(recent) {
171
+ const rows = [];
172
+ for (const g of await fetchRecentGrades({ recent })) {
173
+ const row = trimRecordedGrade({ task_id: g.task_id, graded_at: g.at, grade: g.grade });
174
+ if (row) rows.push(row);
175
+ }
176
+ return rows;
177
+ }
178
+
179
+ function readCorpus(file) {
180
+ const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
181
+ return Array.isArray(doc) ? doc : doc.grades;
182
+ }
183
+
184
+ async function main() {
185
+ const live = hasFlag('--live');
186
+ const rows = live
187
+ ? await fetchLive(Number(arg('--recent')) || 150)
188
+ : readCorpus(arg('--file') || DEFAULT_CORPUS);
189
+ const report = rows.map((row) => ({ row, ...replayRow(row) }));
190
+ const disagree = report.filter((r) => r.mismatches.length);
191
+
192
+ const fixturePath = arg('--write-fixture');
193
+ if (fixturePath) {
194
+ const perShape = new Map();
195
+ const keep = [];
196
+ for (const r of report) {
197
+ if (r.mismatches.length) continue;
198
+ const k = shapeKey(r.row);
199
+ const n = perShape.get(k) || 0;
200
+ if (n >= PER_SHAPE) continue;
201
+ perShape.set(k, n + 1);
202
+ keep.push(r.row);
203
+ }
204
+ const note = 'Recorded panel grades for the differential replay (tests/grade_engine_replay.mjs). Generated by scripts/gds/grade-replay.js --live --write-fixture: only grades whose replay agreed with the stored row, one per panel shape. Issue summaries, their files and stderr are hashed.';
205
+ fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
206
+ // One grade per line: small in a diff, and a regenerated corpus diffs by row.
207
+ fs.writeFileSync(fixturePath, `{"note":${JSON.stringify(note)},\n"grades":[\n${keep.map((g) => JSON.stringify(g)).join(',\n')}\n]}\n`);
208
+ console.error(`wrote ${keep.length} grades (${perShape.size} panel shapes) → ${fixturePath}`);
209
+ }
210
+
211
+ if (hasFlag('--json')) {
212
+ console.log(JSON.stringify({
213
+ replayed: report.length,
214
+ agree: report.length - disagree.length,
215
+ disagree: disagree.map((r) => ({ task_id: r.row.task_id, graded_at: r.row.graded_at, mismatches: r.mismatches })),
216
+ }, null, 2));
217
+ } else {
218
+ console.log(`grade replay: ${report.length} recorded panel grade(s), ${report.length - disagree.length} agree, ${disagree.length} disagree`);
219
+ for (const r of disagree) {
220
+ const what = r.mismatches.map((m) => `${m.field} recorded=${JSON.stringify(m.recorded)} replayed=${JSON.stringify(m.replayed)}`).join('; ');
221
+ console.log(` task ${r.row.task_id}${r.row.graded_at ? ` (${r.row.graded_at})` : ''}: ${what}`);
222
+ }
223
+ }
224
+ process.exitCode = disagree.length ? 1 : 0;
225
+ }
226
+
227
+ module.exports = {
228
+ compareDecision,
229
+ fetchRecentGrades,
230
+ replayRow,
231
+ shapeKey,
232
+ trimRecordedGrade,
233
+ };
234
+
235
+ if (require.main === module) {
236
+ main().catch((err) => { console.error(`grade-replay: ${err.message}`); process.exit(2); });
237
+ }
@@ -18,7 +18,8 @@
18
18
  // 1. NOBODY pushes refs/heads/main directly. Under Option A (ADR 0043) the
19
19
  // GDS lands graded work; a builder pushing main bypasses the grader, the
20
20
  // confirm gate, parallel-safety, and credits — exactly the #863 incident.
21
- // Override for the server/operator deploy identity only: OTB_ALLOW_MAIN_PUSH=1.
21
+ // Override for the server/operator deploy identity only: <PREFIX>_ALLOW_MAIN_PUSH=1
22
+ // (BONGOS_ on a vanilla instance; the legacy OTB_ALLOW_MAIN_PUSH is still read).
22
23
  // 2. A builder BELOW Metic may not push a diff that touches a permission-
23
24
  // sensitive path (rank / permission / ship-grade / deploy / migration
24
25
  // machinery — see src/bongos/permission-path-check.js).
@@ -33,6 +34,9 @@ const path = require('node:path');
33
34
 
34
35
  const ppc = require(path.join(__dirname, '..', '..', '..', 'src', 'bongos', 'permission-path-check.js'));
35
36
  const { apiCall, productName } = require(path.join(__dirname, '..', 'cli-lib.js'));
37
+ // The main-push override resolves through the branded prefix, the same reader
38
+ // push-main.js's canonical spelling is written for (task 1001441).
39
+ const { resolveEnv, envPrefix } = require(path.join(__dirname, '..', '..', '..', 'src', 'instance-config.js'));
36
40
 
37
41
  const ZERO = /^0+$/;
38
42
  const MAIN_REF = 'refs/heads/main';
@@ -122,7 +126,7 @@ async function run() {
122
126
  // The operator / server deploy identity is the ONLY thing allowed to push
123
127
  // main, and it sets this env explicitly. (Belt-and-braces; under full Option A
124
128
  // builders never push main at all.)
125
- const allowMain = process.env.OTB_ALLOW_MAIN_PUSH === '1';
129
+ const allowMain = resolveEnv('ALLOW_MAIN_PUSH') === '1';
126
130
 
127
131
  const stdin = require('node:fs').readFileSync(0, 'utf8');
128
132
  const refs = stdin.split('\n').map((l) => l.trim()).filter(Boolean).map((l) => {
@@ -141,7 +145,7 @@ async function run() {
141
145
  'A direct push to main skips the grader, the confirm gate, parallel-safety, and',
142
146
  'credits — this is exactly the breach SEC #863 closes (ADR 0043).',
143
147
  '',
144
- '(Operator/deploy identity only: set OTB_ALLOW_MAIN_PUSH=1 to override.)',
148
+ `(Operator/deploy identity only: set ${envPrefix()}_ALLOW_MAIN_PUSH=1 to override.)`,
145
149
  ]);
146
150
  }
147
151
  }
@@ -50,6 +50,7 @@
50
50
  const { execSync } = require('node:child_process');
51
51
  const ppc = require(require('node:path').join(__dirname, '..', '..', 'src', 'bongos', 'permission-path-check.js'));
52
52
  const { cliClient, cliExit, API_BASE } = require('./cli-lib');
53
+ const { resolveEnv, envPrefix } = require(require('node:path').join(__dirname, '..', '..', 'src', 'instance-config.js'));
53
54
 
54
55
  // ---------- pure analyzer (tested in tests/main_audit.mjs) ----------
55
56
 
@@ -314,23 +315,31 @@ function arg(name, fallback = null) {
314
315
  return i >= 0 ? (process.argv[i + 1] && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : true) : fallback;
315
316
  }
316
317
 
318
+ // requestedRange — the range this run was asked to audit, or null for the default
319
+ // window. An explicit --range wins; otherwise the <PREFIX>_AUDIT_RANGE env knob
320
+ // (BONGOS_AUDIT_RANGE on a vanilla instance; the legacy OTB_AUDIT_RANGE is still
321
+ // read, and warned about). The deploy gate and the CI workflow hand the range over
322
+ // as --range, because neither can compute the branded env prefix from a shell
323
+ // (task 1001441).
324
+ function requestedRange(argValue, env = process.env) {
325
+ if (argValue && argValue !== true) return argValue;
326
+ return resolveEnv('AUDIT_RANGE', { env }) || null;
327
+ }
328
+
317
329
  async function main() {
318
330
  const verifyHead = process.argv.includes('--verify-head');
319
331
  const doRevert = process.argv.includes('--revert');
320
332
  const strict = process.argv.includes('--strict');
321
333
  const useDb = process.argv.includes('--db');
322
- let range = arg('--range');
323
334
  // #863-followup (findings #8/#14): the verify-head window was a fixed
324
335
  // HEAD~30..HEAD — a poisoned commit buried under >30 commits (merges inflate
325
336
  // the count) or landed before a rebase/force-push could escape it. The deploy
326
- // gate now passes the EXACT previously-deployed SHA via OTB_AUDIT_RANGE
327
- // (deploy-main-audit-gate.sh sets it to <prev-deployed-sha>..HEAD), so the
328
- // gate audits precisely what this deploy introduces — no window to slip under.
337
+ // gate now passes the EXACT previously-deployed SHA as --range
338
+ // <prev-deployed-sha>..HEAD (deploy-main-audit-gate.sh), so the gate audits
339
+ // precisely what this deploy introduces — no window to slip under.
329
340
  // Fallbacks widen to HEAD~200 (verify-head) / a deeper origin scan otherwise.
330
- if ((!range || range === true) && process.env.OTB_AUDIT_RANGE) {
331
- range = process.env.OTB_AUDIT_RANGE;
332
- }
333
- if (!range || range === true) {
341
+ let range = requestedRange(arg('--range'));
342
+ if (!range) {
334
343
  const ref = verifyHead ? 'HEAD' : 'origin/main';
335
344
  if (!verifyHead) {
336
345
  // Default: recent origin/main. Fetch first so we're auditing the truth.
@@ -344,7 +353,7 @@ async function main() {
344
353
  console.error(
345
354
  `main-audit: DID NOT RUN — cannot build a default range for ${ref}: its first-parent history is `
346
355
  + `${Number.isFinite(depth) ? `${depth} commit(s)` : 'unreadable'} deep. `
347
- + 'Pass --range <A..B> or set OTB_AUDIT_RANGE. This is a check that did not execute, not a clean audit.'
356
+ + `Pass --range <A..B> or set ${envPrefix()}_AUDIT_RANGE. This is a check that did not execute, not a clean audit.`
348
357
  );
349
358
  cliExit(2);
350
359
  return;
@@ -411,7 +420,7 @@ if (require.main === module) {
411
420
 
412
421
  module.exports = {
413
422
  analyzeCommits, resolveRank, parseCommits, rowsToMap,
414
- clampWindow, defaultRange, firstParentDepth, DEFAULT_WINDOW,
423
+ clampWindow, defaultRange, firstParentDepth, DEFAULT_WINDOW, requestedRange,
415
424
  // Exported so a test can assert the address is LIVE without a database. The --db
416
425
  // path was dead for as long as the db.js carve has been landed precisely because
417
426
  // nothing checked that this require target still exports what it is called for.
@@ -22,6 +22,7 @@
22
22
  'use strict';
23
23
  const fs = require('node:fs');
24
24
  const path = require('node:path');
25
+ const { resolveEnv } = require('../../src/instance-config');
25
26
 
26
27
  // Budgets, in bytes of MEMORY.md (~4 chars/token for markdown prose).
27
28
  const WARN_BYTES = 16000; // ~4K tok — drifting up; warn so it gets tidied.
@@ -157,7 +158,8 @@ module.exports = {
157
158
  if (require.main === module) {
158
159
  const args = process.argv.slice(2);
159
160
  const dirArg = args.indexOf('--dir');
160
- const memoryDir = dirArg >= 0 ? args[dirArg + 1] : process.env.OTB_MEMORY_DIR;
161
+ // <PREFIX>_MEMORY_DIR (BONGOS_ on a vanilla instance; the legacy OTB_ still read).
162
+ const memoryDir = dirArg >= 0 ? args[dirArg + 1] : resolveEnv('MEMORY_DIR');
161
163
  if (!memoryDir) { console.error('usage: memory-map.js --dir <memoryDir> [--apply]'); process.exit(2); }
162
164
  const mapPath = path.join(memoryDir, 'MEMORY.md');
163
165
  let text; try { text = fs.readFileSync(mapPath, 'utf8'); } catch { console.error('no MEMORY.md at', mapPath); process.exit(0); }
@@ -27,11 +27,12 @@ const MANIFEST_PATH = path.join(SPOOL_DIR, 'manifest.json');
27
27
  // How long the onboarding window stays open after the consent decision.
28
28
  // Friction-mining is for the first stretch of a builder's life; after that
29
29
  // they're not "onboarding" and shipping their transcripts would be noise (and
30
- // a privacy overreach). Override for tests via OTB_ONBOARDING_WINDOW_DAYS.
30
+ // a privacy overreach). Override for tests via <PREFIX>_ONBOARDING_WINDOW_DAYS
31
+ // (BONGOS_ on a vanilla instance; the legacy OTB_ spelling is still read).
31
32
  const DEFAULT_WINDOW_DAYS = 14;
32
33
 
33
34
  function windowDays() {
34
- const env = Number(process.env.OTB_ONBOARDING_WINDOW_DAYS);
35
+ const env = Number(ic.resolveEnv('ONBOARDING_WINDOW_DAYS'));
35
36
  return Number.isFinite(env) && env > 0 ? env : DEFAULT_WINDOW_DAYS;
36
37
  }
37
38