@tangle-network/agent-bench 0.1.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 (111) hide show
  1. package/README.md +15 -0
  2. package/package.json +35 -0
  3. package/src/adapters.ts +60 -0
  4. package/src/aec-gate.mts +217 -0
  5. package/src/atom-humaneval.mts +197 -0
  6. package/src/atom-mcp-e2e.mts +223 -0
  7. package/src/benchmarks/_harness.ts +206 -0
  8. package/src/benchmarks/aec-bench.test.mts +53 -0
  9. package/src/benchmarks/aec-bench.ts +319 -0
  10. package/src/benchmarks/appworld.test.mts +45 -0
  11. package/src/benchmarks/appworld.ts +426 -0
  12. package/src/benchmarks/cad-design.ts +429 -0
  13. package/src/benchmarks/cadbench.ts +135 -0
  14. package/src/benchmarks/cadgenbench.ts +121 -0
  15. package/src/benchmarks/commit0.test.mts +71 -0
  16. package/src/benchmarks/commit0.ts +266 -0
  17. package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
  18. package/src/benchmarks/enterpriseops-gym.ts +343 -0
  19. package/src/benchmarks/finsearchcomp.ts +371 -0
  20. package/src/benchmarks/frames.ts +520 -0
  21. package/src/benchmarks/hotpotqa.ts +320 -0
  22. package/src/benchmarks/humaneval.ts +251 -0
  23. package/src/benchmarks/mind2web.ts +311 -0
  24. package/src/benchmarks/programbench.test.mts +60 -0
  25. package/src/benchmarks/programbench.ts +210 -0
  26. package/src/benchmarks/simpleqa.ts +390 -0
  27. package/src/benchmarks/swe-bench.ts +152 -0
  28. package/src/benchmarks/terminal-bench.ts +182 -0
  29. package/src/benchmarks/trata-hedge.ts +496 -0
  30. package/src/benchmarks/types.ts +58 -0
  31. package/src/browser/adapters/bad-design-audit.ts +153 -0
  32. package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
  33. package/src/browser/adapters/bad.ts +165 -0
  34. package/src/browser/agent-adapter.ts +145 -0
  35. package/src/browser/process-adapter.ts +146 -0
  36. package/src/browser/run-to-spans.ts +105 -0
  37. package/src/browser/run-to-spans.verify.ts +89 -0
  38. package/src/browser/ui-reviewer.ts +200 -0
  39. package/src/browser/ui-reviewer.verify.ts +94 -0
  40. package/src/browser/verify.ts +96 -0
  41. package/src/clbench-codebase-gate.mts +314 -0
  42. package/src/clbench-context-gate.mts +305 -0
  43. package/src/cloud-loop.mts +138 -0
  44. package/src/coding-skills/minimal-diff.md +9 -0
  45. package/src/coding-skills/read-before-edit.md +9 -0
  46. package/src/coding-skills/reproduce-first.md +10 -0
  47. package/src/coding-skills/run-tests-after-edit.md +9 -0
  48. package/src/coding-skills/trace-the-failure.md +9 -0
  49. package/src/commit0-env-run.mts +59 -0
  50. package/src/commit0-env.ts +173 -0
  51. package/src/commit0-gate.mts +529 -0
  52. package/src/commit0-prereqs.sh +48 -0
  53. package/src/corpus-replay.mts +300 -0
  54. package/src/corpus-report.mts +504 -0
  55. package/src/corpus.test.mts +273 -0
  56. package/src/corpus.ts +325 -0
  57. package/src/decoder-live.mts +133 -0
  58. package/src/directives.ts +84 -0
  59. package/src/diverse-gate.mjs +112 -0
  60. package/src/egress-probe.mts +26 -0
  61. package/src/eops-skills/address-every-subtask.md +5 -0
  62. package/src/eops-skills/exact-tools-and-args.md +5 -0
  63. package/src/eops-skills/full-sequence-to-goal.md +5 -0
  64. package/src/eops-skills/ground-every-value.md +5 -0
  65. package/src/eops-skills/honor-the-policies.md +5 -0
  66. package/src/examples/README.md +58 -0
  67. package/src/examples/math-demo.mts +110 -0
  68. package/src/examples/strategy-demo.mts +119 -0
  69. package/src/fleet.mts +121 -0
  70. package/src/gate-cli.mts +101 -0
  71. package/src/gate.test.mts +129 -0
  72. package/src/gate.ts +460 -0
  73. package/src/generate-eval/certify.ts +178 -0
  74. package/src/generate-eval/schema.ts +78 -0
  75. package/src/humaneval-gate.mts +204 -0
  76. package/src/humaneval-repair-gate.mts +143 -0
  77. package/src/index.ts +19 -0
  78. package/src/mcp-mount-probe.mts +126 -0
  79. package/src/profile-coordinates.ts +134 -0
  80. package/src/profiles.ts +128 -0
  81. package/src/refine-loop.test.mts +106 -0
  82. package/src/refine-loop.ts +106 -0
  83. package/src/research-gate.mts +132 -0
  84. package/src/research-shot.ts +134 -0
  85. package/src/resolve-client.ts +58 -0
  86. package/src/router-executor.ts +51 -0
  87. package/src/run-pool.ts +48 -0
  88. package/src/runtime-hook-recorder.ts +137 -0
  89. package/src/sandbox-run.ts +125 -0
  90. package/src/search-bench/bridge.ts +124 -0
  91. package/src/search-bench/export.mts +0 -0
  92. package/src/search-bench/parametric-check.mts +63 -0
  93. package/src/search-bench/profiles.ts +98 -0
  94. package/src/search-bench/run.mts +287 -0
  95. package/src/search-bench/tasks-fresh.ts +688 -0
  96. package/src/search-bench/tasks.ts +129 -0
  97. package/src/search-tool.ts +95 -0
  98. package/src/selector.test.mts +189 -0
  99. package/src/selector.ts +366 -0
  100. package/src/skill-sandbox-smoke.mts +100 -0
  101. package/src/stats.mts +90 -0
  102. package/src/terminal-compare.ts +519 -0
  103. package/src/trajectory-assemble.mjs +130 -0
  104. package/src/trata-gate.mts +243 -0
  105. package/src/trata-gepa.mts +434 -0
  106. package/src/worker-blender.ts +230 -0
  107. package/src/worker-browser.ts +102 -0
  108. package/src/worker-build123d.ts +143 -0
  109. package/src/worker-cad.ts +451 -0
  110. package/src/worker.ts +136 -0
  111. package/src/workspace-loop.mts +133 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Deployable, non-oracle selector (docs/roadmap-rsi.md Phase 1).
3
+ *
4
+ * best-of-N only pays if you can pick the good attempt WITHOUT the judge. Today
5
+ * the loop's winner is judge-selected (verdict.score) — an oracle upper bound, not
6
+ * a deployable system. A `Selector` ranks the k candidate OUTPUTS using only
7
+ * trace-observable signal and never the judge verdict. The type enforces the
8
+ * firewall: a Selector is `(outputs: string[]) => index`; it is structurally
9
+ * incapable of reading `valid`/`score`, so it cannot become an oracle by accident.
10
+ *
11
+ * Scoring is OFFLINE over the corpus: each random@k RunRecord already holds k
12
+ * attempts each with a stored `valid`, so picking index i and reading
13
+ * attempts[i].valid is the judge's verdict of the pick — zero new rollouts, zero
14
+ * new judge calls. selector@k vs random@k (does picking beat a blind random draw?)
15
+ * and selector@k vs oracle@k (how much ceiling is recoverable?) fall straight out.
16
+ */
17
+
18
+ import type { AttemptRecord, RunRecord } from './corpus'
19
+
20
+ /** Picks one of `outputs` by index, using the candidate text only. Never the verdict. */
21
+ export type Selector = (outputs: string[]) => number
22
+
23
+ /** Normalize a short free-text answer so equivalent answers cluster: lowercase,
24
+ * trim, collapse whitespace, strip surrounding quotes/punctuation, drop a leading
25
+ * article. Deliberately conservative — it clusters trivial restatements, not
26
+ * paraphrases (that is the selector's known ceiling, not a bug). */
27
+ export function normalizeAnswer(s: string): string {
28
+ return s
29
+ .toLowerCase()
30
+ .trim()
31
+ .replace(/\s+/g, ' ')
32
+ .replace(/^[\s"'`([{]+|[\s"'`)\]}.,;:!?]+$/g, '')
33
+ .replace(/^(the|a|an)\s+/, '')
34
+ .trim()
35
+ }
36
+
37
+ /**
38
+ * Self-consistency (Wang 2022): pick the output that agrees with the most others
39
+ * after normalization. Ties break to the cluster whose first member appears
40
+ * earliest, and within the winning cluster to its earliest member — deterministic,
41
+ * so test-retest flip rate is 0 by construction (the sanity floor). Empty input is
42
+ * a caller bug (fail loud).
43
+ */
44
+ export const selfConsistencySelect: Selector = (outputs) => {
45
+ if (outputs.length === 0) throw new Error('selfConsistencySelect: no candidate outputs')
46
+ const counts = new Map<string, number>()
47
+ for (const o of outputs) {
48
+ const k = normalizeAnswer(o)
49
+ counts.set(k, (counts.get(k) ?? 0) + 1)
50
+ }
51
+ let bestIdx = 0
52
+ let bestCount = -1
53
+ for (let i = 0; i < outputs.length; i += 1) {
54
+ const c = counts.get(normalizeAnswer(outputs[i] as string)) ?? 0
55
+ if (c > bestCount) {
56
+ bestCount = c
57
+ bestIdx = i // first member of the largest cluster encountered
58
+ }
59
+ }
60
+ return bestIdx
61
+ }
62
+
63
+ /**
64
+ * Verifier-grounded selection: pick the candidate with the highest deployable-checker
65
+ * pass-count (ties → earliest). The pass-counts come from a checker the agent can run
66
+ * in production (a provided test suite), NOT from the gold answer — so unlike a
67
+ * judge/oracle pick this is deployable. A boolean pass/fail is the pass-count's {0,1}
68
+ * special case. Pure and unit-shaped: it reads only the supplied counts, never any
69
+ * verdict. Empty input is a caller bug (fail loud); a negative count is invalid.
70
+ */
71
+ export function verifierGroundedSelect(passCounts: ReadonlyArray<number>): number {
72
+ if (passCounts.length === 0) throw new Error('verifierGroundedSelect: no candidate pass-counts')
73
+ let bestIdx = 0
74
+ let bestCount = Number.NEGATIVE_INFINITY
75
+ for (let i = 0; i < passCounts.length; i += 1) {
76
+ const c = passCounts[i] as number
77
+ if (!Number.isFinite(c) || c < 0) {
78
+ throw new Error(`verifierGroundedSelect: invalid pass-count ${c} at index ${i}`)
79
+ }
80
+ if (c > bestCount) {
81
+ bestCount = c
82
+ bestIdx = i
83
+ }
84
+ }
85
+ return bestIdx
86
+ }
87
+
88
+ /** One instance's offline selector outcome, all derived from stored verdicts. */
89
+ export interface SelectorOutcome {
90
+ /** usable candidates (attempts with a non-empty output) */
91
+ k: number
92
+ /** the picked attempt's stored verdict — selector@k for this instance */
93
+ selectorResolved: boolean
94
+ /** nValid / k — expected pass of a uniform-random pick (the compute control) */
95
+ randomExpected: number
96
+ /** any usable candidate passed — the oracle ceiling */
97
+ oracle: boolean
98
+ /** round-1 verdict — pass@1 */
99
+ blind: boolean
100
+ }
101
+
102
+ const usableCandidates = (record: RunRecord): AttemptRecord[] =>
103
+ record.attempts.filter((a) => typeof a.output === 'string' && a.output.length > 0)
104
+
105
+ /**
106
+ * Score a selector on one RunRecord offline. Returns null when no attempt produced
107
+ * an output (unscoreable — not a pass). The selector sees OUTPUTS ONLY; the stored
108
+ * `valid` flags are read after the pick, never passed to the selector.
109
+ */
110
+ export function scoreSelectorOnRun(record: RunRecord, select: Selector): SelectorOutcome | null {
111
+ const usable = usableCandidates(record)
112
+ if (usable.length === 0) return null
113
+ const picked = select(usable.map((a) => a.output as string))
114
+ if (!Number.isInteger(picked) || picked < 0 || picked >= usable.length) {
115
+ throw new Error(`selector returned out-of-range index ${picked} for ${usable.length} candidates`)
116
+ }
117
+ const nValid = usable.filter((a) => a.valid === true).length
118
+ return {
119
+ k: usable.length,
120
+ selectorResolved: usable[picked]?.valid === true,
121
+ randomExpected: nValid / usable.length,
122
+ oracle: nValid > 0,
123
+ blind: record.blindResolved === true,
124
+ }
125
+ }
126
+
127
+ export interface SelectorReport {
128
+ /** scoreable instances */
129
+ n: number
130
+ selectorRate: number
131
+ randomRate: number
132
+ oracleRate: number
133
+ blindRate: number
134
+ /** selector@k − random@k: does picking beat a blind random draw at the same k? (the gate) */
135
+ dVsRandom: number
136
+ /** selector@k − blind: total lift over one shot */
137
+ dVsBlind: number
138
+ /** oracle@k − selector@k: ceiling left on the table */
139
+ gapToOracle: number
140
+ /** instances skipped as unscoreable (no output in any attempt) */
141
+ skipped: number
142
+ }
143
+
144
+ /** Aggregate the offline selector outcome across a corpus slice (one condition). */
145
+ export function summarizeSelector(records: RunRecord[], select: Selector): SelectorReport {
146
+ let n = 0
147
+ let selector = 0
148
+ let randomExp = 0
149
+ let oracle = 0
150
+ let blind = 0
151
+ let skipped = 0
152
+ for (const r of records) {
153
+ const o = scoreSelectorOnRun(r, select)
154
+ if (o === null) {
155
+ skipped += 1
156
+ continue
157
+ }
158
+ n += 1
159
+ if (o.selectorResolved) selector += 1
160
+ randomExp += o.randomExpected
161
+ if (o.oracle) oracle += 1
162
+ if (o.blind) blind += 1
163
+ }
164
+ const rate = (x: number) => (n > 0 ? x / n : 0)
165
+ const selectorRate = rate(selector)
166
+ const randomRate = rate(randomExp)
167
+ const oracleRate = rate(oracle)
168
+ const blindRate = rate(blind)
169
+ return {
170
+ n,
171
+ selectorRate,
172
+ randomRate,
173
+ oracleRate,
174
+ blindRate,
175
+ dVsRandom: selectorRate - randomRate,
176
+ dVsBlind: selectorRate - blindRate,
177
+ gapToOracle: oracleRate - selectorRate,
178
+ skipped,
179
+ }
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Verifier-grounded selection over a CONTINUOUS deployable-checker reward.
184
+ //
185
+ // HumanEval proved verifier-grounded selection captures the oracle ceiling on a
186
+ // BINARY pass/fail checker. This is the continuous-reward analog for graded
187
+ // domains (commit0's pytest pass-rate, aec's tests/verify.py partial credit):
188
+ // each attempt carries a stored `score` from a DEPLOYABLE checker the agent could
189
+ // re-run itself — NOT the gold answer. The selector picks argmax(score); the gate
190
+ // is selector-reward vs a blind random draw at equal k.
191
+ //
192
+ // Admissibility: `score` MUST be the deployable checker's reward (re-runnable
193
+ // without the gold — provided unit tests / a verify.py that recomputes GT). It is
194
+ // the same signal the selector would have in production. `valid` (the gold
195
+ // resolve verdict) is read AFTER selection only to MEASURE the pick's binary
196
+ // outcome; selection itself ranks by the checker score, never by the gold.
197
+ // ---------------------------------------------------------------------------
198
+
199
+ const scoredAttempts = (record: RunRecord): AttemptRecord[] =>
200
+ record.attempts.filter((a) => typeof a.score === 'number' && Number.isFinite(a.score))
201
+
202
+ /** One instance's verifier-grounded outcome on the continuous graded reward. */
203
+ export interface VerifierSelectorOutcome {
204
+ k: number
205
+ /** reward of the argmax-score pick (== max score) — what a deployable best-of-k ships */
206
+ selectorReward: number
207
+ /** mean score — expected reward of a uniform-random pick (the blind compute control) */
208
+ randomReward: number
209
+ /** max score — the best-of-k ceiling */
210
+ oracleReward: number
211
+ /** round-1 score — one-shot reward */
212
+ blindReward: number
213
+ /** did the verifier-picked attempt fully resolve (binary) */
214
+ selectorResolved: boolean
215
+ oracleResolved: boolean
216
+ blindResolved: boolean
217
+ }
218
+
219
+ /**
220
+ * Score verifier-grounded selection on one RunRecord offline. Returns null when no
221
+ * attempt carries a numeric checker score (unscoreable). The selector ranks by the
222
+ * stored `score` only; `valid` is read post-pick to report the binary outcome.
223
+ */
224
+ export function scoreVerifierSelectorOnRun(record: RunRecord): VerifierSelectorOutcome | null {
225
+ const usable = scoredAttempts(record)
226
+ if (usable.length === 0) return null
227
+ const scores = usable.map((a) => a.score as number)
228
+ const picked = verifierGroundedSelect(scores)
229
+ const sum = scores.reduce((s, x) => s + x, 0)
230
+ const round1 = usable.find((a) => a.round === 0) ?? (usable[0] as AttemptRecord)
231
+ return {
232
+ k: usable.length,
233
+ selectorReward: scores[picked] as number,
234
+ randomReward: sum / usable.length,
235
+ oracleReward: Math.max(...scores),
236
+ blindReward: round1.score as number,
237
+ selectorResolved: usable[picked]?.valid === true,
238
+ oracleResolved: usable.some((a) => a.valid === true),
239
+ blindResolved: record.blindResolved === true,
240
+ }
241
+ }
242
+
243
+ /** Deterministic PRNG so the bootstrap CI is reproducible (no Math.random). */
244
+ function mulberry32(seed: number): () => number {
245
+ let a = seed >>> 0
246
+ return () => {
247
+ a = (a + 0x6d2b79f5) | 0
248
+ let t = Math.imul(a ^ (a >>> 15), 1 | a)
249
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
250
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
251
+ }
252
+ }
253
+
254
+ /** Two-sided percentile bootstrap (95% CI + p) on the mean of paired per-instance
255
+ * deltas — the same instrument corpus-report uses, applied to the within-corpus
256
+ * (selector − random) reward delta. Deterministic seed: same corpus → same CI. */
257
+ export function bootstrapDeltaCI(
258
+ deltas: readonly number[],
259
+ resamples = 10000,
260
+ seed = 0xc0ffee,
261
+ ): { mean: number; lo: number; hi: number; p: number; discordant: number } {
262
+ const n = deltas.length
263
+ if (n === 0) return { mean: 0, lo: 0, hi: 0, p: 1, discordant: 0 }
264
+ const rng = mulberry32(seed)
265
+ const means = new Array<number>(resamples)
266
+ for (let b = 0; b < resamples; b += 1) {
267
+ let s = 0
268
+ for (let i = 0; i < n; i += 1) s += deltas[(rng() * n) | 0] as number
269
+ means[b] = s / n
270
+ }
271
+ means.sort((x, y) => x - y)
272
+ let ge0 = 0
273
+ for (const m of means) if (m >= 0) ge0 += 1
274
+ const pOne = Math.min(ge0, resamples - ge0) / resamples
275
+ return {
276
+ mean: deltas.reduce((s, d) => s + d, 0) / n,
277
+ lo: means[Math.floor(0.025 * resamples)] as number,
278
+ hi: means[Math.floor(0.975 * resamples)] as number,
279
+ p: Math.min(1, 2 * pOne),
280
+ discordant: deltas.filter((d) => d !== 0).length,
281
+ }
282
+ }
283
+
284
+ export interface VerifierSelectorReport {
285
+ /** scoreable instances */
286
+ n: number
287
+ selectorReward: number
288
+ randomReward: number
289
+ oracleReward: number
290
+ blindReward: number
291
+ selectorResolveRate: number
292
+ oracleResolveRate: number
293
+ blindResolveRate: number
294
+ /** THE GATE: deployable selector reward − blind random-draw reward at equal k. */
295
+ rewardVsRandom: number
296
+ /** selector reward − one-shot reward (total best-of-k lift). */
297
+ rewardVsBlind: number
298
+ /** 95% bootstrap CI + two-sided p on the paired (selector − random) per-instance delta. */
299
+ ci: { mean: number; lo: number; hi: number; p: number; discordant: number }
300
+ /** instances skipped as unscoreable (no checker score on any attempt) */
301
+ skipped: number
302
+ }
303
+
304
+ /** Aggregate verifier-grounded selection across a corpus slice (one condition). */
305
+ export function summarizeVerifierSelector(records: RunRecord[]): VerifierSelectorReport {
306
+ let n = 0
307
+ let selR = 0
308
+ let randR = 0
309
+ let oraR = 0
310
+ let blindR = 0
311
+ let selRes = 0
312
+ let oraRes = 0
313
+ let blindRes = 0
314
+ let skipped = 0
315
+ const deltas: number[] = []
316
+ for (const r of records) {
317
+ const o = scoreVerifierSelectorOnRun(r)
318
+ if (o === null) {
319
+ skipped += 1
320
+ continue
321
+ }
322
+ n += 1
323
+ selR += o.selectorReward
324
+ randR += o.randomReward
325
+ oraR += o.oracleReward
326
+ blindR += o.blindReward
327
+ if (o.selectorResolved) selRes += 1
328
+ if (o.oracleResolved) oraRes += 1
329
+ if (o.blindResolved) blindRes += 1
330
+ deltas.push(o.selectorReward - o.randomReward)
331
+ }
332
+ const rate = (x: number) => (n > 0 ? x / n : 0)
333
+ return {
334
+ n,
335
+ selectorReward: rate(selR),
336
+ randomReward: rate(randR),
337
+ oracleReward: rate(oraR),
338
+ blindReward: rate(blindR),
339
+ selectorResolveRate: rate(selRes),
340
+ oracleResolveRate: rate(oraRes),
341
+ blindResolveRate: rate(blindRes),
342
+ rewardVsRandom: rate(selR) - rate(randR),
343
+ rewardVsBlind: rate(selR) - rate(blindR),
344
+ ci: bootstrapDeltaCI(deltas),
345
+ skipped,
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Test-retest: fraction of records where two selector runs pick a DIFFERENT index.
351
+ * A deterministic selector against itself is 0 (the floor); a stochastic selector
352
+ * (or two seeds) reveals how much its decision is noise. A selector whose flips
353
+ * move the resolve rate is not a selector.
354
+ */
355
+ export function flipRate(records: RunRecord[], a: Selector, b: Selector): number {
356
+ let n = 0
357
+ let flips = 0
358
+ for (const r of records) {
359
+ const usable = usableCandidates(r)
360
+ if (usable.length === 0) continue
361
+ const outs = usable.map((x) => x.output as string)
362
+ n += 1
363
+ if (a(outs) !== b(outs)) flips += 1
364
+ }
365
+ return n > 0 ? flips / n : 0
366
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Proof that a SKILL.md from an AgentProfile actually materializes inside the sandbox where the
3
+ * coding harness loads it — the "are we on the right surface" check before any benchmark.
4
+ * Creates one opencode box with resources.skills, then asks the IN-SESSION agent to read the
5
+ * skill back. PASS keys on a UNIQUE MARKER in the skill body, so a hit is unambiguously OUR
6
+ * skill — not opencode's bundled /nix/store skills and not the agent's prose.
7
+ *
8
+ * Two gotchas this guards against (both produced false readings during bring-up):
9
+ * 1. PATH: opencode's discovery dir is ~/.opencode/skill/<name>/SKILL.md (SINGULAR "skill");
10
+ * claude-code uses ~/.claude/skills/. resources.skills lands in the backend's own dir.
11
+ * 2. VIEW: a bare box.exec() WITHOUT a sessionId sees a DIFFERENT filesystem than the agent
12
+ * session (SDK ExecOptions docs). So we verify through the in-SESSION agent (streamPrompt),
13
+ * the path the bench actually runs — not a detached exec.
14
+ *
15
+ * Run: dotenvx run -f ~/company/devops/secrets/.env.keys -- pnpm exec tsx bench/src/skill-sandbox-smoke.mts
16
+ */
17
+ import { Sandbox, defineInlineResource } from '@tangle-network/sandbox'
18
+
19
+ const must = (k: string): string => {
20
+ const v = process.env[k]
21
+ if (!v) throw new Error(`env ${k} required`)
22
+ return v
23
+ }
24
+
25
+ const skillName = 'reproduce-first'
26
+ // A unique token that cannot occur in opencode's bundled skills or in plausible prose.
27
+ const marker = 'SKILLMAT-OK-7f3c91'
28
+ const skillMd = [
29
+ '---',
30
+ `name: ${skillName}`,
31
+ 'description: Reproduce the failing test before changing any code.',
32
+ '---',
33
+ '',
34
+ `Materialization proof token: ${marker}`,
35
+ 'When fixing a bug: run the failing test FIRST to observe the real error, make the smallest',
36
+ 'change that turns it green, then re-run to confirm.',
37
+ ].join('\n')
38
+
39
+ async function main(): Promise<void> {
40
+ const client = new Sandbox({
41
+ baseUrl: process.env.SANDBOX_BASE_URL ?? 'https://sandbox.tangle.tools',
42
+ apiKey: must('TANGLE_API_KEY'),
43
+ timeoutMs: 600_000,
44
+ } as never)
45
+
46
+ console.error(`[smoke] creating opencode box with resources.skills=[${skillName}]…`)
47
+ const box: Record<string, (...a: never[]) => unknown> & { id?: string } = (await client.create({
48
+ backend: {
49
+ type: 'opencode',
50
+ model: {
51
+ provider: process.env.WORKER_PROVIDER ?? 'openai',
52
+ model: process.env.WORKER_MODEL ?? 'deepseek-v4-flash',
53
+ baseUrl: process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1',
54
+ },
55
+ profile: { name: 'skill-smoke', resources: { skills: [defineInlineResource(skillName, skillMd)] } },
56
+ },
57
+ } as never)) as never
58
+ console.error('[smoke] box id:', box.id, '— waiting for running…')
59
+ await box.waitFor('running' as never, { timeoutMs: 180_000 } as never)
60
+ console.error('[smoke] box running; asking the IN-SESSION agent to read the materialized skill back…')
61
+
62
+ // cat the skill at BOTH backend discovery dirs (opencode singular, claude-code plural) + a
63
+ // name-scoped find; the marker appears wherever resources.skills actually landed it.
64
+ const prompt =
65
+ 'Run this exact shell command and paste its raw output verbatim, nothing else:\n' +
66
+ `\`cat ~/.opencode/skill/${skillName}/SKILL.md ~/.claude/skills/${skillName}/SKILL.md 2>/dev/null; ` +
67
+ `echo "--FIND--"; find / -name SKILL.md 2>/dev/null | grep -i ${skillName} | grep -vi /nix/store || echo NOFILE\``
68
+ let out = ''
69
+ const ac = new AbortController()
70
+ const timer = setTimeout(() => ac.abort(), 220_000)
71
+ try {
72
+ for await (const ev of box.streamPrompt(prompt as never, { signal: ac.signal } as never) as AsyncGenerator<unknown>) {
73
+ out += (typeof ev === 'string' ? ev : JSON.stringify(ev)) + '\n'
74
+ }
75
+ } finally {
76
+ clearTimeout(timer)
77
+ }
78
+ console.error(`[smoke] stream chars: ${out.length}`)
79
+ console.error('[smoke] tail of stream:\n' + out.slice(-1500))
80
+
81
+ try {
82
+ await box.delete?.()
83
+ } catch {
84
+ /* best-effort cleanup */
85
+ }
86
+
87
+ // Deterministic: PASS only if the agent read OUR unique marker out of the file — not a loose
88
+ // match on the skill name (which appears in the prompt/prose) or on "SKILL.md" (bundled skills).
89
+ const landed = out.includes(marker)
90
+ const path = /\.opencode\/skill\//.test(out) ? '~/.opencode/skill (opencode)' : /\.claude\/skills\//.test(out) ? '~/.claude/skills (claude-code)' : '(path not surfaced)'
91
+ console.error(
92
+ `\n[smoke] VERDICT: ${landed ? `PASS — resources.skills materialized + the in-session agent read the marker at ${path}` : 'FAIL — marker NOT read back; resources.skills did not land where the harness reads (check backend discovery dir)'}`,
93
+ )
94
+ process.exit(landed ? 0 : 2)
95
+ }
96
+
97
+ main().catch((e) => {
98
+ console.error('[smoke] FAILED:', e instanceof Error ? (e.stack ?? e.message) : e)
99
+ process.exit(1)
100
+ })
package/src/stats.mts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Shared statistics + scheduling helpers for the gate runners.
3
+ *
4
+ * The gate runners (humaneval / commit0 / aec / clbench-context / clbench-codebase)
5
+ * each hand-rolled the same three primitives: a bounded-concurrency pool, the
6
+ * mulberry32 PRNG, and the paired-bootstrap lift. They live here once.
7
+ *
8
+ * Two pools coexist on purpose, because the gates and the batch runners want
9
+ * different fault contracts:
10
+ * - `runPool` (re-exported from ./run-pool) catches a per-item throw and yields
11
+ * `{ ok, error }` for THAT item — the batch never aborts. Used by the batch
12
+ * runners / experiment harness that aggregate partial results.
13
+ * - `pool` (here) lets the worker throw and propagates the rejection — the gates
14
+ * want a router/sandbox fault to abort the arm loudly rather than score a
15
+ * phantom 0. It returns `R[]` in item order. This matches the gates' existing
16
+ * private `pool()` exactly; it is NOT a re-export of `runPool`.
17
+ */
18
+
19
+ export { runPool, type PoolOutcome } from './run-pool'
20
+
21
+ /** Bounded-concurrency pool: run `fn` over `items`, at most `limit` in flight,
22
+ * results returned in item order. A worker that throws propagates the rejection
23
+ * (fail-loud) — the gates rely on this so a router/judge fault aborts the arm
24
+ * rather than being silently scored. */
25
+ export async function pool<T, R>(items: T[], limit: number, fn: (item: T, idx: number) => Promise<R>): Promise<R[]> {
26
+ const results: R[] = new Array(items.length)
27
+ let next = 0
28
+ async function worker(): Promise<void> {
29
+ for (;;) {
30
+ const idx = next
31
+ next += 1
32
+ if (idx >= items.length) return
33
+ results[idx] = await fn(items[idx] as T, idx)
34
+ }
35
+ }
36
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, () => worker()))
37
+ return results
38
+ }
39
+
40
+ /** mulberry32 — deterministic PRNG (Math.imul). Seeded once at 0x9e3779b9 by
41
+ * `pairedLift` so every gate's bootstrap CI is reproducible and identical. */
42
+ export function makeRng(seed: number): () => number {
43
+ let s = seed | 0
44
+ return () => {
45
+ s = (s + 0x6d2b79f5) | 0
46
+ let t = Math.imul(s ^ (s >>> 15), 1 | s)
47
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
48
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
49
+ }
50
+ }
51
+
52
+ export interface PairedLift {
53
+ point: number
54
+ low: number
55
+ high: number
56
+ pairs: number
57
+ discordant: number
58
+ }
59
+
60
+ /** Paired lift = mean over tasks of (treatment − baseline) with a 95% bootstrap
61
+ * CI from resampling the paired tasks (mulberry32 seeded 0x9e3779b9, B=10000).
62
+ * Works on {0,1} arms (humaneval/aec/commit0) and continuous values (clbench
63
+ * rubric fractions) alike. `discordant` uses a |d| > 1e-9 tolerance, which equals
64
+ * the exact `d !== 0` test on integer-delta (binary) arms and avoids counting
65
+ * float noise as a discordant pair on continuous arms. */
66
+ export function pairedLift(baseline: number[], treatment: number[], bootstrapN = 10000): PairedLift {
67
+ if (baseline.length !== treatment.length) throw new Error('pairedLift: misaligned arms')
68
+ const n = baseline.length
69
+ if (n === 0) throw new Error('pairedLift: no pairs')
70
+ const deltas = baseline.map((b, i) => (treatment[i] as number) - b)
71
+ const mean = (a: number[]) => a.reduce((s, x) => s + x, 0) / a.length
72
+ const point = mean(deltas)
73
+ const discordant = deltas.filter((d) => Math.abs(d) > 1e-9).length
74
+ const rng = makeRng(0x9e3779b9)
75
+ const rint = (m: number) => Math.floor(rng() * m)
76
+ const boots: number[] = []
77
+ for (let b = 0; b < bootstrapN; b += 1) {
78
+ let acc = 0
79
+ for (let j = 0; j < n; j += 1) acc += deltas[rint(n)] as number
80
+ boots.push(acc / n)
81
+ }
82
+ boots.sort((x, y) => x - y)
83
+ return {
84
+ point,
85
+ low: boots[Math.floor(0.025 * bootstrapN)] ?? Number.NaN,
86
+ high: boots[Math.floor(0.975 * bootstrapN)] ?? Number.NaN,
87
+ pairs: n,
88
+ discordant,
89
+ }
90
+ }