@event4u/agent-config 9.4.0 → 9.5.0
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/docs/CLAIMS.md +7 -0
- package/docs/benchmark.md +40 -0
- package/docs/proof.md +2 -1
- package/package.json +1 -1
- package/src/scripts/bench_defect_finding.ts +348 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Bootstrap shim for the event4u agent-config suite. This plugin ships ONLY the dispatcher hooks plus one install-pointer skill — ALL content (skills, rules, commands) is installed via `npx -y @event4u/agent-config init`, which writes the file projection AND registers the same hooks in ~/.claude/settings.json. DEPRECATED as a content channel; kept so the marketplace listing stays discoverable and existing installs keep working hooks.",
|
|
9
|
-
"version": "9.
|
|
9
|
+
"version": "9.5.0",
|
|
10
10
|
"keywords": [
|
|
11
11
|
"agent-config",
|
|
12
12
|
"skills",
|
package/CHANGELOG.md
CHANGED
|
@@ -1056,6 +1056,20 @@ our recommendation order, not its support status.
|
|
|
1056
1056
|
> that forces a new era split (`# Era: 9.3.x`, etc.) — see
|
|
1057
1057
|
> [`docs/contracts/CHANGELOG-conventions.md § Era splits`](docs/contracts/CHANGELOG-conventions.md).
|
|
1058
1058
|
|
|
1059
|
+
## [9.5.0](https://github.com/event4u-app/agent-config/compare/9.4.0...9.5.0) (2026-07-20)
|
|
1060
|
+
|
|
1061
|
+
### Features
|
|
1062
|
+
|
|
1063
|
+
* **bench:** defect-finding runner + results (team-mode Phase 5 Steps 2-4) ([aca13fd](https://github.com/event4u-app/agent-config/commit/aca13fd0c212a1af31c0715bf56db1972d819d0b))
|
|
1064
|
+
* **bench:** pre-registered defect-finding corpus (team-mode Phase 5 Step 1) ([e7b5300](https://github.com/event4u-app/agent-config/commit/e7b5300d39fa6f2e9ac4fb0d42934cb08d8a9382))
|
|
1065
|
+
|
|
1066
|
+
### Documentation
|
|
1067
|
+
|
|
1068
|
+
* **roadmap:** close team-mode Phase 5 on the honest-null verdict ([2ff25dd](https://github.com/event4u-app/agent-config/commit/2ff25dd2837872f2ff36139839abc9211d7e7305))
|
|
1069
|
+
* **proof:** record the team-mode defect-finding honest null (Phase 5 Step 4-5) ([908284a](https://github.com/event4u-app/agent-config/commit/908284a9dc20d067aaaf1305b3c352f260c88e82))
|
|
1070
|
+
|
|
1071
|
+
Tests: 7833 (+0 since 9.4.0)
|
|
1072
|
+
|
|
1059
1073
|
## [9.4.0](https://github.com/event4u-app/agent-config/compare/9.3.0...9.4.0) (2026-07-20)
|
|
1060
1074
|
|
|
1061
1075
|
### Features
|
|
@@ -39,7 +39,7 @@ schema_version: 1
|
|
|
39
39
|
# CI guard: a release bump of `package.json` must update this value
|
|
40
40
|
# in lockstep — see scripts/check_template_pin_drift.ts (road-to-
|
|
41
41
|
# portable-runtime-and-update-check P3.3).
|
|
42
|
-
agent_config_version: "9.
|
|
42
|
+
agent_config_version: "9.5.0"
|
|
43
43
|
|
|
44
44
|
# --- Project identity ---
|
|
45
45
|
project:
|
|
@@ -13924,7 +13924,7 @@
|
|
|
13924
13924
|
"reason": "scaffold template for ticket-bundle tickets (ADR-101); carries no runtime frontmatter — emit-tickets fills it in per ticket"
|
|
13925
13925
|
}
|
|
13926
13926
|
],
|
|
13927
|
-
"generated_at": "2026-07-
|
|
13927
|
+
"generated_at": "2026-07-20T14:45:21Z",
|
|
13928
13928
|
"packs": [
|
|
13929
13929
|
{
|
|
13930
13930
|
"artefact_count": 109,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
598c2a96c3ab8210a9c9b23eb4e4292c1ccd9a190179b8172a10a7eb11180f8a discovery-manifest.json
|
package/docs/CLAIMS.md
CHANGED
|
@@ -245,6 +245,13 @@ visible, not hidden.
|
|
|
245
245
|
- status: backed
|
|
246
246
|
- last_verified: 2026-07-12
|
|
247
247
|
|
|
248
|
+
### claim: team-defect-finding-null
|
|
249
|
+
- claim: On the pre-registered 12-fixture defect-finding corpus (three arms, deterministic file-level recall, codex reviewer gpt-5.5), the cross-model team-review arm produced NO recall lift over single-model self-review — all three arms recalled every planted defect (Δ = 0, H1 not met). Honest null, ceiling-limited (recall 1.00 everywhere: the seeded defects are too obvious to discriminate the arms on recall); the only non-null signal is a single self-review false positive on the controversial-but-correct control vs 0 for team/council. No cross-model quality/lift claim binds; team mode stays workflow-value-only. Re-open: a judge-survivable-subtlety corpus or a new model generation.
|
|
250
|
+
- kind: quant
|
|
251
|
+
- evidence: internal/bench/reports/defect-finding.json#honest_null
|
|
252
|
+
- status: backed
|
|
253
|
+
- last_verified: 2026-07-20
|
|
254
|
+
|
|
248
255
|
### claim: adversarial-council-finding-coverage
|
|
249
256
|
- claim: On the residual defect pool (planted defects that survive a single strong cross-model judge), an adversarial panel of >=2 distinct-vendor skeptics finds materially more residual defects than that single judge — relative residual-recall lift >= +25% AND absolute >= +8 percentage points — at a false-positive rate on a controversial-but-correct control no worse than the single-judge baseline (within noise). Scope: finding coverage, NOT decision quality (the separate, unbacked council-vs-solo-baseline question). Pre-registered; honest-null (either threshold missed or FP worse) keeps the surface off by default permanently.
|
|
250
257
|
- kind: quant
|
package/docs/benchmark.md
CHANGED
|
@@ -582,3 +582,43 @@ controversial-but-correct control.
|
|
|
582
582
|
judge-survivable-subtlety corpus with a published distribution, then (2) a
|
|
583
583
|
maintainer-gated paid cross-vendor run. Honest-null on that run keeps the
|
|
584
584
|
surface default-off permanently, like recursive-verification.
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
## Defect-finding: team vs self-review vs council (2026-07-20) — HONEST NULL (ceiling-limited) {#honest-null-defect}
|
|
588
|
+
|
|
589
|
+
Pre-registered 12-fixture corpus (`internal/bench/corpora/defect-finding.yaml`:
|
|
590
|
+
10 seeded-defect diffs across logic / off-by-one / race / missing-empty-state /
|
|
591
|
+
security-smell, + 2 controls), three arms, deterministic file-level recall
|
|
592
|
+
against ground truth (blind rubric judge deferred — the primary metric is
|
|
593
|
+
deterministic). Codex reviewer pinned `gpt-5.5`. Total billable $0.083 (arm b
|
|
594
|
+
codex = ChatGPT subscription, $0).
|
|
595
|
+
|
|
596
|
+
| arm | recall | correctness | design | false-positives |
|
|
597
|
+
|---|--:|--:|--:|--:|
|
|
598
|
+
| self-review (single model) | 1.00 | 1.00 | 1.00 | 1 |
|
|
599
|
+
| team (cross-model, codex) | 1.00 | 1.00 | 1.00 | 0 |
|
|
600
|
+
| council (neutral breadth) | 1.00 | 1.00 | 1.00 | 0 |
|
|
601
|
+
|
|
602
|
+
**Verdict: HONEST NULL.** H1 (cross-model team > single-model self-review on
|
|
603
|
+
correctness recall, Δ ≥ +0.20) is **not met** — Δ = 0: all three arms recalled
|
|
604
|
+
every planted defect. H2 (council ≈ team on design, within 0.10) is met (Δ = 0).
|
|
605
|
+
H3 (≤ 1 false positive/arm) is met.
|
|
606
|
+
|
|
607
|
+
**Corpus-validity caveat (the honest bound):** recall 1.00 across every arm is a
|
|
608
|
+
**ceiling effect** — these seeded defects are catchable by any strong model, so
|
|
609
|
+
the corpus cannot discriminate the arms on recall. This is the same limitation
|
|
610
|
+
the adversarial-verification-council section names: a corpus of obvious,
|
|
611
|
+
model-differentiating defects measures parity, not the judge-survivable
|
|
612
|
+
subtleties where a cross-model lens might actually differ. The one non-null
|
|
613
|
+
signal is precision, not recall: single-model self-review produced 1 false
|
|
614
|
+
positive on the controversial-but-correct control (`df-ctl-01`, a
|
|
615
|
+
behaviour-preserving `clamp` refactor) where team and council produced 0 — a
|
|
616
|
+
hint of a multi-arm precision edge, within the pre-registered H3 bound, too
|
|
617
|
+
small to claim.
|
|
618
|
+
|
|
619
|
+
**Disposition (Phase 5 Step 5): evidence-closed as NULL.** No cross-model
|
|
620
|
+
defect-finding *quality/lift* claim binds; team mode stays documented as
|
|
621
|
+
**workflow value only**. Re-open conditions: (a) a curated
|
|
622
|
+
judge-survivable-subtlety corpus that breaks the recall ceiling, or (b) a new
|
|
623
|
+
model generation. The worker-via-bundle delegate (roadmap Phase 3 Step 4) stays
|
|
624
|
+
deferred — it re-opened only on a measured review lift, which did not occur.
|
package/docs/proof.md
CHANGED
|
@@ -48,9 +48,10 @@ evidence pointer, or `task check-claims` fails the build.
|
|
|
48
48
|
| Every artifact the package ships — source AND the condensed projection that reaches consumers — is machine-scanned in CI for hidden-Unicode, mixed-script-confusable, and instruction-smuggling payloads (the rules-file-backdoor class); a finding blocks the release before `npm publish`, not just the merge. | qual | `.github/workflows/publish-npm.yml#lint_agent_security` | ✅ |
|
|
49
49
|
| 278 skills. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
|
|
50
50
|
| Removes only its own keys from a shared host config (matched by JSON-pointer + SHA-256), never a neighbour tool's entries. | qual | `docs/contracts/install-layout.md#JSON-pointer` | ✅ |
|
|
51
|
+
| On the pre-registered 12-fixture defect-finding corpus (three arms, deterministic file-level recall, codex reviewer gpt-5.5), the cross-model team-review arm produced NO recall lift over single-model self-review — all three arms recalled every planted defect (Δ = 0, H1 not met). Honest null, ceiling-limited (recall 1.00 everywhere: the seeded defects are too obvious to discriminate the arms on recall); the only non-null signal is a single self-review false positive on the controversial-but-correct control vs 0 for team/council. No cross-model quality/lift claim binds; team mode stays workflow-value-only. Re-open: a judge-survivable-subtlety corpus or a new model generation. | quant | `internal/bench/reports/defect-finding.json#honest_null` | ✅ |
|
|
51
52
|
| On the A3 orchestration eval (deterministic verify, measured token deltas), the production-validator subagent returned the correct verdict on both fixtures — NOT READY with the exact `file:line` citation on a planted hollow implementation, READY with zero spurious findings on the clean control — while consuming ~45k fewer tokens than the inline-host baseline on each task. Scope: two planted fixtures on a Claude Code host, not a broad hit-rate. | quant | `internal/bench/orchestration/pv-a3-results.md#token_delta_provenance: measured` | ✅ |
|
|
52
53
|
|
|
53
|
-
**
|
|
54
|
+
**24 backed claim(s)** — all evidence pointers resolve in CI.
|
|
54
55
|
|
|
55
56
|
Artefact counts in public prose (skills, commands, governed rules,
|
|
56
57
|
guidelines, personas) are **generated from source and CI-drift-checked**:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.5.0",
|
|
4
4
|
"description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Defect-finding benchmark runner (road-to-team-mode Phase 5, Steps 2-4).
|
|
4
|
+
*
|
|
5
|
+
* Three arms over the pre-registered corpus (internal/bench/corpora/defect-finding.yaml):
|
|
6
|
+
* (a) self-review — single strong model (Anthropic) + the adversarial-review frame
|
|
7
|
+
* (b) team — codex exec (subscription; reviewer pinned via --codex-model)
|
|
8
|
+
* (c) council — the neutral breadth arm: Anthropic + OpenAI independent reviews, union
|
|
9
|
+
*
|
|
10
|
+
* Every arm gets the SAME strict output contract so scoring is fair + objective:
|
|
11
|
+
* a review must end with `VERDICT: <n>` and one `DEFECT: <file>:<line> — <why>`
|
|
12
|
+
* line per defect. Recall + false-positives are then scored DETERMINISTICALLY
|
|
13
|
+
* against the corpus ground truth (no LLM judge for the PRIMARY metric — the
|
|
14
|
+
* blind rubric judge is the SECONDARY metric and is deferred this pass).
|
|
15
|
+
*
|
|
16
|
+
* Spend: arm (a) + arm (c) are billable API calls; arm (b) is the ChatGPT
|
|
17
|
+
* subscription (non-billable through this process). `--max-usd` is a HARD cap:
|
|
18
|
+
* the run aborts before a call that would cross it. `--dry-run` makes zero calls.
|
|
19
|
+
*
|
|
20
|
+
* Exit 0 ok · 1 aborted-on-cap / error · 2 usage.
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
AnthropicClient,
|
|
31
|
+
OpenAIClient,
|
|
32
|
+
load_anthropic_key,
|
|
33
|
+
load_openai_key,
|
|
34
|
+
} from './ai_council/clients.js';
|
|
35
|
+
|
|
36
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const ROOT = path.resolve(HERE, '..', '..');
|
|
38
|
+
const CORPUS = path.join(ROOT, 'internal', 'bench', 'corpora', 'defect-finding.yaml');
|
|
39
|
+
const OUT_JSON = path.join(ROOT, 'internal', 'bench', 'reports', 'defect-finding.json');
|
|
40
|
+
const OUT_MD = path.join(ROOT, 'internal', 'bench', 'reports', 'defect-finding.md');
|
|
41
|
+
|
|
42
|
+
const OUTPUT_CONTRACT =
|
|
43
|
+
'\n\nReport STRICTLY in this format, nothing after it:\n' +
|
|
44
|
+
'DEFECT: <file>:<line> — <one-line why> (one line per real defect; omit if none)\n' +
|
|
45
|
+
'VERDICT: <integer count of defects>';
|
|
46
|
+
|
|
47
|
+
interface Fixture {
|
|
48
|
+
id: string;
|
|
49
|
+
defect_class: string;
|
|
50
|
+
prompt: string;
|
|
51
|
+
ground_truth: string;
|
|
52
|
+
rubric: string;
|
|
53
|
+
negative_control?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ArmId = 'self-review' | 'team' | 'council';
|
|
57
|
+
|
|
58
|
+
interface ArmResult {
|
|
59
|
+
arm: ArmId;
|
|
60
|
+
text: string;
|
|
61
|
+
calls: number;
|
|
62
|
+
input_tokens: number;
|
|
63
|
+
output_tokens: number;
|
|
64
|
+
usd: number;
|
|
65
|
+
ms: number;
|
|
66
|
+
error: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Ground-truth FILENAME for a fixture, or null for a control. Each planted
|
|
70
|
+
* fixture touches exactly ONE unique file, so "a DEFECT line names this file"
|
|
71
|
+
* is the honest, line-number-robust recall signal (diff line numbering is
|
|
72
|
+
* ambiguous and models cite lines inconsistently — matching the exact
|
|
73
|
+
* file:line token systematically under-counts real finds). */
|
|
74
|
+
function groundTruthFile(gt: string): string | null {
|
|
75
|
+
if (/^\s*NONE\b/i.test(gt)) return null;
|
|
76
|
+
const head = gt.split('—')[0] ?? gt;
|
|
77
|
+
const m = /([\w./-]+\.\w+):[\d,]+/.exec(head);
|
|
78
|
+
return m ? (m[1] as string) : null;
|
|
79
|
+
}
|
|
80
|
+
/** Basename of a path token (find.ts from src/search/find.ts). */
|
|
81
|
+
function basename(p: string): string { return p.split('/').pop() ?? p; }
|
|
82
|
+
|
|
83
|
+
/** Parse the DEFECT lines an arm emitted (file:line tokens it claims). */
|
|
84
|
+
function parseClaimedDefects(text: string): { count: number; files: string[] } {
|
|
85
|
+
const files: string[] = [];
|
|
86
|
+
for (const line of text.split('\n')) {
|
|
87
|
+
// DEFECT: <file>[:line] — ... ; capture the file token (line optional).
|
|
88
|
+
const m = /^\s*DEFECT:\s*([\w./-]+\.\w+)/.exec(line);
|
|
89
|
+
if (m) files.push(m[1] as string);
|
|
90
|
+
}
|
|
91
|
+
const vm = /VERDICT:\s*(\d+)/i.exec(text);
|
|
92
|
+
const count = vm ? Number(vm[1]) : files.length;
|
|
93
|
+
return { count, files };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Deterministic recall/FP for one arm on one fixture. */
|
|
97
|
+
function score(
|
|
98
|
+
fx: Fixture,
|
|
99
|
+
arm: ArmResult,
|
|
100
|
+
): { recall_hit: boolean; false_positive: boolean; claimed: number } {
|
|
101
|
+
const gtFile = groundTruthFile(fx.ground_truth);
|
|
102
|
+
const claimed = parseClaimedDefects(arm.text);
|
|
103
|
+
if (gtFile === null || fx.negative_control === true) {
|
|
104
|
+
// Control: any claimed defect is a false positive.
|
|
105
|
+
return { recall_hit: false, false_positive: claimed.count > 0, claimed: claimed.count };
|
|
106
|
+
}
|
|
107
|
+
// Recall: a DEFECT line names the planted file (by basename — robust to the
|
|
108
|
+
// model prefixing or omitting the dir). Line-agnostic on purpose.
|
|
109
|
+
const target = basename(gtFile);
|
|
110
|
+
const hit = claimed.files.some((f) => basename(f) === target) || arm.text.includes(target);
|
|
111
|
+
return { recall_hit: hit, false_positive: false, claimed: claimed.count };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function usd(inTok: number, outTok: number, inRate: number, outRate: number): number {
|
|
115
|
+
return (inTok / 1_000_000) * inRate + (outTok / 1_000_000) * outRate;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Conservative published-tier rates (per 1M tokens) for the estimate + tally.
|
|
119
|
+
const RATES = {
|
|
120
|
+
anthropic: { in: 3, out: 15 }, // claude-sonnet class
|
|
121
|
+
openai: { in: 2.5, out: 10 }, // gpt-4o class
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
interface Budget {
|
|
125
|
+
cap: number;
|
|
126
|
+
spent: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function guard(b: Budget, next: number): void {
|
|
130
|
+
if (b.cap > 0 && b.spent + next > b.cap) {
|
|
131
|
+
throw new Error(`hard cap $${b.cap} would be crossed (spent $${b.spent.toFixed(4)}, next ~$${next.toFixed(4)})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function armSelfReview(fx: Fixture, key: string, b: Budget): ArmResult {
|
|
136
|
+
const t0 = Date.now();
|
|
137
|
+
guard(b, 0.05);
|
|
138
|
+
const client = new AnthropicClient({ api_key: key });
|
|
139
|
+
const sys =
|
|
140
|
+
'You are a single-model adversarial code reviewer (Attack-Defend-Revise). ' +
|
|
141
|
+
'Find real defects in the diff; do not invent problems in correct code.';
|
|
142
|
+
const r = client.ask(sys, fx.prompt + OUTPUT_CONTRACT, 1024);
|
|
143
|
+
const cost = usd(r.input_tokens, r.output_tokens, RATES.anthropic.in, RATES.anthropic.out);
|
|
144
|
+
b.spent += cost;
|
|
145
|
+
return {
|
|
146
|
+
arm: 'self-review', text: r.text, calls: 1,
|
|
147
|
+
input_tokens: r.input_tokens, output_tokens: r.output_tokens,
|
|
148
|
+
usd: cost, ms: Date.now() - t0, error: r.error,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function armTeam(fx: Fixture, model: string, _b: Budget): ArmResult {
|
|
153
|
+
const t0 = Date.now();
|
|
154
|
+
// codex = ChatGPT subscription: non-billable through this process. `--json`
|
|
155
|
+
// streams events; the review is the LAST agent_message item's text (the
|
|
156
|
+
// interactive mode without --json blocks/hangs — must use --json here).
|
|
157
|
+
let text = '';
|
|
158
|
+
let error: string | null = null;
|
|
159
|
+
try {
|
|
160
|
+
const raw = execFileSync('codex', ['exec', '--json', '-m', model, fx.prompt + OUTPUT_CONTRACT], {
|
|
161
|
+
encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: 180_000,
|
|
162
|
+
});
|
|
163
|
+
for (const line of raw.split('\n')) {
|
|
164
|
+
const s = line.trim();
|
|
165
|
+
if (!s.startsWith('{')) continue;
|
|
166
|
+
try {
|
|
167
|
+
const ev = JSON.parse(s) as { item?: { type?: string; text?: string } };
|
|
168
|
+
if (ev.item?.type === 'agent_message' && typeof ev.item.text === 'string') {
|
|
169
|
+
text = ev.item.text; // keep the last agent_message
|
|
170
|
+
}
|
|
171
|
+
} catch { /* skip non-JSON / partial lines */ }
|
|
172
|
+
}
|
|
173
|
+
if (!text) error = 'no agent_message in codex --json stream';
|
|
174
|
+
} catch (e) {
|
|
175
|
+
error = e instanceof Error ? e.message.slice(0, 300) : String(e);
|
|
176
|
+
}
|
|
177
|
+
return { arm: 'team', text, calls: 1, input_tokens: 0, output_tokens: 0, usd: 0, ms: Date.now() - t0, error };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function armCouncil(fx: Fixture, aKey: string, oKey: string, b: Budget): ArmResult {
|
|
181
|
+
const t0 = Date.now();
|
|
182
|
+
const sys =
|
|
183
|
+
'You are a neutral code reviewer. Review ONLY the supplied diff for real ' +
|
|
184
|
+
'defects; never invent problems in correct code.';
|
|
185
|
+
guard(b, 0.05);
|
|
186
|
+
const a = new AnthropicClient({ api_key: aKey }).ask(sys, fx.prompt + OUTPUT_CONTRACT, 1024);
|
|
187
|
+
let cost = usd(a.input_tokens, a.output_tokens, RATES.anthropic.in, RATES.anthropic.out);
|
|
188
|
+
b.spent += cost;
|
|
189
|
+
guard(b, 0.05);
|
|
190
|
+
const o = new OpenAIClient({ api_key: oKey }).ask(sys, fx.prompt + OUTPUT_CONTRACT, 1024);
|
|
191
|
+
const oc = usd(o.input_tokens, o.output_tokens, RATES.openai.in, RATES.openai.out);
|
|
192
|
+
b.spent += oc;
|
|
193
|
+
cost += oc;
|
|
194
|
+
// Council = breadth: union the two independent reviews.
|
|
195
|
+
const text = `# reviewer A\n${a.text}\n\n# reviewer B\n${o.text}`;
|
|
196
|
+
return {
|
|
197
|
+
arm: 'council', text, calls: 2,
|
|
198
|
+
input_tokens: a.input_tokens + o.input_tokens,
|
|
199
|
+
output_tokens: a.output_tokens + o.output_tokens,
|
|
200
|
+
usd: cost, ms: Date.now() - t0, error: a.error ?? o.error,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const CORRECTNESS = new Set(['logic-bug', 'off-by-one', 'race']);
|
|
205
|
+
const DESIGN = new Set(['missing-empty-state', 'security-smell']);
|
|
206
|
+
|
|
207
|
+
export function main(argv: string[]): 0 | 1 | 2 {
|
|
208
|
+
const dryRun = argv.includes('--dry-run');
|
|
209
|
+
const capIdx = argv.indexOf('--max-usd');
|
|
210
|
+
const cap = capIdx >= 0 ? Number(argv[capIdx + 1]) : 1.2;
|
|
211
|
+
const modelIdx = argv.indexOf('--codex-model');
|
|
212
|
+
const codexModel = (modelIdx >= 0 ? argv[modelIdx + 1] : undefined) ?? 'gpt-5.5';
|
|
213
|
+
|
|
214
|
+
const doc = yamlLoad(fs.readFileSync(CORPUS, 'utf8')) as { fixtures: Fixture[] };
|
|
215
|
+
const fixtures = doc.fixtures;
|
|
216
|
+
|
|
217
|
+
if (dryRun) {
|
|
218
|
+
const planted = fixtures.filter((f) => !f.negative_control).length;
|
|
219
|
+
process.stdout.write(
|
|
220
|
+
`defect-finding (dry-run — no calls):\n` +
|
|
221
|
+
` fixtures: ${fixtures.length} (${planted} planted, ${fixtures.length - planted} control)\n` +
|
|
222
|
+
` arms: self-review (anthropic, ${fixtures.length} calls) · team (codex ${codexModel}, ${fixtures.length} calls, subscription) · council (anthropic+openai, ${fixtures.length * 2} calls)\n` +
|
|
223
|
+
` billable estimate: arm a ~$${(fixtures.length * 0.02).toFixed(2)} + arm c ~$${(fixtures.length * 0.04).toFixed(2)} ≈ $${(fixtures.length * 0.06).toFixed(2)} (hard cap $${cap})\n` +
|
|
224
|
+
` arm b (codex): 0 billable (ChatGPT subscription quota)\n`,
|
|
225
|
+
);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const aKey = load_anthropic_key();
|
|
230
|
+
const oKey = load_openai_key();
|
|
231
|
+
const budget: Budget = { cap, spent: 0 };
|
|
232
|
+
const rows: Array<{ fx: Fixture; arms: Record<ArmId, ArmResult> }> = [];
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
for (const fx of fixtures) {
|
|
236
|
+
process.stdout.write(`· ${fx.id} (${fx.defect_class})\n`);
|
|
237
|
+
const arms = {
|
|
238
|
+
'self-review': armSelfReview(fx, aKey, budget),
|
|
239
|
+
team: armTeam(fx, codexModel, budget),
|
|
240
|
+
council: armCouncil(fx, aKey, oKey, budget),
|
|
241
|
+
} as Record<ArmId, ArmResult>;
|
|
242
|
+
rows.push({ fx, arms });
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
process.stderr.write(`ABORTED: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
246
|
+
// fall through to write a partial report
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── aggregate ──
|
|
250
|
+
const ARMS: ArmId[] = ['self-review', 'team', 'council'];
|
|
251
|
+
const agg: Record<string, unknown> = {};
|
|
252
|
+
const perArm: Record<ArmId, { recall: number; planted: number; fp: number; usd: number; ms: number; calls: number; byClass: Record<string, { hit: number; n: number }> }> =
|
|
253
|
+
Object.fromEntries(ARMS.map((a) => [a, { recall: 0, planted: 0, fp: 0, usd: 0, ms: 0, calls: 0, byClass: {} }])) as never;
|
|
254
|
+
|
|
255
|
+
const detail: unknown[] = [];
|
|
256
|
+
for (const { fx, arms } of rows) {
|
|
257
|
+
const d: Record<string, unknown> = { id: fx.id, defect_class: fx.defect_class, control: !!fx.negative_control };
|
|
258
|
+
for (const a of ARMS) {
|
|
259
|
+
const ar = arms[a];
|
|
260
|
+
const sc = score(fx, ar);
|
|
261
|
+
const pa = perArm[a];
|
|
262
|
+
pa.usd += ar.usd; pa.ms += ar.ms; pa.calls += ar.calls;
|
|
263
|
+
if (!fx.negative_control) {
|
|
264
|
+
pa.planted += 1;
|
|
265
|
+
if (sc.recall_hit) pa.recall += 1;
|
|
266
|
+
const c = (pa.byClass[fx.defect_class] ??= { hit: 0, n: 0 });
|
|
267
|
+
c.n += 1; if (sc.recall_hit) c.hit += 1;
|
|
268
|
+
} else if (sc.false_positive) {
|
|
269
|
+
pa.fp += 1;
|
|
270
|
+
}
|
|
271
|
+
d[a] = { recall_hit: sc.recall_hit, false_positive: sc.false_positive, claimed: sc.claimed, usd: Number(ar.usd.toFixed(4)), ms: ar.ms, error: ar.error, review: ar.text.slice(0, 1200) };
|
|
272
|
+
}
|
|
273
|
+
detail.push(d);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const recallRate = (a: ArmId): number => (perArm[a].planted ? perArm[a].recall / perArm[a].planted : 0);
|
|
277
|
+
const classRecall = (a: ArmId, classes: Set<string>): { hit: number; n: number } => {
|
|
278
|
+
let hit = 0, n = 0;
|
|
279
|
+
for (const [cls, c] of Object.entries(perArm[a].byClass)) if (classes.has(cls)) { hit += c.hit; n += c.n; }
|
|
280
|
+
return { hit, n };
|
|
281
|
+
};
|
|
282
|
+
const rr = (x: { hit: number; n: number }): number => (x.n ? x.hit / x.n : 0);
|
|
283
|
+
|
|
284
|
+
// Hypotheses (pre-registered thresholds).
|
|
285
|
+
const aCorr = classRecall('self-review', CORRECTNESS), bCorr = classRecall('team', CORRECTNESS);
|
|
286
|
+
const bDes = classRecall('team', DESIGN), cDes = classRecall('council', DESIGN);
|
|
287
|
+
const h1 = rr(bCorr) - rr(aCorr); // team - self on correctness; predict >= +0.20
|
|
288
|
+
const h2 = Math.abs(rr(cDes) - rr(bDes)); // council vs team on design; predict within 0.10
|
|
289
|
+
const h3ok = ARMS.every((a) => perArm[a].fp <= 1);
|
|
290
|
+
|
|
291
|
+
agg['preregistered'] = {
|
|
292
|
+
roadmap: 'road-to-team-mode Phase 5', authored: '2026-07-20',
|
|
293
|
+
primary: ['recall', 'false_positives', 'cost/time/calls'],
|
|
294
|
+
secondary: 'blind rubric judge — DEFERRED this pass (primary recall is deterministic)',
|
|
295
|
+
hypotheses: { H1: 'team>self on correctness, Δrecall>=+0.20', H2: 'council≈team on design, within 0.10', H3: 'FP<=1/arm on controls' },
|
|
296
|
+
codex_model: codexModel, models_cache_fetched: '2026-07-20T12:21Z',
|
|
297
|
+
};
|
|
298
|
+
agg['arms'] = Object.fromEntries(ARMS.map((a) => [a, {
|
|
299
|
+
recall_rate: Number(recallRate(a).toFixed(3)), recall: perArm[a].recall, planted: perArm[a].planted,
|
|
300
|
+
false_positives: perArm[a].fp, usd: Number(perArm[a].usd.toFixed(4)), ms: perArm[a].ms, calls: perArm[a].calls,
|
|
301
|
+
correctness_recall: Number(rr(classRecall(a, CORRECTNESS)).toFixed(3)),
|
|
302
|
+
design_recall: Number(rr(classRecall(a, DESIGN)).toFixed(3)),
|
|
303
|
+
}]));
|
|
304
|
+
const nullish = Math.abs(h1) <= 0.1 && h2 <= 0.1;
|
|
305
|
+
agg['verdict'] = {
|
|
306
|
+
H1_team_minus_self_correctness: Number(h1.toFixed(3)), H1_met: h1 >= 0.2,
|
|
307
|
+
H2_council_vs_team_design_absdelta: Number(h2.toFixed(3)), H2_met: h2 <= 0.1,
|
|
308
|
+
H3_fp_within_bound: h3ok,
|
|
309
|
+
honest_null: nullish,
|
|
310
|
+
disposition: nullish
|
|
311
|
+
? 'HONEST NULL — arms indistinguishable within pre-registered thresholds; no lift claim binds.'
|
|
312
|
+
: h1 >= 0.2
|
|
313
|
+
? 'LIFT on correctness-class — H1 met; a team-lift CLAIM may bind (maintainer decision).'
|
|
314
|
+
: 'MIXED — see per-class rates; no blanket lift claim.',
|
|
315
|
+
};
|
|
316
|
+
agg['total_billable_usd'] = Number((perArm['self-review'].usd + perArm.council.usd).toFixed(4));
|
|
317
|
+
agg['detail'] = detail;
|
|
318
|
+
|
|
319
|
+
fs.mkdirSync(path.dirname(OUT_JSON), { recursive: true });
|
|
320
|
+
fs.writeFileSync(OUT_JSON, JSON.stringify(agg, null, 2) + '\n');
|
|
321
|
+
|
|
322
|
+
const v = agg['verdict'] as Record<string, unknown>;
|
|
323
|
+
const md = [
|
|
324
|
+
'# Defect-finding benchmark — results (road-to-team-mode Phase 5)',
|
|
325
|
+
'', `> Run 2026-07-20. Codex reviewer: \`${codexModel}\`. Primary metric = deterministic`,
|
|
326
|
+
'> recall against corpus ground truth; blind rubric judge (secondary) deferred.',
|
|
327
|
+
'', '## Per-arm', '', '| arm | recall | correctness | design | false-pos | calls | $ |', '|---|--:|--:|--:|--:|--:|--:|',
|
|
328
|
+
...ARMS.map((a) => {
|
|
329
|
+
const x = (agg['arms'] as Record<string, Record<string, number>>)[a] as Record<string, number>;
|
|
330
|
+
return `| ${a} | ${x.recall_rate} | ${x.correctness_recall} | ${x.design_recall} | ${x.false_positives} | ${x.calls} | ${x.usd} |`;
|
|
331
|
+
}),
|
|
332
|
+
'', '## Verdict', '',
|
|
333
|
+
`- H1 (team − self, correctness recall Δ): **${v.H1_team_minus_self_correctness}** — met: ${v.H1_met}`,
|
|
334
|
+
`- H2 (council vs team, design recall |Δ|): **${v.H2_council_vs_team_design_absdelta}** — met: ${v.H2_met}`,
|
|
335
|
+
`- H3 (false positives ≤ 1/arm): ${v.H3_fp_within_bound}`,
|
|
336
|
+
'', `**Disposition:** ${v.disposition}`,
|
|
337
|
+
'', `Total billable: $${agg['total_billable_usd']} (arm b codex = subscription, $0 billable).`,
|
|
338
|
+
'',
|
|
339
|
+
].join('\n');
|
|
340
|
+
fs.writeFileSync(OUT_MD, md);
|
|
341
|
+
process.stdout.write(`\nwrote ${path.relative(ROOT, OUT_JSON)} + ${path.relative(ROOT, OUT_MD)}\n`);
|
|
342
|
+
process.stdout.write(`spent ~$${budget.spent.toFixed(4)} (cap $${cap})\n`);
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (fs.existsSync(process.argv[1] ?? '') && import.meta.url === `file://${process.argv[1]}`) {
|
|
347
|
+
process.exit(main(process.argv.slice(2)));
|
|
348
|
+
}
|