@event4u/agent-config 9.4.0 → 9.6.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.
Files changed (134) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +35 -0
  3. package/dist/agent-src/commands/agents/audit.md +2 -3
  4. package/dist/agent-src/commands/agents/optimize.md +2 -3
  5. package/dist/agent-src/commands/agents/user/review.md +4 -5
  6. package/dist/agent-src/commands/agents/user/show.md +4 -5
  7. package/dist/agent-src/commands/agents/user/update.md +4 -5
  8. package/dist/agent-src/commands/agents/user.md +2 -3
  9. package/dist/agent-src/commands/analytics/prune.md +2 -3
  10. package/dist/agent-src/commands/analytics/show.md +2 -3
  11. package/dist/agent-src/commands/analyze/decision.md +2 -3
  12. package/dist/agent-src/commands/analyze/incident.md +2 -3
  13. package/dist/agent-src/commands/analyze/near-miss.md +2 -3
  14. package/dist/agent-src/commands/analyze/postmortem.md +2 -3
  15. package/dist/agent-src/commands/analyze/premortem.md +2 -3
  16. package/dist/agent-src/commands/analyze/reference-repo.md +2 -3
  17. package/dist/agent-src/commands/brand/identity.md +2 -3
  18. package/dist/agent-src/commands/brand/review.md +2 -3
  19. package/dist/agent-src/commands/brand/strategy.md +2 -3
  20. package/dist/agent-src/commands/brand/tokens.md +2 -3
  21. package/dist/agent-src/commands/brand/voice.md +2 -3
  22. package/dist/agent-src/commands/bug/fix.md +2 -3
  23. package/dist/agent-src/commands/bug/investigate.md +2 -3
  24. package/dist/agent-src/commands/challenge-me/vision.md +2 -3
  25. package/dist/agent-src/commands/challenge-me/with-docs.md +2 -3
  26. package/dist/agent-src/commands/chat-history/import.md +2 -3
  27. package/dist/agent-src/commands/commit/in-chunks.md +2 -3
  28. package/dist/agent-src/commands/context/create.md +2 -3
  29. package/dist/agent-src/commands/context/refactor.md +2 -3
  30. package/dist/agent-src/commands/cost/report.md +2 -3
  31. package/dist/agent-src/commands/council/analysis.md +2 -3
  32. package/dist/agent-src/commands/council/debate.md +2 -3
  33. package/dist/agent-src/commands/council/design.md +2 -3
  34. package/dist/agent-src/commands/council/optimize.md +2 -3
  35. package/dist/agent-src/commands/council/pr.md +2 -3
  36. package/dist/agent-src/commands/feature/dev.md +2 -3
  37. package/dist/agent-src/commands/feature/explore.md +2 -3
  38. package/dist/agent-src/commands/feature/plan.md +2 -3
  39. package/dist/agent-src/commands/feature/refactor.md +2 -3
  40. package/dist/agent-src/commands/feature/roadmap.md +2 -3
  41. package/dist/agent-src/commands/fix/ci.md +2 -3
  42. package/dist/agent-src/commands/fix/comments.md +2 -3
  43. package/dist/agent-src/commands/fix/pr-comments.md +2 -3
  44. package/dist/agent-src/commands/fix/quality.md +2 -3
  45. package/dist/agent-src/commands/fix/route.md +2 -3
  46. package/dist/agent-src/commands/fix/seeder.md +2 -3
  47. package/dist/agent-src/commands/ghostwriter/delete.md +2 -3
  48. package/dist/agent-src/commands/ghostwriter/fetch.md +2 -3
  49. package/dist/agent-src/commands/ghostwriter/list.md +2 -3
  50. package/dist/agent-src/commands/ghostwriter/show.md +2 -3
  51. package/dist/agent-src/commands/ghostwriter/write.md +2 -3
  52. package/dist/agent-src/commands/image/analyse.md +2 -3
  53. package/dist/agent-src/commands/image/create.md +2 -3
  54. package/dist/agent-src/commands/image/verify.md +2 -3
  55. package/dist/agent-src/commands/knowledge/cross-repo.md +2 -3
  56. package/dist/agent-src/commands/knowledge/forget.md +2 -3
  57. package/dist/agent-src/commands/knowledge/ingest.md +2 -3
  58. package/dist/agent-src/commands/knowledge/list.md +2 -3
  59. package/dist/agent-src/commands/memory/add.md +2 -3
  60. package/dist/agent-src/commands/memory/learn-low-impact.md +2 -3
  61. package/dist/agent-src/commands/module/create.md +2 -3
  62. package/dist/agent-src/commands/module/explore.md +2 -3
  63. package/dist/agent-src/commands/optimize/agents-dir.md +2 -3
  64. package/dist/agent-src/commands/optimize/project.md +2 -3
  65. package/dist/agent-src/commands/optimize/prompt.md +2 -3
  66. package/dist/agent-src/commands/optimize/skills.md +2 -3
  67. package/dist/agent-src/commands/override/create.md +2 -3
  68. package/dist/agent-src/commands/override/manage.md +2 -3
  69. package/dist/agent-src/commands/post-as/ghostwriter.md +2 -3
  70. package/dist/agent-src/commands/post-as/me.md +2 -3
  71. package/dist/agent-src/commands/pr/create/description-only.md +2 -3
  72. package/dist/agent-src/commands/pr/create.md +1 -1
  73. package/dist/agent-src/commands/profile/activate.md +2 -3
  74. package/dist/agent-src/commands/profile/deactivate.md +2 -3
  75. package/dist/agent-src/commands/profile/show.md +2 -3
  76. package/dist/agent-src/commands/project/analyze.md +2 -3
  77. package/dist/agent-src/commands/project/health.md +2 -3
  78. package/dist/agent-src/commands/research/deep.md +2 -3
  79. package/dist/agent-src/commands/research/report.md +2 -3
  80. package/dist/agent-src/commands/review/changes.md +2 -3
  81. package/dist/agent-src/commands/review/routing.md +2 -3
  82. package/dist/agent-src/commands/roadmap/ai-council.md +2 -3
  83. package/dist/agent-src/commands/roadmap/create.md +2 -3
  84. package/dist/agent-src/commands/roadmap/materialize.md +2 -3
  85. package/dist/agent-src/commands/roadmap/process-full.md +2 -3
  86. package/dist/agent-src/commands/roadmap/process-phase.md +2 -3
  87. package/dist/agent-src/commands/roadmap/process-step.md +2 -3
  88. package/dist/agent-src/commands/skill/preview.md +2 -3
  89. package/dist/agent-src/commands/skills/discover.md +2 -3
  90. package/dist/agent-src/commands/sync/gitignore/fix.md +3 -4
  91. package/dist/agent-src/commands/tdd/green.md +2 -3
  92. package/dist/agent-src/commands/tdd/red.md +2 -3
  93. package/dist/agent-src/commands/tdd/refactor.md +2 -3
  94. package/dist/agent-src/commands/team/adversarial.md +2 -3
  95. package/dist/agent-src/commands/team/review.md +2 -3
  96. package/dist/agent-src/commands/team-knowledge/bootstrap.md +2 -3
  97. package/dist/agent-src/commands/team-knowledge/consolidate.md +2 -3
  98. package/dist/agent-src/commands/tests/create.md +2 -3
  99. package/dist/agent-src/commands/tests/e2e-heal.md +2 -3
  100. package/dist/agent-src/commands/tests/e2e-plan.md +2 -3
  101. package/dist/agent-src/commands/tests/execute.md +2 -3
  102. package/dist/agent-src/commands/video/from-script.md +2 -3
  103. package/dist/agent-src/commands/video/from-song.md +2 -3
  104. package/dist/agent-src/commands/video/scene.md +2 -3
  105. package/dist/agent-src/commands/video/stitch.md +2 -3
  106. package/dist/agent-src/commands/video/storyboard.md +2 -3
  107. package/dist/agent-src/commands/worktree/cleanup.md +2 -3
  108. package/dist/agent-src/commands/worktree/create.md +2 -3
  109. package/dist/agent-src/commands/worktree/status.md +2 -3
  110. package/dist/agent-src/commands/worktree/verify.md +2 -3
  111. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +10 -0
  112. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  113. package/dist/discovery/deprecation-report.md +1 -1
  114. package/dist/discovery/discovery-manifest.json +110 -110
  115. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  116. package/dist/discovery/discovery-manifest.summary.md +1 -1
  117. package/dist/discovery/orphan-report.md +1 -1
  118. package/dist/discovery/packs.json +2 -2
  119. package/dist/discovery/trust-report.md +1 -1
  120. package/dist/discovery/workspaces.json +2 -2
  121. package/dist/mcp/registry-manifest.json +1 -1
  122. package/docs/CLAIMS.md +7 -0
  123. package/docs/benchmark.md +40 -0
  124. package/docs/decisions/ADR-123-runtime-security-scope-and-spawn-hardening.md +111 -0
  125. package/docs/decisions/INDEX.md +1 -0
  126. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +26 -0
  127. package/docs/proof.md +2 -1
  128. package/docs/threat-model.md +2 -1
  129. package/package.json +1 -1
  130. package/src/scripts/_lib/spawn_env.ts +100 -0
  131. package/src/scripts/ai_council/clients.ts +6 -0
  132. package/src/scripts/bench_defect_finding.ts +348 -0
  133. package/src/scripts/hot_context_hook.ts +2 -0
  134. package/src/scripts/roadmap_progress_hook.ts +2 -0
@@ -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
+ }
@@ -35,6 +35,7 @@ import * as path from 'node:path';
35
35
  import process from 'node:process';
36
36
  import { fileURLToPath } from 'node:url';
37
37
 
38
+ import { hardenedSpawnEnv } from './_lib/spawn_env.js';
38
39
  import { redact_low_impact_entry } from './ai_council/redact_low_impact_entry.js';
39
40
 
40
41
  export const HOT_CONTEXT_REL = path.join('agents', 'runtime', 'state', 'hot-context.md');
@@ -85,6 +86,7 @@ export function _current_branch(root: string): string {
85
86
  cwd: root,
86
87
  encoding: 'utf-8',
87
88
  timeout: 5000,
89
+ env: hardenedSpawnEnv(),
88
90
  });
89
91
  if (proc.status === 0) {
90
92
  return (proc.stdout || '').trim() || 'unknown';
@@ -38,6 +38,7 @@ import fs from "node:fs";
38
38
  import path from "node:path";
39
39
  import { fileURLToPath, pathToFileURL } from "node:url";
40
40
 
41
+ import { hardenedSpawnEnv } from "./_lib/spawn_env.js";
41
42
  import { log_dispatch_issue } from "./hooks/dispatch_issues.js";
42
43
 
43
44
  export const REPLAY_ENV_VAR = "AGENT_CONFIG_REPLAY";
@@ -414,6 +415,7 @@ export function run(
414
415
  cwd: root,
415
416
  stdio: ["ignore", "ignore", "ignore"],
416
417
  timeout: 30000,
418
+ env: hardenedSpawnEnv(),
417
419
  });
418
420
  } catch {
419
421
  // never propagate regenerator failures into the agent loop