@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.
@@ -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.4.0",
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.4.0"
42
+ agent_config_version: "9.5.0"
43
43
 
44
44
  # --- Project identity ---
45
45
  project:
@@ -1,6 +1,6 @@
1
1
  # Discovery — Deprecation Report
2
2
 
3
- - Generated: `2026-07-20T09:32:39Z`
3
+ - Generated: `2026-07-20T14:45:21Z`
4
4
  - Deprecated artefacts: **0**
5
5
 
6
6
  _None. Tree is clean._
@@ -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-20T09:32:39Z",
13927
+ "generated_at": "2026-07-20T14:45:21Z",
13928
13928
  "packs": [
13929
13929
  {
13930
13930
  "artefact_count": 109,
@@ -1 +1 @@
1
- a59f1fc1e9fccbdf2e6b8b99257e83439d96bb9b0c2ad694f2cfece80671657c discovery-manifest.json
1
+ 598c2a96c3ab8210a9c9b23eb4e4292c1ccd9a190179b8172a10a7eb11180f8a discovery-manifest.json
@@ -1,6 +1,6 @@
1
1
  # Discovery Manifest — Summary
2
2
 
3
- - Generated: `2026-07-20T09:32:39Z`
3
+ - Generated: `2026-07-20T14:45:21Z`
4
4
  - Scanner: `c24312e116f8`
5
5
  - Artefacts: **575**
6
6
  - Unassigned: **0**
@@ -1,6 +1,6 @@
1
1
  # Discovery — Orphan Report
2
2
 
3
- - Generated: `2026-07-20T09:32:39Z`
3
+ - Generated: `2026-07-20T14:45:21Z`
4
4
  - Orphan artefacts: **0**
5
5
 
6
6
  > An orphan is an artefact whose declared pack has no other members.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "checksum": "sha256:16d037e41d10dcc83bb73c58b96e6aa3377ce5feb56a8d23aee0ef58e8a97c5c",
3
- "generated_at": "2026-07-20T09:32:39Z",
3
+ "generated_at": "2026-07-20T14:45:21Z",
4
4
  "packs": [
5
5
  {
6
6
  "always_on": false,
@@ -1,6 +1,6 @@
1
1
  # Discovery — Trust Report
2
2
 
3
- - Generated: `2026-07-20T09:32:39Z`
3
+ - Generated: `2026-07-20T14:45:21Z`
4
4
  - Workspaces tracked: **10**
5
5
  - Human-review-required artefacts: **3**
6
6
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "checksum": "sha256:16d037e41d10dcc83bb73c58b96e6aa3377ce5feb56a8d23aee0ef58e8a97c5c",
3
- "generated_at": "2026-07-20T09:32:39Z",
3
+ "generated_at": "2026-07-20T14:45:21Z",
4
4
  "scanner_version": "c24312e116f8",
5
5
  "workspaces": [
6
6
  {
@@ -9,7 +9,7 @@
9
9
  "homepage": "https://event4u-app.github.io/agent-config",
10
10
  "name": "@event4u/agent-config",
11
11
  "repository": "https://github.com/event4u-app/agent-config",
12
- "version": "9.4.0"
12
+ "version": "9.5.0"
13
13
  },
14
14
  "registries": [
15
15
  {
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
- **23 backed claim(s)** — all evidence pointers resolve in CI.
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.4.0",
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
+ }