@tangle-network/agent-bench 0.7.1 → 0.8.1

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 (155) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/HARNESS.md +2 -2
  3. package/dist/adapters.js +6 -2
  4. package/dist/adapters.js.map +1 -1
  5. package/dist/benchmarks/appworld.d.ts +61 -2
  6. package/dist/benchmarks/appworld.js +111 -40
  7. package/dist/benchmarks/appworld.js.map +1 -1
  8. package/dist/benchmarks/cadbench.js +1 -1
  9. package/dist/benchmarks/cadgenbench.js +1 -1
  10. package/dist/benchmarks/finresearchbench.js +16 -22
  11. package/dist/benchmarks/finresearchbench.js.map +1 -1
  12. package/dist/benchmarks/finsearchcomp.js +16 -22
  13. package/dist/benchmarks/finsearchcomp.js.map +1 -1
  14. package/dist/benchmarks/frames.js +18 -20
  15. package/dist/benchmarks/frames.js.map +1 -1
  16. package/dist/benchmarks/mcad-bench.d.ts +106 -0
  17. package/dist/benchmarks/mcad-bench.js +569 -0
  18. package/dist/benchmarks/mcad-bench.js.map +1 -0
  19. package/dist/benchmarks/mcad-cq-bench.d.ts +82 -0
  20. package/dist/benchmarks/mcad-cq-bench.js +339 -0
  21. package/dist/benchmarks/mcad-cq-bench.js.map +1 -0
  22. package/dist/benchmarks/mcad-cq-golds.d.ts +36 -0
  23. package/dist/benchmarks/mcad-cq-golds.js +342 -0
  24. package/dist/benchmarks/mcad-cq-golds.js.map +1 -0
  25. package/dist/benchmarks/mcad-golds.d.ts +20 -0
  26. package/dist/benchmarks/mcad-golds.js +318 -0
  27. package/dist/benchmarks/mcad-golds.js.map +1 -0
  28. package/dist/benchmarks/mcad-tasks.d.ts +66 -0
  29. package/dist/benchmarks/mcad-tasks.js +508 -0
  30. package/dist/benchmarks/mcad-tasks.js.map +1 -0
  31. package/dist/benchmarks/simpleqa.js +15 -19
  32. package/dist/benchmarks/simpleqa.js.map +1 -1
  33. package/dist/benchmarks/trata-hedge.js +20 -23
  34. package/dist/benchmarks/trata-hedge.js.map +1 -1
  35. package/dist/{cadbench-BrpwOU6A.js → cadbench-BLSyxR1N.js} +21 -20
  36. package/dist/cadbench-BLSyxR1N.js.map +1 -0
  37. package/dist/{cadgenbench-DF7hYHdl.js → cadgenbench-x2OFkf8y.js} +2 -2
  38. package/dist/cadgenbench-x2OFkf8y.js.map +1 -0
  39. package/dist/index.d.ts +0 -1
  40. package/dist/index.js +10 -9
  41. package/dist/index.js.map +1 -1
  42. package/dist/router-turn-C2wMiDoo.js +31 -0
  43. package/dist/router-turn-C2wMiDoo.js.map +1 -0
  44. package/package.json +7 -7
  45. package/scripts/appworld_driver.py +1 -170
  46. package/scripts/run-package-tests.mjs +56 -36
  47. package/scripts/run-package-tests.test.mjs +33 -0
  48. package/scripts/trata-hedge/README.md +3 -3
  49. package/scripts/trata-hedge/run.sh +6 -2
  50. package/src/adapters.ts +11 -0
  51. package/src/aec-gate.mts +30 -9
  52. package/src/agent-graphs-gen2.mts +523 -0
  53. package/src/agent-graphs-gen3.mts +660 -0
  54. package/src/agent-graphs-improve/offline-seams.mts +128 -0
  55. package/src/agent-graphs-improve.mts +747 -0
  56. package/src/atom-humaneval.mts +76 -55
  57. package/src/atom-mcp-e2e.mts +27 -13
  58. package/src/benchmarks/appworld.test.mts +119 -1
  59. package/src/benchmarks/appworld.ts +156 -33
  60. package/src/benchmarks/cadbench.ts +21 -8
  61. package/src/benchmarks/finresearchbench.ts +20 -16
  62. package/src/benchmarks/finsearchcomp.ts +20 -20
  63. package/src/benchmarks/frames.ts +19 -18
  64. package/src/benchmarks/mcad-bench.test.mts +455 -0
  65. package/src/benchmarks/mcad-bench.ts +561 -0
  66. package/src/benchmarks/mcad-cq-bench.ts +423 -0
  67. package/src/benchmarks/mcad-cq-golds.ts +374 -0
  68. package/src/benchmarks/mcad-cq.test.mts +386 -0
  69. package/src/benchmarks/mcad-golds.ts +359 -0
  70. package/src/benchmarks/mcad-tasks.ts +490 -0
  71. package/src/benchmarks/simpleqa.ts +19 -17
  72. package/src/benchmarks/trata-hedge.ts +26 -18
  73. package/src/browser/ui-reviewer.ts +1 -1
  74. package/src/clbench-context-gate.mts +45 -9
  75. package/src/cloud-loop.mts +24 -6
  76. package/src/commit0-env-run.mts +6 -3
  77. package/src/commit0-env.ts +2 -3
  78. package/src/commit0-gate.mts +53 -24
  79. package/src/corpus.ts +3 -3
  80. package/src/david-attribution.mts +28 -9
  81. package/src/david-goliath.mts +31 -12
  82. package/src/egress-probe.mts +3 -3
  83. package/src/examples/lean-proof-gate.mts +10 -3
  84. package/src/examples/math-demo.mts +10 -3
  85. package/src/examples/strategy-demo.mts +10 -4
  86. package/src/fleet.mts +30 -8
  87. package/src/gate-cli.mts +9 -4
  88. package/src/gate.test.mts +7 -2
  89. package/src/gate.ts +23 -75
  90. package/src/generate-eval/certify.ts +13 -5
  91. package/src/hev-eval.mts +39 -10
  92. package/src/hev-improve.mts +28 -16
  93. package/src/hev-structural.mts +28 -20
  94. package/src/humaneval-gate.mts +24 -8
  95. package/src/humaneval-object-ablation.mts +52 -14
  96. package/src/humaneval-repair-gate.mts +53 -14
  97. package/src/mbpp-structural.mts +28 -20
  98. package/src/mcp-mount-probe.mts +23 -11
  99. package/src/official-optimizer-config.mts +61 -12
  100. package/src/official-optimizer-config.test.mts +72 -3
  101. package/src/quant-arena/profiles/quant-researcher.profile.json +6 -1
  102. package/src/quant-arena/quant-loop.mts +88 -61
  103. package/src/research-shot.ts +21 -11
  104. package/src/resolve-client.ts +6 -8
  105. package/src/rollout-ledger/backfill-swe-arena.test.mts +7 -2
  106. package/src/router-executor.ts +1 -1
  107. package/src/router-turn.ts +135 -0
  108. package/src/run-benchmarks.ts +7 -2
  109. package/src/sandbox-run.ts +23 -41
  110. package/src/search-bench/bridge.ts +66 -8
  111. package/src/search-bench/parametric-check.mts +19 -4
  112. package/src/search-bench/profiles.ts +24 -9
  113. package/src/search-bench/run.mts +3 -4
  114. package/src/smoke-structural-rollout.mts +21 -5
  115. package/src/supervisor-arena.mts +28 -20
  116. package/src/swe-arena/arms.ts +73 -15
  117. package/src/swe-arena/capacity.ts +44 -41
  118. package/src/swe-arena/diagnosis-ensemble.ts +51 -48
  119. package/src/swe-arena/execution.test.mts +12 -12
  120. package/src/swe-arena/gepa-seat.mts +12 -5
  121. package/src/swe-arena/gepa-seat.test.mts +45 -33
  122. package/src/swe-arena/outer-loop.mts +94 -70
  123. package/src/swe-arena/premeasured-from-cells.mts +16 -1
  124. package/src/swe-arena/premeasured-from-cells.test.mts +21 -1
  125. package/src/swe-arena/profiles/deepseek-author.profile.json +12 -0
  126. package/src/swe-arena/profiles/default-author.profile.json +9 -1
  127. package/src/swe-arena/proposer-fanout.mts +52 -99
  128. package/src/swe-arena/proposer-fanout.test.mts +92 -57
  129. package/src/swe-arena/proposer-provenance.mts +1 -1
  130. package/src/swe-code-improve.mts +30 -52
  131. package/src/swe-emit-patch.mts +26 -5
  132. package/src/swe-improve.mts +27 -8
  133. package/src/swe-jail.test.ts +128 -0
  134. package/src/swe-jail.ts +106 -22
  135. package/src/swe-local-proof.mts +27 -5
  136. package/src/swe-repro-calibrate.mts +5 -0
  137. package/src/swe-self-improve.mts +45 -10
  138. package/src/swe-stream.mts +68 -6
  139. package/src/swe-structural.mts +46 -7
  140. package/src/tb-container-executor.mts +38 -6
  141. package/src/tb-container-executor.test.mts +11 -3
  142. package/src/tb-supervisor-sidecar.mts +11 -27
  143. package/src/terminal-compare.ts +38 -18
  144. package/src/trata-gate.mts +26 -26
  145. package/src/trata-gepa.mts +25 -16
  146. package/src/trata-hedge-solve.mts +80 -0
  147. package/src/worker-blender.ts +21 -11
  148. package/src/worker-browser.ts +18 -5
  149. package/src/worker-build123d.ts +17 -8
  150. package/src/worker-cad.ts +31 -12
  151. package/src/worker.ts +1 -1
  152. package/tb_agents/opencode_router_agent.py +84 -42
  153. package/dist/cadbench-BrpwOU6A.js.map +0 -1
  154. package/dist/cadgenbench-DF7hYHdl.js.map +0 -1
  155. package/scripts/trata-hedge/solve.py +0 -83
@@ -0,0 +1,660 @@
1
+ /**
2
+ * Generation v3 of the agent-graphs skill improvement loop — same composition as
3
+ * agent-graphs-gen2.mts (agent-eval's `runImprovementLoop` + the two caller closures
4
+ * from agent-graphs-improve.mts), with the gen3 protocol deltas:
5
+ *
6
+ * • baseline surface is the PROMOTED v2 SKILL.md (sha asserted at startup);
7
+ * • TRAIN grew to 7 cases (the 5 prior + artifact-mission-release-notes and
8
+ * audited-single-writer, both targeting the residual mission-in-deliverable
9
+ * under-graphing cluster); HOLDOUT is unchanged and never enters the prompt;
10
+ * • k=5 reps per case per surface, sequential, author temp 0.2;
11
+ * • transient router 5xx: bounded per-cell retry (up to 3, receipts kept) BEFORE a
12
+ * cell is declared failed — the #723 workaround for the gen2 503 cell loss;
13
+ * • the revision prompt carries ONLY the measured k=5 v2 TRAIN failures plus a
14
+ * mechanical per-check failure tally — no inherited failure-cluster narrative.
15
+ *
16
+ * Gate: ship iff v3 holdout mean > v2 holdout mean
17
+ * and v3 train mean >= v2 train mean - 0.05
18
+ * and neither anti-over-graphing case regresses
19
+ * (single-agent-suffices, runtime-discovered-fanout).
20
+ *
21
+ * Run: pnpm tsx src/agent-graphs-gen3.mts (from bench/)
22
+ * Smoke: GEN3_SMOKE=1 pnpm tsx src/agent-graphs-gen3.mts — stubs both LLM calls;
23
+ * writes into .gen3-runs/ only, never the tracked artifact.
24
+ *
25
+ * Writes skills/agent-graphs/generations/gen3.json; on ship, replaces SKILL.md with v3.
26
+ */
27
+
28
+ import { createHash } from 'node:crypto'
29
+ import { mkdirSync, writeFileSync } from 'node:fs'
30
+ import { join, dirname } from 'node:path'
31
+ import { fileURLToPath } from 'node:url'
32
+ import { setTimeout as sleep } from 'node:timers/promises'
33
+ import {
34
+ runEval,
35
+ runImprovementLoop,
36
+ type CampaignResult,
37
+ type DispatchContext,
38
+ type Gate,
39
+ type GateContext,
40
+ type JudgeConfig,
41
+ type MutableSurface,
42
+ type ProposeContext,
43
+ type ProposedCandidate,
44
+ type SurfaceProposer,
45
+ } from '@tangle-network/agent-eval/campaign'
46
+ import {
47
+ type AuthoredArtifact,
48
+ type CaseSpec,
49
+ buildAgentGraphsAuthorProfile,
50
+ callAuthor,
51
+ dispatchWithSurface,
52
+ judgeArtifact,
53
+ loadInputs,
54
+ } from './agent-graphs-improve.mts'
55
+
56
+ const HERE = dirname(fileURLToPath(import.meta.url))
57
+ const REPO = join(HERE, '..', '..')
58
+ const SKILL_PATH = join(REPO, 'skills', 'agent-graphs', 'SKILL.md')
59
+ const OUT_PATH = join(REPO, 'skills', 'agent-graphs', 'generations', 'gen3.json')
60
+ const RUNS_ROOT = join(REPO, '.gen3-runs')
61
+ const SMOKE = process.env.GEN3_SMOKE === '1'
62
+ // Smoke runs must never clobber the tracked generation record.
63
+ const EFFECTIVE_OUT = SMOKE ? join(RUNS_ROOT, 'gen3-smoke.json') : OUT_PATH
64
+
65
+ // The promoted v2 surface this generation improves on (generations/gen2.json surfaces.v2Sha256).
66
+ const EXPECTED_V2_SHA = '4c6615b6164f6c5a86efb2596556bdf325d33f08a4e1715cae9d71cb28b6255e'
67
+
68
+ const K = 5
69
+ const SEED = 42
70
+ const TRAIN_IDS = [
71
+ 'review-pipeline',
72
+ 'single-agent-suffices',
73
+ 'cap-as-stop-mistake',
74
+ 'runtime-discovered-fanout',
75
+ 'artifact-mission-release-notes',
76
+ 'audited-single-writer',
77
+ ] as const
78
+ const HOLDOUT_IDS = ['mission-in-deliverable', 'steer-heavy-drafting', 'unmeasured-harness'] as const
79
+ // The anti-over-graphing cases an "always graph" hack would regress on.
80
+ const DEGENERATE_IDS = ['single-agent-suffices', 'runtime-discovered-fanout'] as const
81
+ // Holdout-brief phrases that must never reach the reviser (belt over the whole-brief check).
82
+ const BANNED_PROMPT_STRINGS = ['CHANGELOG', 'redirect it up to five times', 'three probes on claude-code'] as const
83
+
84
+ type GraphScenario = CaseSpec & { kind: 'agent-graph-case' }
85
+ type CellArtifact = AuthoredArtifact & { repIndex: number; surfaceSha: string }
86
+
87
+ function sha256(text: string): string {
88
+ return createHash('sha256').update(text).digest('hex')
89
+ }
90
+
91
+ // ── Captured evidence (fed to the proposer; TRAIN-filtered at prompt build) ────
92
+
93
+ interface JudgedRecord {
94
+ surfaceSha: string
95
+ scenarioId: string
96
+ rep: number
97
+ decision: string
98
+ score: number
99
+ failures: string[]
100
+ validationError?: string
101
+ }
102
+
103
+ const judged: JudgedRecord[] = []
104
+
105
+ function makeJudge(): JudgeConfig<CellArtifact, GraphScenario> {
106
+ return {
107
+ name: 'deterministic-expect',
108
+ judgeVersion: 'gen3-1',
109
+ dimensions: [{ key: 'expect', description: 'fraction of case expectations satisfied' }],
110
+ score({ artifact, scenario }) {
111
+ const { score, reasons } = judgeArtifact(artifact, scenario)
112
+ judged.push({
113
+ surfaceSha: artifact.surfaceSha,
114
+ scenarioId: scenario.id,
115
+ rep: artifact.repIndex,
116
+ decision: artifact.decision,
117
+ score,
118
+ failures: reasons.filter((r) => !r.startsWith('PASS')),
119
+ ...(artifact.validationError !== undefined ? { validationError: artifact.validationError } : {}),
120
+ })
121
+ return { dimensions: { expect: score }, composite: score, notes: reasons.join('\n') }
122
+ },
123
+ }
124
+ }
125
+
126
+ // ── Dispatch: closure A + the bounded transient-retry policy (#723 workaround) ─
127
+
128
+ const MAX_TRANSIENT_RETRIES = 3
129
+ const TRANSIENT_PATTERN =
130
+ /HTTP 5\d\d|platform_unreachable|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|fetch failed|socket|TimeoutError|aborted|empty content/i
131
+
132
+ interface RetryReceipt {
133
+ surfaceSha12: string
134
+ scenarioId: string
135
+ rep: number
136
+ attempt: number
137
+ error: string
138
+ at: string
139
+ }
140
+
141
+ interface CellFailure {
142
+ surfaceSha12: string
143
+ scenarioId: string
144
+ rep: number
145
+ attempts: number
146
+ error: string
147
+ }
148
+
149
+ const retryReceipts: RetryReceipt[] = []
150
+ const cellFailures: CellFailure[] = []
151
+
152
+ function smokeArtifact(scenario: GraphScenario): AuthoredArtifact {
153
+ return { decision: 'single-agent', reason: `smoke stub for ${scenario.id}`, raw: '{}' }
154
+ }
155
+
156
+ async function dispatchCell(
157
+ surface: MutableSurface,
158
+ scenario: GraphScenario,
159
+ ctx: DispatchContext,
160
+ ): Promise<CellArtifact> {
161
+ if (typeof surface !== 'string') throw new Error('gen3 surfaces are strings')
162
+ const surfaceSha = sha256(surface)
163
+ let lastErr: unknown
164
+ for (let attempt = 1; attempt <= 1 + MAX_TRANSIENT_RETRIES; attempt += 1) {
165
+ try {
166
+ const artifact = SMOKE ? smokeArtifact(scenario) : await dispatchWithSurface(surface, scenario)
167
+ return { ...artifact, repIndex: ctx.rep, surfaceSha }
168
+ } catch (err) {
169
+ lastErr = err
170
+ const message = err instanceof Error ? err.message : String(err)
171
+ const transient = TRANSIENT_PATTERN.test(message)
172
+ if (!transient || attempt > MAX_TRANSIENT_RETRIES) break
173
+ retryReceipts.push({
174
+ surfaceSha12: surfaceSha.slice(0, 12),
175
+ scenarioId: scenario.id,
176
+ rep: ctx.rep,
177
+ attempt,
178
+ error: message.slice(0, 300),
179
+ at: new Date().toISOString(),
180
+ })
181
+ await sleep(5_000 * attempt)
182
+ }
183
+ }
184
+ const message = lastErr instanceof Error ? lastErr.message : String(lastErr)
185
+ cellFailures.push({
186
+ surfaceSha12: surfaceSha.slice(0, 12),
187
+ scenarioId: scenario.id,
188
+ rep: ctx.rep,
189
+ attempts: Math.min(1 + MAX_TRANSIENT_RETRIES, retryReceipts.filter((r) => r.scenarioId === scenario.id && r.rep === ctx.rep && r.surfaceSha12 === surfaceSha.slice(0, 12)).length + 1),
190
+ error: message.slice(0, 300),
191
+ })
192
+ throw lastErr
193
+ }
194
+
195
+ // ── The reviser proposer (prompt = v2 text + measured k=5 TRAIN failures ONLY) ─
196
+
197
+ let revisionPrompt = ''
198
+ let revisionPromptSha256 = ''
199
+
200
+ function failKey(line: string): string {
201
+ return line.match(/^FAIL ([^:]+):/)?.[1] ?? line.split(':')[0] ?? line.slice(0, 40)
202
+ }
203
+
204
+ /** Mechanical per-case tally of failing checks in the baseline's TRAIN measurements. */
205
+ function tallyTrainFailures(baselineSha: string): Record<string, Record<string, number>> {
206
+ const tally: Record<string, Record<string, number>> = {}
207
+ for (const r of judged) {
208
+ if (r.surfaceSha !== baselineSha) continue
209
+ if (!(TRAIN_IDS as readonly string[]).includes(r.scenarioId)) continue
210
+ for (const f of r.failures) {
211
+ const key = failKey(f)
212
+ tally[r.scenarioId] = tally[r.scenarioId] ?? {}
213
+ tally[r.scenarioId][key] = (tally[r.scenarioId][key] ?? 0) + 1
214
+ }
215
+ }
216
+ return tally
217
+ }
218
+
219
+ function buildRevisionPrompt(v2Surface: string, trainCases: GraphScenario[]): string {
220
+ const v2Sha = sha256(v2Surface)
221
+ const caseBlocks = trainCases.map((kase) => {
222
+ const rows = judged
223
+ .filter((r) => r.surfaceSha === v2Sha && r.scenarioId === kase.id)
224
+ .sort((a, b) => a.rep - b.rep)
225
+ .map((r) => {
226
+ const fails = r.failures.length > 0 ? r.failures.join('\n ') : '(all checks passed)'
227
+ return ` rep ${r.rep}: decision=${r.decision} score=${r.score.toFixed(2)}\n ${fails}`
228
+ })
229
+ return [`<case id="${kase.id}">`, `brief: ${kase.brief}`, `measured (k=${K}):`, ...rows, '</case>'].join('\n')
230
+ })
231
+ const tally = tallyTrainFailures(v2Sha)
232
+ const tallyLines = trainCases.map((kase) => {
233
+ const byKey = tally[kase.id]
234
+ if (!byKey || Object.keys(byKey).length === 0) return ` ${kase.id}: clean (no failing checks)`
235
+ const parts = Object.entries(byKey)
236
+ .sort((a, b) => b[1] - a[1])
237
+ .map(([key, count]) => `${key} failed ${count}/${K} reps`)
238
+ return ` ${kase.id}: ${parts.join(', ')}`
239
+ })
240
+ return [
241
+ 'You are revising an agent-skill document. The skill below ("v2") instructs a model to author',
242
+ `agent graphs (or decline to) from loose case briefs. It was measured k=${K} per case against a`,
243
+ 'deterministic scorer; the per-rep results for the training cases are listed after the text.',
244
+ '',
245
+ '<v2-skill>',
246
+ v2Surface,
247
+ '</v2-skill>',
248
+ '',
249
+ 'Measured training results:',
250
+ '',
251
+ ...caseBlocks,
252
+ '',
253
+ 'Mechanical failure tally (check -> failed reps, from the measurements above; this tally is the',
254
+ 'ONLY ground truth about what is failing — do not assume any earlier generation\'s failure',
255
+ 'clusters still hold):',
256
+ ...tallyLines,
257
+ '',
258
+ 'Rewrite the skill into v3 targeting exactly the failing checks in the tally. For each failing',
259
+ 'check, find the doctrine gap that lets the author fail it and close that gap. Leave the clean',
260
+ 'cases\' behavior alone.',
261
+ '',
262
+ 'Hard constraints:',
263
+ '- Keep the YAML frontmatter: `name: agent-graphs` unchanged; `description:` must be a single',
264
+ ' line of at most 96 characters.',
265
+ '- Total file must stay under 20000 bytes.',
266
+ '- Keep the decision honest: "single-agent" and "dynamic-workflow" remain the CORRECT answers',
267
+ ' when one profile suffices or when topology is discovered mid-run. Do not teach "always',
268
+ ' graph" — fixing under-graphing must not create over-graphing.',
269
+ '- Keep the existing correct doctrine (traversal caps, analyzes-cap-is-not-a-stop,',
270
+ ' deliverable-carries-mission, offline proving) — sharpen it, do not delete it.',
271
+ '- The skill is consumed by a model that must output a strict JSON graph spec; keep the text',
272
+ ' operational, not narrative.',
273
+ '',
274
+ 'Reply with the COMPLETE revised SKILL.md between the markers, nothing else:',
275
+ '<<<SKILL',
276
+ '(full file here)',
277
+ 'SKILL>>>',
278
+ ].join('\n')
279
+ }
280
+
281
+ function extractSkill(reply: string): string {
282
+ const m = reply.match(/<<<SKILL\n([\s\S]*?)\nSKILL>>>/)
283
+ if (!m?.[1]) throw new Error('proposer reply carries no <<<SKILL ... SKILL>>> block')
284
+ return `${m[1].trim()}\n`
285
+ }
286
+
287
+ function validateSkillGate(text: string): string[] {
288
+ const problems: string[] = []
289
+ const fm = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1]
290
+ if (!fm) problems.push('missing YAML frontmatter')
291
+ const name = fm?.match(/^name:\s*(.+)$/m)?.[1]?.trim()
292
+ if (name !== 'agent-graphs') problems.push(`frontmatter name is ${JSON.stringify(name)}, expected agent-graphs`)
293
+ const description = fm?.match(/^description:\s*(.+)$/m)?.[1]?.trim().replace(/^["']|["']$/g, '')
294
+ if (!description) problems.push('frontmatter description missing')
295
+ else if (description.length > 96) problems.push(`description is ${description.length} chars (max 96)`)
296
+ if (Buffer.byteLength(text) > 20_000) problems.push(`file is ${Buffer.byteLength(text)} bytes (max 20000)`)
297
+ return problems
298
+ }
299
+
300
+ function assertNoHoldoutLeak(prompt: string): void {
301
+ for (const id of HOLDOUT_IDS) {
302
+ if (prompt.includes(id)) throw new Error(`holdout id '${id}' leaked into the revision prompt`)
303
+ }
304
+ const allCases = loadInputs().cases
305
+ for (const id of HOLDOUT_IDS) {
306
+ const brief = allCases.find((c) => c.id === id)?.brief
307
+ if (brief && prompt.includes(brief)) {
308
+ throw new Error(`holdout brief for '${id}' leaked into the revision prompt`)
309
+ }
310
+ }
311
+ for (const phrase of BANNED_PROMPT_STRINGS) {
312
+ if (prompt.includes(phrase)) {
313
+ throw new Error(`banned holdout phrase '${phrase}' leaked into the revision prompt`)
314
+ }
315
+ }
316
+ }
317
+
318
+ function makeProposer(v2Surface: string, trainCases: GraphScenario[]): SurfaceProposer {
319
+ return {
320
+ kind: 'agent-graphs-skill-reviser',
321
+ async propose(_ctx: ProposeContext): Promise<ProposedCandidate[]> {
322
+ revisionPrompt = buildRevisionPrompt(v2Surface, trainCases)
323
+ assertNoHoldoutLeak(revisionPrompt)
324
+ revisionPromptSha256 = sha256(revisionPrompt)
325
+ if (SMOKE) {
326
+ return [
327
+ {
328
+ surface: v2Surface.replace(
329
+ '# Agent graphs',
330
+ '# Agent graphs\n\n(smoke marker: candidate differs from baseline)',
331
+ ),
332
+ label: 'smoke-candidate',
333
+ rationale: 'zero-cost wiring check',
334
+ },
335
+ ]
336
+ }
337
+ let prompt = revisionPrompt
338
+ let lastProblems: string[] = []
339
+ for (let attempt = 0; attempt < 2; attempt += 1) {
340
+ const reply = await callAuthor(buildAgentGraphsAuthorProfile(v2Surface), prompt)
341
+ const skill = extractSkill(reply)
342
+ lastProblems = validateSkillGate(skill)
343
+ if (lastProblems.length === 0) {
344
+ return [
345
+ {
346
+ surface: skill,
347
+ label: 'gen3-revision',
348
+ rationale: 'glm-5.2 rewrite targeting the k=5-measured failing checks on the 7 train cases',
349
+ },
350
+ ]
351
+ }
352
+ prompt = `${revisionPrompt}\n\nYour previous attempt violated: ${lastProblems.join('; ')}. Fix these and reply again with the full file between the markers.`
353
+ }
354
+ throw new Error(`proposer surface failed the skills gate after retry: ${lastProblems.join('; ')}`)
355
+ },
356
+ }
357
+ }
358
+
359
+ // ── The protocol gate ──────────────────────────────────────────────────────────
360
+
361
+ const trainMeanBySurfaceSha = new Map<string, number>()
362
+ const trainCaseMeansBySurfaceSha = new Map<string, Map<string, number>>()
363
+
364
+ function campaignPerRep(campaign: CampaignResult<CellArtifact, GraphScenario>) {
365
+ const perCase = new Map<string, Array<{ rep: number; score: number; decision: string }>>()
366
+ for (const cell of campaign.cells) {
367
+ const s = cell.judgeScores['deterministic-expect']
368
+ if (!s || s.failed) continue
369
+ const rows = perCase.get(cell.scenarioId) ?? []
370
+ rows.push({ rep: cell.rep, score: s.composite, decision: cell.artifact?.decision ?? 'unknown' })
371
+ perCase.set(cell.scenarioId, rows)
372
+ }
373
+ for (const rows of perCase.values()) rows.sort((a, b) => a.rep - b.rep)
374
+ return perCase
375
+ }
376
+
377
+ /** Split mean per protocol: mean over cases of the per-case rep means. */
378
+ function splitMean(perCase: Map<string, Array<{ score: number }>>, ids: readonly string[]): number {
379
+ const caseMeans = ids.map((id) => {
380
+ const rows = perCase.get(id) ?? []
381
+ return rows.length === 0 ? 0 : rows.reduce((s, r) => s + r.score, 0) / rows.length
382
+ })
383
+ return caseMeans.reduce((s, x) => s + x, 0) / Math.max(caseMeans.length, 1)
384
+ }
385
+
386
+ /** Worst-case split mean: every missing rep of every case scored as 0 (denominator K). */
387
+ function splitMeanImputedZero(perCase: Map<string, Array<{ score: number }>>, ids: readonly string[]): number {
388
+ const caseMeans = ids.map((id) => {
389
+ const rows = perCase.get(id) ?? []
390
+ return rows.reduce((s, r) => s + r.score, 0) / K
391
+ })
392
+ return caseMeans.reduce((s, x) => s + x, 0) / Math.max(caseMeans.length, 1)
393
+ }
394
+
395
+ function holdoutMeanFromScores(
396
+ scores: Map<string, Record<string, import('@tangle-network/agent-eval/campaign').JudgeScore>>,
397
+ ): number {
398
+ const values: number[] = []
399
+ for (const byJudge of scores.values()) {
400
+ const s = byJudge['deterministic-expect']
401
+ if (s && !s.failed) values.push(s.composite)
402
+ }
403
+ return values.length === 0 ? 0 : values.reduce((a, b) => a + b, 0) / values.length
404
+ }
405
+
406
+ function makeGate(v2Sha: string): Gate<CellArtifact, GraphScenario> {
407
+ return {
408
+ name: 'gen3-protocol-gate',
409
+ async decide(ctx: GateContext<CellArtifact, GraphScenario>) {
410
+ const winnerHoldout = holdoutMeanFromScores(ctx.judgeScores)
411
+ const baselineHoldout = ctx.baselineJudgeScores ? holdoutMeanFromScores(ctx.baselineJudgeScores) : 0
412
+ const v2Train = trainMeanBySurfaceSha.get(v2Sha)
413
+ const candidateShas = [...trainMeanBySurfaceSha.keys()].filter((k) => k !== v2Sha)
414
+ const v3Train = candidateShas.length === 1 ? trainMeanBySurfaceSha.get(candidateShas[0] ?? '') : undefined
415
+ const v2Cases = trainCaseMeansBySurfaceSha.get(v2Sha)
416
+ const v3Cases = candidateShas.length === 1 ? trainCaseMeansBySurfaceSha.get(candidateShas[0] ?? '') : undefined
417
+ const holdoutOk = winnerHoldout > baselineHoldout
418
+ const trainOk = v2Train !== undefined && v3Train !== undefined && v3Train >= v2Train - 0.05
419
+ const degenerateOk =
420
+ v2Cases !== undefined &&
421
+ v3Cases !== undefined &&
422
+ DEGENERATE_IDS.every((id) => (v3Cases.get(id) ?? 0) >= (v2Cases.get(id) ?? 0))
423
+ const ship = holdoutOk && trainOk && degenerateOk
424
+ return {
425
+ decision: ship ? ('ship' as const) : ('hold' as const),
426
+ delta: winnerHoldout - baselineHoldout,
427
+ reasons: [
428
+ `holdout: winner ${winnerHoldout.toFixed(3)} vs baseline ${baselineHoldout.toFixed(3)} → ${holdoutOk ? 'pass' : 'fail'}`,
429
+ `train: v3 ${v3Train?.toFixed(3) ?? 'unmeasured'} vs v2 ${v2Train?.toFixed(3) ?? 'unmeasured'} - 0.05 → ${trainOk ? 'pass' : 'fail'}`,
430
+ `degenerate cases non-regression → ${degenerateOk ? 'pass' : 'fail'}`,
431
+ ],
432
+ contributingGates: [
433
+ { name: 'holdout-mean-strictly-better', status: holdoutOk ? 'pass' : 'fail', detail: { winnerHoldout, baselineHoldout } },
434
+ { name: 'train-mean-within-0.05', status: trainOk ? 'pass' : 'fail', detail: { v3Train, v2Train } },
435
+ { name: 'anti-over-graphing-non-regression', status: degenerateOk ? 'pass' : 'fail', detail: { degenerateIds: [...DEGENERATE_IDS] } },
436
+ ],
437
+ }
438
+ },
439
+ }
440
+ }
441
+
442
+ // ── The run ────────────────────────────────────────────────────────────────────
443
+
444
+ interface RepRow {
445
+ rep: number
446
+ score: number
447
+ decision: string
448
+ }
449
+
450
+ function tableFor(perCase: Map<string, RepRow[]>, ids: readonly string[]): Record<string, RepRow[]> {
451
+ return Object.fromEntries(ids.map((id) => [id, perCase.get(id) ?? []]))
452
+ }
453
+
454
+ function printSplit(label: string, perCase: Map<string, RepRow[]>, ids: readonly string[]): void {
455
+ console.log(` ${label}:`)
456
+ for (const id of ids) {
457
+ const rows = perCase.get(id) ?? []
458
+ const reps = rows.map((r) => r.score.toFixed(2)).join(' ')
459
+ const mean = rows.length === 0 ? 0 : rows.reduce((s, r) => s + r.score, 0) / rows.length
460
+ console.log(` ${id.padEnd(32)} reps=[${reps}] mean=${mean.toFixed(3)}`)
461
+ }
462
+ console.log(` split mean = ${splitMean(perCase, ids).toFixed(4)}`)
463
+ }
464
+
465
+ function firstCellSurfaceSha(campaign: CampaignResult<CellArtifact, GraphScenario>): string | undefined {
466
+ for (const cell of campaign.cells) {
467
+ const sha = cell.artifact?.surfaceSha
468
+ if (typeof sha === 'string') return sha
469
+ }
470
+ return undefined
471
+ }
472
+
473
+ async function main(): Promise<void> {
474
+ const inputs = loadInputs()
475
+ const v2Surface = inputs.surface
476
+ const v2Sha = sha256(v2Surface)
477
+ if (!SMOKE && v2Sha !== EXPECTED_V2_SHA) {
478
+ throw new Error(
479
+ `working-tree SKILL.md sha ${v2Sha.slice(0, 12)} != promoted v2 ${EXPECTED_V2_SHA.slice(0, 12)}; gen3 must start from the promoted v2 surface`,
480
+ )
481
+ }
482
+ const byId = new Map(inputs.cases.map((c) => [c.id, c]))
483
+ const missing = [...TRAIN_IDS, ...HOLDOUT_IDS].filter((id) => !byId.has(id))
484
+ if (missing.length > 0) throw new Error(`cases missing from skills/agent-graphs/cases: ${missing.join(', ')}`)
485
+ if (TRAIN_IDS.length + HOLDOUT_IDS.length !== inputs.cases.length) {
486
+ throw new Error(`split covers ${TRAIN_IDS.length + HOLDOUT_IDS.length} of ${inputs.cases.length} cases`)
487
+ }
488
+ const toScenario = (id: string): GraphScenario => ({ ...(byId.get(id) as CaseSpec), kind: 'agent-graph-case' })
489
+ const trainScenarios = TRAIN_IDS.map(toScenario)
490
+ const holdoutScenarios = HOLDOUT_IDS.map(toScenario)
491
+
492
+ console.log(
493
+ `gen3 ${SMOKE ? '(SMOKE) ' : ''}v2=${v2Sha.slice(0, 12)} (${v2Surface.length} chars, ${inputs.source}); train=${TRAIN_IDS.length} holdout=${HOLDOUT_IDS.length} k=${K}`,
494
+ )
495
+
496
+ const runDir = join(RUNS_ROOT, SMOKE ? 'smoke-loop' : 'loop')
497
+ mkdirSync(runDir, { recursive: true })
498
+
499
+ const result = await runImprovementLoop<GraphScenario, CellArtifact>({
500
+ scenarios: trainScenarios,
501
+ holdoutScenarios,
502
+ reps: K,
503
+ seed: SEED,
504
+ maxConcurrency: 1,
505
+ candidateConcurrency: 1,
506
+ populationSize: 1,
507
+ maxGenerations: 1,
508
+ baselineSurface: v2Surface,
509
+ dispatchRef: SMOKE ? 'gen3-smoke-stub' : 'agent-graphs-author/glm-5.2/temp-0.2',
510
+ dispatchWithSurface: dispatchCell,
511
+ // Room for the worst retry ladder: 4 author attempts x (2x240s) + backoffs.
512
+ dispatchTimeoutMs: 2_400_000,
513
+ expectUsage: 'off',
514
+ judges: [makeJudge()],
515
+ proposer: makeProposer(v2Surface, trainScenarios),
516
+ analyzeGeneration: async ({ candidates }) => {
517
+ for (const c of candidates) {
518
+ const perCase = campaignPerRep(c.campaign)
519
+ const sha = firstCellSurfaceSha(c.campaign)
520
+ if (sha !== undefined) {
521
+ trainMeanBySurfaceSha.set(sha, splitMean(perCase, TRAIN_IDS))
522
+ trainCaseMeansBySurfaceSha.set(
523
+ sha,
524
+ new Map(
525
+ TRAIN_IDS.map((id) => {
526
+ const rows = perCase.get(id) ?? []
527
+ return [id, rows.length === 0 ? 0 : rows.reduce((s, r) => s + r.score, 0) / rows.length]
528
+ }),
529
+ ),
530
+ )
531
+ }
532
+ }
533
+ return []
534
+ },
535
+ gate: makeGate(v2Sha),
536
+ autoOnPromote: 'none',
537
+ runDir,
538
+ })
539
+
540
+ // ── Assemble the four arms ──
541
+ const v2Train = campaignPerRep(result.baselineCampaign)
542
+ const candidateGen = result.generations[0]?.surfaces[0]
543
+ if (!candidateGen) throw new Error('loop produced no generation-0 candidate campaign')
544
+ const v3Surface = candidateGen.surface
545
+ if (typeof v3Surface !== 'string') throw new Error('candidate surface is not a string')
546
+ const v3Sha = sha256(v3Surface)
547
+ const v3Train = campaignPerRep(candidateGen.campaign)
548
+ const v2Holdout = campaignPerRep(result.baselineOnHoldout)
549
+
550
+ // When upstream winner-selection kept the baseline (e.g. the coverage/no-op guard),
551
+ // the protocol still requires v3 measured on holdout: same judge, reps, seed.
552
+ const winnerIsCandidate = result.winnerSurfaceHash !== undefined && result.winnerSurface === v3Surface
553
+ let v3HoldoutCampaign: CampaignResult<CellArtifact, GraphScenario>
554
+ if (winnerIsCandidate) {
555
+ v3HoldoutCampaign = result.winnerOnHoldout
556
+ } else {
557
+ console.log('upstream winner = baseline; measuring v3 on holdout via runEval for the protocol gate')
558
+ v3HoldoutCampaign = await runEval<GraphScenario, CellArtifact>({
559
+ scenarios: holdoutScenarios,
560
+ dispatch: (scenario, ctx) => dispatchCell(v3Surface, scenario, ctx),
561
+ dispatchRef: SMOKE ? 'gen3-smoke-stub-v3' : 'agent-graphs-author/glm-5.2/temp-0.2/v3',
562
+ judges: [makeJudge()],
563
+ reps: K,
564
+ seed: SEED,
565
+ maxConcurrency: 1,
566
+ dispatchTimeoutMs: 2_400_000,
567
+ expectUsage: 'off',
568
+ runDir: join(RUNS_ROOT, SMOKE ? 'smoke-v3-holdout' : 'v3-holdout'),
569
+ })
570
+ }
571
+ const v3Holdout = campaignPerRep(v3HoldoutCampaign)
572
+
573
+ // ── Protocol gate, applied to the assembled arms ──
574
+ const v2TrainMean = splitMean(v2Train, TRAIN_IDS)
575
+ const v3TrainMean = splitMean(v3Train, TRAIN_IDS)
576
+ const v2HoldoutMean = splitMean(v2Holdout, HOLDOUT_IDS)
577
+ const v3HoldoutMean = splitMean(v3Holdout, HOLDOUT_IDS)
578
+
579
+ const caseMean = (perCase: Map<string, RepRow[]>, id: string): number => {
580
+ const rows = perCase.get(id) ?? []
581
+ return rows.length === 0 ? 0 : rows.reduce((s, r) => s + r.score, 0) / rows.length
582
+ }
583
+ const degenerate = Object.fromEntries(
584
+ DEGENERATE_IDS.map((id) => [id, { v2: caseMean(v2Train, id), v3: caseMean(v3Train, id) }]),
585
+ )
586
+ const degenerateOk = DEGENERATE_IDS.every((id) => caseMean(v3Train, id) >= caseMean(v2Train, id))
587
+ const promoted = v3HoldoutMean > v2HoldoutMean && v3TrainMean >= v2TrainMean - 0.05 && degenerateOk
588
+ const gateVerdict = promoted ? 'ship' : 'hold'
589
+
590
+ // Robustness: the same gate with every failed/missing cell imputed as 0.
591
+ const imputed = {
592
+ v2TrainMean: splitMeanImputedZero(v2Train, TRAIN_IDS),
593
+ v3TrainMean: splitMeanImputedZero(v3Train, TRAIN_IDS),
594
+ v2HoldoutMean: splitMeanImputedZero(v2Holdout, HOLDOUT_IDS),
595
+ v3HoldoutMean: splitMeanImputedZero(v3Holdout, HOLDOUT_IDS),
596
+ }
597
+ const promotedUnderImputation =
598
+ imputed.v3HoldoutMean > imputed.v2HoldoutMean &&
599
+ imputed.v3TrainMean >= imputed.v2TrainMean - 0.05 &&
600
+ degenerateOk
601
+
602
+ console.log('\nv2 (baseline surface):')
603
+ printSplit('train', v2Train, TRAIN_IDS)
604
+ printSplit('holdout', v2Holdout, HOLDOUT_IDS)
605
+ console.log('v3 (revised surface):')
606
+ printSplit('train', v3Train, TRAIN_IDS)
607
+ printSplit('holdout', v3Holdout, HOLDOUT_IDS)
608
+ console.log(`\ndegenerate check (anti-over-graphing cases, v2 → v3): ${JSON.stringify(degenerate)}`)
609
+ console.log(`retries: ${retryReceipts.length} receipt(s); unrecovered cell failures: ${cellFailures.length}`)
610
+ console.log(`upstream gate: ${result.gateResult.decision} [${result.gateResult.reasons.join(' | ')}]`)
611
+ console.log(
612
+ `protocol gate: ${gateVerdict} (holdout ${v2HoldoutMean.toFixed(3)} → ${v3HoldoutMean.toFixed(3)}, train ${v2TrainMean.toFixed(3)} → ${v3TrainMean.toFixed(3)}, degenerate ${degenerateOk ? 'ok' : 'REGRESSED'})`,
613
+ )
614
+
615
+ const out = {
616
+ generation: 3,
617
+ date: new Date().toISOString(),
618
+ smoke: SMOKE,
619
+ authorModel: 'glm-5.2',
620
+ authorTemperature: 0.2,
621
+ proposerModel: 'glm-5.2',
622
+ proposerTemperature: 0.7,
623
+ split: { train: TRAIN_IDS, holdout: HOLDOUT_IDS },
624
+ k: K,
625
+ seed: SEED,
626
+ surfaces: { v2Sha256: v2Sha, v3Sha256: v3Sha, v3Label: result.generations[0]?.record.candidates[0]?.label },
627
+ perCase: {
628
+ v2: { train: tableFor(v2Train, TRAIN_IDS), holdout: tableFor(v2Holdout, HOLDOUT_IDS) },
629
+ v3: { train: tableFor(v3Train, TRAIN_IDS), holdout: tableFor(v3Holdout, HOLDOUT_IDS) },
630
+ },
631
+ aggregates: {
632
+ v2: { trainMean: v2TrainMean, holdoutMean: v2HoldoutMean },
633
+ v3: { trainMean: v3TrainMean, holdoutMean: v3HoldoutMean },
634
+ },
635
+ trainFailureTallyV2: tallyTrainFailures(v2Sha),
636
+ degenerateCheck: degenerate,
637
+ retryReceipts,
638
+ cellFailures,
639
+ worstCaseImputation: { ...imputed, promotedUnderImputation, note: 'every failed or missing rep scored 0 with denominator k' },
640
+ upstreamGate: result.gateResult,
641
+ upstreamWinnerWasCandidate: winnerIsCandidate,
642
+ gateVerdict,
643
+ promoted,
644
+ revisionPromptSha256,
645
+ v3Surface,
646
+ }
647
+ mkdirSync(dirname(EFFECTIVE_OUT), { recursive: true })
648
+ writeFileSync(EFFECTIVE_OUT, `${JSON.stringify(out, null, 2)}\n`)
649
+ console.log(`written: ${EFFECTIVE_OUT}`)
650
+
651
+ if (promoted && !SMOKE) {
652
+ writeFileSync(SKILL_PATH, v3Surface)
653
+ console.log(`promoted: ${SKILL_PATH} replaced with v3 (${v3Sha.slice(0, 12)})`)
654
+ }
655
+ }
656
+
657
+ main().catch((err) => {
658
+ console.error(err instanceof Error ? (err.stack ?? err.message) : String(err))
659
+ process.exit(1)
660
+ })