issue-flow 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -274
- package/dist/agent-BXRIEKVM.js +238 -0
- package/dist/agent-BXRIEKVM.js.map +1 -0
- package/dist/{analyze-OEET7KGE.js → analyze-WS47UYGN.js} +28 -16
- package/dist/analyze-WS47UYGN.js.map +1 -0
- package/dist/{apply-ZIA3C52S.js → apply-OS62UWRN.js} +15 -7
- package/dist/{apply-ZIA3C52S.js.map → apply-OS62UWRN.js.map} +1 -1
- package/dist/bench-SA3BOME5.js +906 -0
- package/dist/bench-SA3BOME5.js.map +1 -0
- package/dist/chunk-37N2RJW2.js +120 -0
- package/dist/chunk-37N2RJW2.js.map +1 -0
- package/dist/chunk-3XLCNVMD.js +180 -0
- package/dist/chunk-3XLCNVMD.js.map +1 -0
- package/dist/chunk-5TB6KLC2.js +72 -0
- package/dist/chunk-5TB6KLC2.js.map +1 -0
- package/dist/{chunk-MJULIKQU.js → chunk-664Y3NDX.js} +23 -47
- package/dist/chunk-664Y3NDX.js.map +1 -0
- package/dist/chunk-6FDFRNXZ.js +357 -0
- package/dist/chunk-6FDFRNXZ.js.map +1 -0
- package/dist/{chunk-56KS3TFQ.js → chunk-7KD66LIM.js} +115 -160
- package/dist/chunk-7KD66LIM.js.map +1 -0
- package/dist/chunk-7Z4Y3VKM.js +26 -0
- package/dist/chunk-7Z4Y3VKM.js.map +1 -0
- package/dist/chunk-ARFVHC2F.js +1111 -0
- package/dist/chunk-ARFVHC2F.js.map +1 -0
- package/dist/{chunk-NS6R37II.js → chunk-BHJCGWQX.js} +141 -12
- package/dist/chunk-BHJCGWQX.js.map +1 -0
- package/dist/chunk-BQDQTIM7.js +74 -0
- package/dist/chunk-BQDQTIM7.js.map +1 -0
- package/dist/chunk-CNBYCMSU.js +2709 -0
- package/dist/chunk-CNBYCMSU.js.map +1 -0
- package/dist/chunk-CRGIQFUQ.js +83 -0
- package/dist/chunk-CRGIQFUQ.js.map +1 -0
- package/dist/chunk-DIG5XKQQ.js +298 -0
- package/dist/chunk-DIG5XKQQ.js.map +1 -0
- package/dist/chunk-FZDR7CGH.js +71 -0
- package/dist/chunk-FZDR7CGH.js.map +1 -0
- package/dist/chunk-HEF2RMKN.js +413 -0
- package/dist/chunk-HEF2RMKN.js.map +1 -0
- package/dist/{chunk-M5UCXTIT.js → chunk-HJGGNLWI.js} +22 -18
- package/dist/chunk-HJGGNLWI.js.map +1 -0
- package/dist/chunk-HQAHNIAR.js +283 -0
- package/dist/chunk-HQAHNIAR.js.map +1 -0
- package/dist/{chunk-FJP3UQGD.js → chunk-IHIEXUTC.js} +39 -19
- package/dist/chunk-IHIEXUTC.js.map +1 -0
- package/dist/{chunk-Y5ZDJ4PJ.js → chunk-IV5SS4JN.js} +18 -10
- package/dist/chunk-IV5SS4JN.js.map +1 -0
- package/dist/{chunk-BYZDPR7N.js → chunk-IYV6KNVF.js} +146 -13
- package/dist/chunk-IYV6KNVF.js.map +1 -0
- package/dist/{chunk-TSOLWK7Y.js → chunk-J62QSVP6.js} +8 -8
- package/dist/chunk-KQHB7ZT3.js +559 -0
- package/dist/chunk-KQHB7ZT3.js.map +1 -0
- package/dist/chunk-L6HSKTQE.js +77 -0
- package/dist/chunk-L6HSKTQE.js.map +1 -0
- package/dist/{chunk-3QHK4YQA.js → chunk-LZ6GFQAG.js} +334 -117
- package/dist/chunk-LZ6GFQAG.js.map +1 -0
- package/dist/{chunk-LAFNLLQP.js → chunk-MIVHK5YI.js} +5 -5
- package/dist/chunk-NEZZ6KDR.js +1217 -0
- package/dist/chunk-NEZZ6KDR.js.map +1 -0
- package/dist/{chunk-NTMMF5UD.js → chunk-OPNGPUKJ.js} +436 -254
- package/dist/chunk-OPNGPUKJ.js.map +1 -0
- package/dist/chunk-P3LRUNC6.js +16 -0
- package/dist/chunk-P3LRUNC6.js.map +1 -0
- package/dist/{chunk-FADTCQGM.js → chunk-PIVGJM6K.js} +94 -32
- package/dist/chunk-PIVGJM6K.js.map +1 -0
- package/dist/{chunk-73ATPIYX.js → chunk-PV4OX5TC.js} +36 -71
- package/dist/chunk-PV4OX5TC.js.map +1 -0
- package/dist/chunk-QCXPNSEL.js +141 -0
- package/dist/chunk-QCXPNSEL.js.map +1 -0
- package/dist/{chunk-XQUSIEKA.js → chunk-QXDNBYQF.js} +52 -14
- package/dist/chunk-QXDNBYQF.js.map +1 -0
- package/dist/chunk-SNTTOQAD.js +30 -0
- package/dist/chunk-SNTTOQAD.js.map +1 -0
- package/dist/{chunk-QCPGHBSZ.js → chunk-TM67EHSV.js} +63 -21
- package/dist/chunk-TM67EHSV.js.map +1 -0
- package/dist/chunk-TVN2JVOF.js +77 -0
- package/dist/chunk-TVN2JVOF.js.map +1 -0
- package/dist/chunk-TX5WIVYP.js +129 -0
- package/dist/chunk-TX5WIVYP.js.map +1 -0
- package/dist/{chunk-A4SLHOW7.js → chunk-UJ7XWK7J.js} +24 -18
- package/dist/chunk-UJ7XWK7J.js.map +1 -0
- package/dist/{chunk-EZATO3DW.js → chunk-UOCPC2Y2.js} +594 -17
- package/dist/chunk-UOCPC2Y2.js.map +1 -0
- package/dist/chunk-XEYH6ZZI.js +289 -0
- package/dist/chunk-XEYH6ZZI.js.map +1 -0
- package/dist/cli.js +264 -44
- package/dist/cli.js.map +1 -1
- package/dist/{config-PRJGWW7J.js → config-MEM2IWWN.js} +28 -5
- package/dist/contract-JH2WBIO6.js +8 -0
- package/dist/conventions-WV4NDXGH.js +143 -0
- package/dist/conventions-WV4NDXGH.js.map +1 -0
- package/dist/execute-NITIHHCK.js +31 -0
- package/dist/{generate-EGB4KV2B.js → generate-MVQI7A45.js} +34 -20
- package/dist/{generate-EGB4KV2B.js.map → generate-MVQI7A45.js.map} +1 -1
- package/dist/git-YRVRUN3C.js +31 -0
- package/dist/init-OKWEC3EV.js +21 -0
- package/dist/{operations-BYT626KQ.js → operations-LU6JA3BX.js} +24 -15
- package/dist/{operations-BYT626KQ.js.map → operations-LU6JA3BX.js.map} +1 -1
- package/dist/permissions-7NMDFU27.js +18 -0
- package/dist/plan-LEJJF3DE.js +31 -0
- package/dist/{policy-6S4GFC76.js → policy-JSYEK5B2.js} +17 -6
- package/dist/policy-JSYEK5B2.js.map +1 -0
- package/dist/pr-HPVUZ7S3.js +36 -0
- package/dist/pr-review-R2QZBT3F.js +29 -0
- package/dist/prd-5O6GYCC6.js +30 -0
- package/dist/ps-TNE44R5V.js +90 -0
- package/dist/ps-TNE44R5V.js.map +1 -0
- package/dist/recorder-XGF55WXI.js +31 -0
- package/dist/recorder-XGF55WXI.js.map +1 -0
- package/dist/registry-LGPE53QD.js +15 -0
- package/dist/registry-LGPE53QD.js.map +1 -0
- package/dist/{resume-PPCNQVVV.js → resume-QMEPMSZW.js} +51 -34
- package/dist/{resume-PPCNQVVV.js.map → resume-QMEPMSZW.js.map} +1 -1
- package/dist/review-YKWD6SNN.js +32 -0
- package/dist/review-YKWD6SNN.js.map +1 -0
- package/dist/routing-2ZUMZPJB.js +91 -0
- package/dist/routing-2ZUMZPJB.js.map +1 -0
- package/dist/run-YU4TR62R.js +50 -0
- package/dist/run-YU4TR62R.js.map +1 -0
- package/dist/{state-manager-2EDVDBAR.js → state-manager-X3PT3FGT.js} +4 -2
- package/dist/state-manager-X3PT3FGT.js.map +1 -0
- package/dist/usage-VCFLDA5Y.js +174 -0
- package/dist/usage-VCFLDA5Y.js.map +1 -0
- package/dist/{web-EF5WIIGG.js → web-XVHLX62B.js} +41 -30
- package/dist/web-XVHLX62B.js.map +1 -0
- package/package.json +6 -2
- package/prompts/execute.md +7 -8
- package/prompts/plan.md +4 -5
- package/prompts/pr.md +2 -2
- package/web/AGENTS.md +3 -3
- package/web/public/app.css +38 -0
- package/web/public/app.js +121 -11
- package/web/public/index.html +24 -0
- package/dist/analyze-OEET7KGE.js.map +0 -1
- package/dist/chunk-3QHK4YQA.js.map +0 -1
- package/dist/chunk-42PJFMAR.js +0 -605
- package/dist/chunk-42PJFMAR.js.map +0 -1
- package/dist/chunk-4JSOYE4G.js +0 -368
- package/dist/chunk-4JSOYE4G.js.map +0 -1
- package/dist/chunk-56KS3TFQ.js.map +0 -1
- package/dist/chunk-73ATPIYX.js.map +0 -1
- package/dist/chunk-A4SLHOW7.js.map +0 -1
- package/dist/chunk-BAUPQXPE.js +0 -173
- package/dist/chunk-BAUPQXPE.js.map +0 -1
- package/dist/chunk-BYZDPR7N.js.map +0 -1
- package/dist/chunk-CD5ULRVC.js +0 -615
- package/dist/chunk-CD5ULRVC.js.map +0 -1
- package/dist/chunk-DIDUVL6O.js +0 -1365
- package/dist/chunk-DIDUVL6O.js.map +0 -1
- package/dist/chunk-EZATO3DW.js.map +0 -1
- package/dist/chunk-FADTCQGM.js.map +0 -1
- package/dist/chunk-FJP3UQGD.js.map +0 -1
- package/dist/chunk-LWRSMTTZ.js +0 -18
- package/dist/chunk-LWRSMTTZ.js.map +0 -1
- package/dist/chunk-M5UCXTIT.js.map +0 -1
- package/dist/chunk-M66SJHQQ.js +0 -206
- package/dist/chunk-M66SJHQQ.js.map +0 -1
- package/dist/chunk-MJULIKQU.js.map +0 -1
- package/dist/chunk-NS6R37II.js.map +0 -1
- package/dist/chunk-NTMMF5UD.js.map +0 -1
- package/dist/chunk-QCPGHBSZ.js.map +0 -1
- package/dist/chunk-XQUSIEKA.js.map +0 -1
- package/dist/chunk-Y5ZDJ4PJ.js.map +0 -1
- package/dist/execute-XBNVL753.js +0 -18
- package/dist/init-AJHCXJBP.js +0 -10
- package/dist/plan-QOZ3RIO6.js +0 -21
- package/dist/policy-6S4GFC76.js.map +0 -1
- package/dist/pr-EBBHD2ZX.js +0 -26
- package/dist/pr-review-VVMW5PO4.js +0 -20
- package/dist/prd-JQSZF7PR.js +0 -21
- package/dist/review-3F6MARWH.js +0 -19
- package/dist/run-RXE5GDQO.js +0 -36
- package/dist/web-EF5WIIGG.js.map +0 -1
- /package/dist/{chunk-TSOLWK7Y.js.map → chunk-J62QSVP6.js.map} +0 -0
- /package/dist/{chunk-LAFNLLQP.js.map → chunk-MIVHK5YI.js.map} +0 -0
- /package/dist/{config-PRJGWW7J.js.map → config-MEM2IWWN.js.map} +0 -0
- /package/dist/{execute-XBNVL753.js.map → contract-JH2WBIO6.js.map} +0 -0
- /package/dist/{init-AJHCXJBP.js.map → execute-NITIHHCK.js.map} +0 -0
- /package/dist/{plan-QOZ3RIO6.js.map → git-YRVRUN3C.js.map} +0 -0
- /package/dist/{pr-EBBHD2ZX.js.map → init-OKWEC3EV.js.map} +0 -0
- /package/dist/{pr-review-VVMW5PO4.js.map → permissions-7NMDFU27.js.map} +0 -0
- /package/dist/{prd-JQSZF7PR.js.map → plan-LEJJF3DE.js.map} +0 -0
- /package/dist/{review-3F6MARWH.js.map → pr-HPVUZ7S3.js.map} +0 -0
- /package/dist/{run-RXE5GDQO.js.map → pr-review-R2QZBT3F.js.map} +0 -0
- /package/dist/{state-manager-2EDVDBAR.js.map → prd-5O6GYCC6.js.map} +0 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
summarizePhaseTiming
|
|
4
|
+
} from "./chunk-37N2RJW2.js";
|
|
5
|
+
import {
|
|
6
|
+
runAcceptance
|
|
7
|
+
} from "./chunk-HEF2RMKN.js";
|
|
8
|
+
import {
|
|
9
|
+
evaluateCeilings,
|
|
10
|
+
spendFromCost
|
|
11
|
+
} from "./chunk-ARFVHC2F.js";
|
|
12
|
+
import "./chunk-3XLCNVMD.js";
|
|
13
|
+
import "./chunk-QCXPNSEL.js";
|
|
14
|
+
import {
|
|
15
|
+
ensureHarnessVersion,
|
|
16
|
+
setIssuesCliOverrides
|
|
17
|
+
} from "./chunk-CNBYCMSU.js";
|
|
18
|
+
import {
|
|
19
|
+
resetStorageResolutionCache,
|
|
20
|
+
resolveIssuePaths
|
|
21
|
+
} from "./chunk-HQAHNIAR.js";
|
|
22
|
+
import {
|
|
23
|
+
createInitialSnapshot,
|
|
24
|
+
printError,
|
|
25
|
+
printInfo,
|
|
26
|
+
printWarning,
|
|
27
|
+
reduceSessionEvent
|
|
28
|
+
} from "./chunk-IYV6KNVF.js";
|
|
29
|
+
import {
|
|
30
|
+
redactSecrets
|
|
31
|
+
} from "./chunk-6FDFRNXZ.js";
|
|
32
|
+
import "./chunk-P3LRUNC6.js";
|
|
33
|
+
import {
|
|
34
|
+
loadTaskPlan
|
|
35
|
+
} from "./chunk-DIG5XKQQ.js";
|
|
36
|
+
import {
|
|
37
|
+
sessionSnapshotSchema
|
|
38
|
+
} from "./chunk-NEZZ6KDR.js";
|
|
39
|
+
import "./chunk-L6HSKTQE.js";
|
|
40
|
+
import "./chunk-TVN2JVOF.js";
|
|
41
|
+
import {
|
|
42
|
+
GLOBAL_ROOT_ENV,
|
|
43
|
+
ISSUES_DIR_NAME,
|
|
44
|
+
getGlobalRoot
|
|
45
|
+
} from "./chunk-TX5WIVYP.js";
|
|
46
|
+
import {
|
|
47
|
+
run
|
|
48
|
+
} from "./chunk-KQHB7ZT3.js";
|
|
49
|
+
import "./chunk-7Z4Y3VKM.js";
|
|
50
|
+
import "./chunk-XEYH6ZZI.js";
|
|
51
|
+
|
|
52
|
+
// src/commands/bench.ts
|
|
53
|
+
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
54
|
+
import { dirname as dirname2 } from "path";
|
|
55
|
+
import { createInterface } from "readline";
|
|
56
|
+
|
|
57
|
+
// src/benchmark/corpus.ts
|
|
58
|
+
var TASK_CLASSES = ["trivial", "small", "medium", "analysis"];
|
|
59
|
+
var CORPUS = [
|
|
60
|
+
{
|
|
61
|
+
id: "trivial",
|
|
62
|
+
title: "Change one constant and its comment",
|
|
63
|
+
fixture: "1\u20132 files, no test change",
|
|
64
|
+
verification: "none",
|
|
65
|
+
strategy: "direct"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "small",
|
|
69
|
+
title: "Fix a failing assertion in an existing test",
|
|
70
|
+
fixture: "correction against tests that already exist",
|
|
71
|
+
verification: "existing-tests",
|
|
72
|
+
strategy: "pipeline"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "medium",
|
|
76
|
+
title: "Add a function, tests and a README note",
|
|
77
|
+
fixture: "implementation plus tests plus documentation",
|
|
78
|
+
verification: "tests-and-docs",
|
|
79
|
+
strategy: "pipeline"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "analysis",
|
|
83
|
+
title: "Read the tree and write a short report",
|
|
84
|
+
fixture: "read-only artifact, no implementation",
|
|
85
|
+
verification: "artifact-only",
|
|
86
|
+
strategy: "direct"
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// src/benchmark/live.ts
|
|
91
|
+
function assertLiveBenchAllowed(env = process.env) {
|
|
92
|
+
if (env.VITEST && env.ISSUE_FLOW_E2E_BENCH !== "1") {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"issue-flow bench --mode real does not run under npm test. Set ISSUE_FLOW_E2E_BENCH=1 for a deliberate live campaign."
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function createLiveRepeatRunner(deps) {
|
|
99
|
+
return async (input) => {
|
|
100
|
+
assertLiveBenchAllowed();
|
|
101
|
+
const previousHome = process.env[GLOBAL_ROOT_ENV];
|
|
102
|
+
const previousCwd = process.cwd();
|
|
103
|
+
process.env[GLOBAL_ROOT_ENV] = input.campaignHome;
|
|
104
|
+
resetStorageResolutionCache();
|
|
105
|
+
setIssuesCliOverrides({ preferredProvider: "local" });
|
|
106
|
+
process.chdir(input.fixture.root);
|
|
107
|
+
const started = performance.now();
|
|
108
|
+
try {
|
|
109
|
+
await deps.runPipeline(input.fixture.issueRef);
|
|
110
|
+
const paths = await resolveIssuePaths(input.fixture.issueRef, {
|
|
111
|
+
projectRoot: input.fixture.root
|
|
112
|
+
});
|
|
113
|
+
const plan = await loadTaskPlan(paths.tasksFile);
|
|
114
|
+
const records = plan.executions ?? [];
|
|
115
|
+
const acceptance = await runAcceptance({
|
|
116
|
+
cwd: input.fixture.root,
|
|
117
|
+
issueDir: paths.issueDir,
|
|
118
|
+
declared: input.fixture.expectedVerification,
|
|
119
|
+
executionId: records.at(-1)?.id ?? null,
|
|
120
|
+
skipReviewer: input.arm !== "L2",
|
|
121
|
+
explicit: input.arm === "L2"
|
|
122
|
+
});
|
|
123
|
+
const wall = performance.now() - started;
|
|
124
|
+
const harness = records.reduce((sum, record) => sum + (record.durationMs ?? 0), 0);
|
|
125
|
+
const reported = records.flatMap(
|
|
126
|
+
(record) => record.cost.status === "reported" ? [record.cost.amount] : []
|
|
127
|
+
);
|
|
128
|
+
const cost = reported.length > 0 ? {
|
|
129
|
+
status: "reported",
|
|
130
|
+
amount: reported.reduce((sum, amount) => sum + amount, 0),
|
|
131
|
+
currency: "USD"
|
|
132
|
+
} : { status: "unknown", reason: "not_reported" };
|
|
133
|
+
return {
|
|
134
|
+
records,
|
|
135
|
+
verdict: acceptance.verdict,
|
|
136
|
+
taskDurationMs: wall,
|
|
137
|
+
harnessExecutionMs: harness,
|
|
138
|
+
orchestrationOverheadMs: Math.max(0, wall - harness),
|
|
139
|
+
attemptCount: records.reduce((max, record) => Math.max(max, record.attempt), 0) || 1,
|
|
140
|
+
cost
|
|
141
|
+
};
|
|
142
|
+
} finally {
|
|
143
|
+
process.chdir(previousCwd);
|
|
144
|
+
if (previousHome === void 0) delete process.env[GLOBAL_ROOT_ENV];
|
|
145
|
+
else process.env[GLOBAL_ROOT_ENV] = previousHome;
|
|
146
|
+
resetStorageResolutionCache();
|
|
147
|
+
setIssuesCliOverrides({});
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/benchmark/real.ts
|
|
153
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
154
|
+
import { join as join2 } from "path";
|
|
155
|
+
|
|
156
|
+
// src/benchmark/fixtures/materialize.ts
|
|
157
|
+
import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
|
|
158
|
+
import { tmpdir } from "os";
|
|
159
|
+
import { dirname, join } from "path";
|
|
160
|
+
|
|
161
|
+
// src/benchmark/fixtures/templates.ts
|
|
162
|
+
function withSeed(content, seed) {
|
|
163
|
+
return `${content}
|
|
164
|
+
// fixture-seed:${seed}
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
function templateFor(task, seed) {
|
|
168
|
+
switch (task) {
|
|
169
|
+
case "trivial":
|
|
170
|
+
return {
|
|
171
|
+
files: {
|
|
172
|
+
"src/answer.js": withSeed(
|
|
173
|
+
[
|
|
174
|
+
"/** The answer to life, the universe and everything. */",
|
|
175
|
+
"export const ANSWER = 41;"
|
|
176
|
+
].join("\n"),
|
|
177
|
+
seed
|
|
178
|
+
)
|
|
179
|
+
},
|
|
180
|
+
issueTitle: "Change one constant and its comment",
|
|
181
|
+
issueBody: [
|
|
182
|
+
"Change `ANSWER` in `src/answer.js` from 41 to 42.",
|
|
183
|
+
"Update the comment so it no longer claims the wrong value.",
|
|
184
|
+
"Do not add tests."
|
|
185
|
+
].join("\n"),
|
|
186
|
+
expectedVerification: []
|
|
187
|
+
};
|
|
188
|
+
case "small":
|
|
189
|
+
return {
|
|
190
|
+
files: {
|
|
191
|
+
"src/sum.js": withSeed("export function sum(a, b) {\n return a - b;\n}\n", seed),
|
|
192
|
+
"src/sum.test.js": [
|
|
193
|
+
"import { sum } from './sum.js';",
|
|
194
|
+
"import assert from 'node:assert/strict';",
|
|
195
|
+
"assert.equal(sum(2, 2), 4);",
|
|
196
|
+
""
|
|
197
|
+
].join("\n"),
|
|
198
|
+
"package.json": `${JSON.stringify({ name: "bench-small", type: "module", scripts: { test: "node src/sum.test.js" } }, null, 2)}
|
|
199
|
+
`
|
|
200
|
+
},
|
|
201
|
+
issueTitle: "Fix a failing assertion in an existing test",
|
|
202
|
+
issueBody: [
|
|
203
|
+
"`src/sum.test.js` fails because `sum` subtracts instead of adding.",
|
|
204
|
+
"Fix `src/sum.js` so `node src/sum.test.js` exits 0.",
|
|
205
|
+
"Do not change the test."
|
|
206
|
+
].join("\n"),
|
|
207
|
+
expectedVerification: [{ id: "existing-tests", run: "node src/sum.test.js", fatal: true }]
|
|
208
|
+
};
|
|
209
|
+
case "medium":
|
|
210
|
+
return {
|
|
211
|
+
files: {
|
|
212
|
+
"src/greet.js": withSeed("// greet(name) is not implemented yet.\n", seed),
|
|
213
|
+
"src/greet.test.js": [
|
|
214
|
+
"import { greet } from './greet.js';",
|
|
215
|
+
"import assert from 'node:assert/strict';",
|
|
216
|
+
"assert.equal(greet('Ada'), 'Hello, Ada');",
|
|
217
|
+
""
|
|
218
|
+
].join("\n"),
|
|
219
|
+
"README.md": ["# bench-medium", "", "A tiny library.", ""].join("\n"),
|
|
220
|
+
"package.json": `${JSON.stringify({ name: "bench-medium", type: "module", scripts: { test: "node src/greet.test.js" } }, null, 2)}
|
|
221
|
+
`
|
|
222
|
+
},
|
|
223
|
+
issueTitle: "Add a function, tests and a README note",
|
|
224
|
+
issueBody: [
|
|
225
|
+
"Implement `greet(name)` in `src/greet.js` so it returns Hello, <name>.",
|
|
226
|
+
"Keep `src/greet.test.js` as the contract.",
|
|
227
|
+
"Add an `## API` section to `README.md` documenting `greet`."
|
|
228
|
+
].join("\n"),
|
|
229
|
+
expectedVerification: [
|
|
230
|
+
{ id: "tests", run: "node src/greet.test.js", fatal: true },
|
|
231
|
+
{ id: "docs", expectFiles: ["README.md"], fatal: true }
|
|
232
|
+
]
|
|
233
|
+
};
|
|
234
|
+
case "analysis":
|
|
235
|
+
return {
|
|
236
|
+
files: {
|
|
237
|
+
"src/alpha.js": withSeed("export const ALPHA = 1;\n", seed),
|
|
238
|
+
"src/beta.js": withSeed(
|
|
239
|
+
"import { ALPHA } from './alpha.js';\nexport const BETA = ALPHA + 1;\n",
|
|
240
|
+
seed
|
|
241
|
+
),
|
|
242
|
+
"src/gamma.js": withSeed(
|
|
243
|
+
"import { BETA } from './beta.js';\nexport const GAMMA = BETA + 1;\n",
|
|
244
|
+
seed
|
|
245
|
+
)
|
|
246
|
+
},
|
|
247
|
+
issueTitle: "Read the tree and write a short report",
|
|
248
|
+
issueBody: [
|
|
249
|
+
"Read `src/` and write `report.md` describing the module graph.",
|
|
250
|
+
"Do not change the source files."
|
|
251
|
+
].join("\n"),
|
|
252
|
+
expectedVerification: [{ id: "artifact", expectFiles: ["report.md"], fatal: true }]
|
|
253
|
+
};
|
|
254
|
+
default: {
|
|
255
|
+
const _exhaustive = task;
|
|
256
|
+
throw new Error(`Unknown corpus class: ${_exhaustive}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/benchmark/fixtures/materialize.ts
|
|
262
|
+
var GIT_IDENT = ["-c", "user.name=issue-flow-bench", "-c", "user.email=bench@issue-flow.test"];
|
|
263
|
+
async function git(cwd, args) {
|
|
264
|
+
const result = await run("git", args, { cwd });
|
|
265
|
+
if (result.exitCode !== 0) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`git ${args.join(" ")} failed (${result.exitCode}): ${result.stderr || result.stdout}`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function writeTree(root, files) {
|
|
272
|
+
for (const [relative, content] of Object.entries(files)) {
|
|
273
|
+
const path = join(root, relative);
|
|
274
|
+
await mkdir(dirname(path), { recursive: true });
|
|
275
|
+
await writeFile(path, content, "utf-8");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function writeLocalIssue(root, title, body) {
|
|
279
|
+
const issueDir = join(root, ISSUES_DIR_NAME, "1");
|
|
280
|
+
await mkdir(issueDir, { recursive: true });
|
|
281
|
+
await writeFile(join(issueDir, "issue.md"), `# ${title}
|
|
282
|
+
|
|
283
|
+
${body.trim()}
|
|
284
|
+
`, "utf-8");
|
|
285
|
+
await writeFile(
|
|
286
|
+
join(issueDir, "metadata.json"),
|
|
287
|
+
`${JSON.stringify(
|
|
288
|
+
{
|
|
289
|
+
schemaVersion: 1,
|
|
290
|
+
id: "1",
|
|
291
|
+
number: 1,
|
|
292
|
+
source: "local",
|
|
293
|
+
title,
|
|
294
|
+
labels: ["bench"],
|
|
295
|
+
state: "open",
|
|
296
|
+
createdAt: "2026-08-30T00:00:00.000Z",
|
|
297
|
+
updatedAt: "2026-08-30T00:00:00.000Z",
|
|
298
|
+
contentHash: "sha256:fixture"
|
|
299
|
+
},
|
|
300
|
+
null,
|
|
301
|
+
2
|
|
302
|
+
)}
|
|
303
|
+
`,
|
|
304
|
+
"utf-8"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
async function materialize(task, seed) {
|
|
308
|
+
const root = await mkdtemp(join(tmpdir(), `issue-flow-bench-${task.id}-${seed}-`));
|
|
309
|
+
const template = templateFor(task.id, seed);
|
|
310
|
+
await writeTree(root, template.files);
|
|
311
|
+
await writeLocalIssue(root, template.issueTitle, template.issueBody);
|
|
312
|
+
await git(root, ["-c", "init.templateDir=", "init"]);
|
|
313
|
+
await git(root, ["add", "."]);
|
|
314
|
+
await git(root, [...GIT_IDENT, "commit", "-m", `fixture ${task.id} seed=${seed}`]);
|
|
315
|
+
return {
|
|
316
|
+
root,
|
|
317
|
+
issueRef: "1",
|
|
318
|
+
expectedVerification: template.expectedVerification,
|
|
319
|
+
seed,
|
|
320
|
+
task: task.id,
|
|
321
|
+
async dispose() {
|
|
322
|
+
await rm(root, { recursive: true, force: true });
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function corpusTask(id) {
|
|
327
|
+
const task = CORPUS.find((entry) => entry.id === id);
|
|
328
|
+
if (!task) throw new Error(`Unknown corpus class: ${id}`);
|
|
329
|
+
return task;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/benchmark/stats.ts
|
|
333
|
+
function percentile(values, p) {
|
|
334
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
335
|
+
if (sorted.length === 0) return 0;
|
|
336
|
+
const index = Math.min(sorted.length - 1, Math.ceil(p / 100 * sorted.length) - 1);
|
|
337
|
+
return sorted[index] ?? 0;
|
|
338
|
+
}
|
|
339
|
+
function p50(values) {
|
|
340
|
+
return percentile(values, 50);
|
|
341
|
+
}
|
|
342
|
+
function p95(values) {
|
|
343
|
+
return percentile(values, 95);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/benchmark/tuple.ts
|
|
347
|
+
var TUPLE_FIELDS = [
|
|
348
|
+
"task",
|
|
349
|
+
"harness",
|
|
350
|
+
"harnessVersion",
|
|
351
|
+
"model",
|
|
352
|
+
"modelVersion",
|
|
353
|
+
"effort",
|
|
354
|
+
"verification",
|
|
355
|
+
"strategy",
|
|
356
|
+
"settingSourcesPinned",
|
|
357
|
+
"strictMcpConfig",
|
|
358
|
+
"fallbackModelPassed"
|
|
359
|
+
];
|
|
360
|
+
var ComparabilityError = class extends Error {
|
|
361
|
+
field;
|
|
362
|
+
left;
|
|
363
|
+
right;
|
|
364
|
+
constructor(field, left, right) {
|
|
365
|
+
super(`ComparabilityTuple.${field} differs: ${String(left)} vs ${String(right)}`);
|
|
366
|
+
this.name = "ComparabilityError";
|
|
367
|
+
this.field = field;
|
|
368
|
+
this.left = left;
|
|
369
|
+
this.right = right;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
function assertComparable(left, right, options = {}) {
|
|
373
|
+
const ignored = new Set(options.ignore ?? []);
|
|
374
|
+
for (const field of TUPLE_FIELDS) {
|
|
375
|
+
if (ignored.has(field)) continue;
|
|
376
|
+
if (left[field] !== right[field]) {
|
|
377
|
+
throw new ComparabilityError(field, left[field], right[field]);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function isRowInvalid(tuple) {
|
|
382
|
+
return tuple.fallbackModelPassed || !tuple.settingSourcesPinned;
|
|
383
|
+
}
|
|
384
|
+
async function collectHarnessVersion(provider) {
|
|
385
|
+
const version = await ensureHarnessVersion(provider);
|
|
386
|
+
return version ?? "unknown";
|
|
387
|
+
}
|
|
388
|
+
function tupleAxisForArm(arm) {
|
|
389
|
+
if (arm === "strict-mcp") return "strictMcpConfig";
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/benchmark/real.ts
|
|
394
|
+
function campaignHome(campaignId, env) {
|
|
395
|
+
return join2(getGlobalRoot({ env }), "bench", campaignId);
|
|
396
|
+
}
|
|
397
|
+
function combineCost(records) {
|
|
398
|
+
let reported = 0;
|
|
399
|
+
let estimated = 0;
|
|
400
|
+
let unknown = 0;
|
|
401
|
+
for (const record of records) {
|
|
402
|
+
if (record.cost.status === "reported") reported += record.cost.amount;
|
|
403
|
+
else if (record.cost.status === "estimated") estimated += record.cost.amount;
|
|
404
|
+
else unknown += 1;
|
|
405
|
+
}
|
|
406
|
+
if (reported > 0) return { status: "reported", amount: reported, currency: "USD" };
|
|
407
|
+
if (estimated > 0) {
|
|
408
|
+
return {
|
|
409
|
+
status: "estimated",
|
|
410
|
+
amount: estimated,
|
|
411
|
+
currency: "USD",
|
|
412
|
+
pricing: {
|
|
413
|
+
tableVersion: "bench",
|
|
414
|
+
modelKey: "aggregated",
|
|
415
|
+
inputPerMillion: 0,
|
|
416
|
+
outputPerMillion: 0,
|
|
417
|
+
capturedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return { status: "unknown", reason: unknown > 0 ? "not_reported" : "not_reported" };
|
|
422
|
+
}
|
|
423
|
+
function summarizeCell(cell) {
|
|
424
|
+
const durations = cell.repeats.map((repeat) => repeat.taskDurationMs);
|
|
425
|
+
const harness = cell.repeats.map((repeat) => repeat.harnessExecutionMs);
|
|
426
|
+
const overhead = cell.repeats.map((repeat) => repeat.orchestrationOverheadMs);
|
|
427
|
+
const accepted = cell.repeats.map((repeat) => repeat.timeToAcceptedResultMs).filter((value) => value !== null);
|
|
428
|
+
const reported = cell.repeats.map((repeat) => repeat.cost.status === "reported" ? repeat.cost.amount : null).filter((value) => value !== null);
|
|
429
|
+
const verdicts = { passed: 0, failed: 0, unverified: 0 };
|
|
430
|
+
let unknownRepeats = 0;
|
|
431
|
+
let estimatedRepeats = 0;
|
|
432
|
+
for (const repeat of cell.repeats) {
|
|
433
|
+
verdicts[repeat.verdict] += 1;
|
|
434
|
+
if (repeat.cost.status === "unknown") unknownRepeats += 1;
|
|
435
|
+
if (repeat.cost.status === "estimated") estimatedRepeats += 1;
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
n: cell.repeats.length,
|
|
439
|
+
taskDurationMs: { p50: p50(durations), p95: p95(durations), n: durations.length },
|
|
440
|
+
harnessExecutionMs: { p50: p50(harness), p95: p95(harness), n: harness.length },
|
|
441
|
+
orchestrationOverheadMs: { p50: p50(overhead), p95: p95(overhead), n: overhead.length },
|
|
442
|
+
timeToAcceptedResultMs: {
|
|
443
|
+
p50: accepted.length === 0 ? null : p50(accepted),
|
|
444
|
+
p95: accepted.length === 0 ? null : p95(accepted),
|
|
445
|
+
n: accepted.length
|
|
446
|
+
},
|
|
447
|
+
cost: {
|
|
448
|
+
reportedP50: reported.length === 0 ? null : p50(reported),
|
|
449
|
+
reportedP95: reported.length === 0 ? null : p95(reported),
|
|
450
|
+
unknownRepeats,
|
|
451
|
+
estimatedRepeats
|
|
452
|
+
},
|
|
453
|
+
verdicts
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function tupleForArm(task, arm, base) {
|
|
457
|
+
return {
|
|
458
|
+
task,
|
|
459
|
+
harness: base.harness,
|
|
460
|
+
harnessVersion: base.harnessVersion,
|
|
461
|
+
model: base.model,
|
|
462
|
+
modelVersion: base.modelVersion,
|
|
463
|
+
effort: base.effort,
|
|
464
|
+
verification: base.verification,
|
|
465
|
+
strategy: base.strategy,
|
|
466
|
+
settingSourcesPinned: base.settingSourcesPinned,
|
|
467
|
+
strictMcpConfig: arm === "strict-mcp" ? true : base.strictMcpConfig ?? false,
|
|
468
|
+
fallbackModelPassed: base.fallbackModelPassed ?? false
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function declaredIgnore(arms) {
|
|
472
|
+
const fields = /* @__PURE__ */ new Set();
|
|
473
|
+
for (const arm of arms) {
|
|
474
|
+
const axis = tupleAxisForArm(arm);
|
|
475
|
+
if (axis) fields.add(axis);
|
|
476
|
+
}
|
|
477
|
+
return [...fields];
|
|
478
|
+
}
|
|
479
|
+
async function runRealCorpus(options) {
|
|
480
|
+
const tasks = options.tasks ?? CORPUS.map((entry) => entry.id);
|
|
481
|
+
const campaignId = options.campaignId ?? `campaign-${Date.now()}`;
|
|
482
|
+
const home = campaignHome(campaignId, options.env);
|
|
483
|
+
await mkdir2(home, { recursive: true });
|
|
484
|
+
const cells = /* @__PURE__ */ new Map();
|
|
485
|
+
const ignore = declaredIgnore(options.arms);
|
|
486
|
+
const now = options.now ?? Date.now;
|
|
487
|
+
const started = now();
|
|
488
|
+
let reportedUsd = 0;
|
|
489
|
+
let knownCost = "unknown";
|
|
490
|
+
let stop = { reason: "completed" };
|
|
491
|
+
const schedule = [];
|
|
492
|
+
for (let seed = 0; seed < options.repeats; seed += 1) {
|
|
493
|
+
for (const arm of options.arms) {
|
|
494
|
+
for (const task of tasks) {
|
|
495
|
+
schedule.push({ task, arm, seed });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
for (const step of schedule) {
|
|
500
|
+
const task = corpusTask(step.task);
|
|
501
|
+
const tuple = tupleForArm(step.task, step.arm, {
|
|
502
|
+
...options.tupleBase,
|
|
503
|
+
verification: task.verification,
|
|
504
|
+
strategy: task.strategy
|
|
505
|
+
});
|
|
506
|
+
const key = `${step.task}:${step.arm}`;
|
|
507
|
+
const existing = cells.get(key);
|
|
508
|
+
if (existing) {
|
|
509
|
+
assertComparable(existing.tuple, tuple, { ignore });
|
|
510
|
+
} else {
|
|
511
|
+
for (const cell of cells.values()) {
|
|
512
|
+
if (cell.task !== step.task) continue;
|
|
513
|
+
assertComparable(cell.tuple, tuple, { ignore });
|
|
514
|
+
}
|
|
515
|
+
cells.set(key, { task: step.task, tuple, arm: step.arm, repeats: [] });
|
|
516
|
+
}
|
|
517
|
+
const fixture = await materialize(task, step.seed);
|
|
518
|
+
try {
|
|
519
|
+
const outcome = await options.runner({
|
|
520
|
+
fixture,
|
|
521
|
+
arm: step.arm,
|
|
522
|
+
tuple,
|
|
523
|
+
campaignHome: home
|
|
524
|
+
});
|
|
525
|
+
const cost = outcome.cost.status !== void 0 ? outcome.cost : combineCost(outcome.records);
|
|
526
|
+
const spent = spendFromCost(cost);
|
|
527
|
+
if (spent.costStatus === "reported") {
|
|
528
|
+
reportedUsd += spent.reportedUsd;
|
|
529
|
+
knownCost = "reported";
|
|
530
|
+
}
|
|
531
|
+
const cell = cells.get(key);
|
|
532
|
+
if (!cell) throw new Error(`missing cell ${key}`);
|
|
533
|
+
cell.repeats.push({
|
|
534
|
+
seed: step.seed,
|
|
535
|
+
taskDurationMs: outcome.taskDurationMs,
|
|
536
|
+
harnessExecutionMs: outcome.harnessExecutionMs,
|
|
537
|
+
orchestrationOverheadMs: outcome.orchestrationOverheadMs,
|
|
538
|
+
timeToAcceptedResultMs: outcome.verdict === "passed" ? outcome.taskDurationMs : null,
|
|
539
|
+
verdict: outcome.verdict,
|
|
540
|
+
cost,
|
|
541
|
+
attemptCount: outcome.attemptCount,
|
|
542
|
+
executionIds: outcome.records.map((record) => record.id),
|
|
543
|
+
invalid: isRowInvalid(tuple)
|
|
544
|
+
});
|
|
545
|
+
} finally {
|
|
546
|
+
await fixture.dispose();
|
|
547
|
+
}
|
|
548
|
+
const ceiling = evaluateCeilings({
|
|
549
|
+
ceilings: {
|
|
550
|
+
maxCostUsdPerIssue: options.maxCostUsd ?? null,
|
|
551
|
+
maxDurationMsPerIssue: options.maxDurationMs ?? null,
|
|
552
|
+
maxExecutionsPerIssue: null,
|
|
553
|
+
onCeiling: "block"
|
|
554
|
+
},
|
|
555
|
+
spent: {
|
|
556
|
+
reportedUsd,
|
|
557
|
+
costStatus: knownCost,
|
|
558
|
+
durationMs: now() - started,
|
|
559
|
+
executions: [...cells.values()].reduce((sum, cell) => sum + cell.repeats.length, 0)
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
if (!ceiling.ok) {
|
|
563
|
+
stop = {
|
|
564
|
+
reason: ceiling.stopReason === "max_attempts" ? "max_duration" : ceiling.stopReason,
|
|
565
|
+
spent: ceiling.numbers.spent,
|
|
566
|
+
ceiling: ceiling.numbers.ceiling,
|
|
567
|
+
partial: true
|
|
568
|
+
};
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return { id: campaignId, home, cells: [...cells.values()], stop };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/benchmark/report.ts
|
|
576
|
+
import { hostname } from "os";
|
|
577
|
+
function formatMs(value) {
|
|
578
|
+
return value === null ? "\u2014" : `${Math.round(value)}`;
|
|
579
|
+
}
|
|
580
|
+
function formatUsd(value) {
|
|
581
|
+
return value === null ? "\u2014" : `$${value.toFixed(2)}`;
|
|
582
|
+
}
|
|
583
|
+
function tupleLine(tuple) {
|
|
584
|
+
return [
|
|
585
|
+
tuple.task,
|
|
586
|
+
tuple.harness,
|
|
587
|
+
tuple.harnessVersion,
|
|
588
|
+
tuple.model,
|
|
589
|
+
tuple.modelVersion ?? "\u2014",
|
|
590
|
+
tuple.effort,
|
|
591
|
+
tuple.verification,
|
|
592
|
+
tuple.strategy,
|
|
593
|
+
tuple.settingSourcesPinned ? "setting-sources:pinned" : "setting-sources:unpinned",
|
|
594
|
+
tuple.strictMcpConfig ? "strict-mcp" : "mcp:default",
|
|
595
|
+
tuple.fallbackModelPassed ? "INVALID:fallback-model" : "fallback:none"
|
|
596
|
+
].join(" \xD7 ");
|
|
597
|
+
}
|
|
598
|
+
function verdictCounts(summary) {
|
|
599
|
+
return `passed=${summary.verdicts.passed} failed=${summary.verdicts.failed} unverified=${summary.verdicts.unverified}`;
|
|
600
|
+
}
|
|
601
|
+
function scrubPaths(text) {
|
|
602
|
+
const home = getGlobalRoot();
|
|
603
|
+
return text.split(home).join("<issue-flow-home>");
|
|
604
|
+
}
|
|
605
|
+
function renderCampaignMarkdown(campaign, subject) {
|
|
606
|
+
const lines = [
|
|
607
|
+
`# Harness campaign \u2014 ${subject}`,
|
|
608
|
+
"",
|
|
609
|
+
`> **Research document.** Campaign \`${campaign.id}\`.`,
|
|
610
|
+
`> Machine: \`${hostname()}\`. Node ${process.version}.`,
|
|
611
|
+
campaign.stop.reason === "completed" ? "> Status: complete." : `> Status: **partial** \u2014 stopped on \`${campaign.stop.reason}\` (spent ${campaign.stop.spent}, ceiling ${campaign.stop.ceiling}). Results below are the repeats that finished; they are not a silently truncated table.`,
|
|
612
|
+
"",
|
|
613
|
+
"`time-to-accepted-result` is null when the verdict is not `passed`. Cost `unknown` is never summed as zero.",
|
|
614
|
+
"",
|
|
615
|
+
"## Cells",
|
|
616
|
+
"",
|
|
617
|
+
"| Task | Arm | n | duration p50/p95 (ms) | harness p50/p95 | overhead p50/p95 | accepted p50/p95 | cost p50/p95 | verdicts | invalid |",
|
|
618
|
+
"|---|---|---:|---|---|---|---|---|---|---|"
|
|
619
|
+
];
|
|
620
|
+
for (const cell of campaign.cells) {
|
|
621
|
+
const summary = summarizeCell(cell);
|
|
622
|
+
const invalid = cell.repeats.some((repeat) => repeat.invalid);
|
|
623
|
+
lines.push(
|
|
624
|
+
`| ${cell.task} | ${cell.arm} | ${summary.n} | ${formatMs(summary.taskDurationMs.p50)} / ${formatMs(summary.taskDurationMs.p95)} | ${formatMs(summary.harnessExecutionMs.p50)} / ${formatMs(summary.harnessExecutionMs.p95)} | ${formatMs(summary.orchestrationOverheadMs.p50)} / ${formatMs(summary.orchestrationOverheadMs.p95)} | ${formatMs(summary.timeToAcceptedResultMs.p50)} / ${formatMs(summary.timeToAcceptedResultMs.p95)} | ${formatUsd(summary.cost.reportedP50)} / ${formatUsd(summary.cost.reportedP95)} | ${verdictCounts(summary)} | ${invalid ? "yes" : "no"} |`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
lines.push("", "## Comparability tuples", "");
|
|
628
|
+
for (const cell of campaign.cells) {
|
|
629
|
+
lines.push(`- **${cell.task} / ${cell.arm}**: \`${tupleLine(cell.tuple)}\``);
|
|
630
|
+
}
|
|
631
|
+
lines.push("");
|
|
632
|
+
return redactSecrets(scrubPaths(lines.join("\n")));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/benchmark/synthetic.ts
|
|
636
|
+
function scriptedEvents() {
|
|
637
|
+
return [
|
|
638
|
+
{
|
|
639
|
+
type: "session:start",
|
|
640
|
+
at: "2026-08-30T03:00:00.000Z",
|
|
641
|
+
sessionId: "bench",
|
|
642
|
+
issueNumber: 79,
|
|
643
|
+
phases: ["prd", "execute"]
|
|
644
|
+
},
|
|
645
|
+
{ type: "phase:start", at: "2026-08-30T03:00:01.000Z", phase: "prd" },
|
|
646
|
+
{
|
|
647
|
+
type: "phase:end",
|
|
648
|
+
at: "2026-08-30T03:00:02.000Z",
|
|
649
|
+
phase: "prd",
|
|
650
|
+
success: true,
|
|
651
|
+
harnessExecutionMs: 800,
|
|
652
|
+
orchestrationOverheadMs: 200
|
|
653
|
+
},
|
|
654
|
+
{ type: "phase:start", at: "2026-08-30T03:00:02.000Z", phase: "execute" },
|
|
655
|
+
{
|
|
656
|
+
type: "phase:end",
|
|
657
|
+
at: "2026-08-30T03:00:03.000Z",
|
|
658
|
+
phase: "execute",
|
|
659
|
+
success: true,
|
|
660
|
+
harnessExecutionMs: 900,
|
|
661
|
+
orchestrationOverheadMs: 100,
|
|
662
|
+
attemptCount: 1
|
|
663
|
+
},
|
|
664
|
+
{ type: "session:end", at: "2026-08-30T03:00:03.000Z", status: "completed" }
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
function reduceScriptedRun() {
|
|
668
|
+
let snap = createInitialSnapshot();
|
|
669
|
+
for (const event of scriptedEvents()) {
|
|
670
|
+
snap = reduceSessionEvent(snap, event);
|
|
671
|
+
}
|
|
672
|
+
return snap;
|
|
673
|
+
}
|
|
674
|
+
function mockedRecord(durationMs) {
|
|
675
|
+
return {
|
|
676
|
+
id: "synthetic",
|
|
677
|
+
sessionId: "bench",
|
|
678
|
+
purpose: "execute",
|
|
679
|
+
attempt: 1,
|
|
680
|
+
trigger: "initial",
|
|
681
|
+
triggerReason: null,
|
|
682
|
+
agent: {
|
|
683
|
+
harness: "mocked",
|
|
684
|
+
provider: null,
|
|
685
|
+
model: { requested: null, resolved: null, source: "unavailable" },
|
|
686
|
+
providerSessionId: null
|
|
687
|
+
},
|
|
688
|
+
startedAt: "2026-08-30T03:00:00.000Z",
|
|
689
|
+
finishedAt: "2026-08-30T03:00:01.000Z",
|
|
690
|
+
durationMs,
|
|
691
|
+
cliDurationMs: Math.max(0, durationMs - 5),
|
|
692
|
+
harnessStartupMs: 5,
|
|
693
|
+
ttftMs: null,
|
|
694
|
+
usage: { source: "unavailable" },
|
|
695
|
+
cost: { status: "unknown", reason: "not_reported" },
|
|
696
|
+
status: "completed",
|
|
697
|
+
failure: null
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function runSyntheticCorpus() {
|
|
701
|
+
return CORPUS.map((task) => {
|
|
702
|
+
const harnessMs = 1e3;
|
|
703
|
+
const orchestrateStart = performance.now();
|
|
704
|
+
const snap = reduceScriptedRun();
|
|
705
|
+
sessionSnapshotSchema.parse(snap);
|
|
706
|
+
const orchestrationMs = performance.now() - orchestrateStart;
|
|
707
|
+
const timing = summarizePhaseTiming(
|
|
708
|
+
[mockedRecord(harnessMs)],
|
|
709
|
+
"execute",
|
|
710
|
+
harnessMs + orchestrationMs
|
|
711
|
+
);
|
|
712
|
+
return {
|
|
713
|
+
task: task.id,
|
|
714
|
+
mode: "synthetic",
|
|
715
|
+
harness: "mocked",
|
|
716
|
+
model: "none",
|
|
717
|
+
effort: "none",
|
|
718
|
+
verification: task.verification,
|
|
719
|
+
strategy: task.strategy,
|
|
720
|
+
taskDurationMs: harnessMs + orchestrationMs,
|
|
721
|
+
harnessExecutionDurationMs: timing.harnessExecutionMs ?? harnessMs,
|
|
722
|
+
orchestrationOverheadMs: timing.orchestrationOverheadMs ?? orchestrationMs,
|
|
723
|
+
timeToFirstOutputMs: timing.ttftMs,
|
|
724
|
+
attemptCount: timing.attemptCount ?? 1,
|
|
725
|
+
retryDurationMs: timing.retryDurationMs ?? 0,
|
|
726
|
+
// no retries in the mocked record
|
|
727
|
+
verdict: "unverified"
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/commands/bench.ts
|
|
733
|
+
var BASELINE_COST_P50_USD = {
|
|
734
|
+
trivial: 0.2,
|
|
735
|
+
small: 3.17,
|
|
736
|
+
medium: 6.08,
|
|
737
|
+
analysis: 1.86
|
|
738
|
+
};
|
|
739
|
+
var BenchConfirmationError = class extends Error {
|
|
740
|
+
exitCode = 1;
|
|
741
|
+
constructor(message) {
|
|
742
|
+
super(message);
|
|
743
|
+
this.name = "BenchConfirmationError";
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
function estimateCampaignUsd(tasks, arms, repeats) {
|
|
747
|
+
const perRepeat = tasks.reduce((sum, task) => sum + BASELINE_COST_P50_USD[task], 0);
|
|
748
|
+
return perRepeat * arms * repeats;
|
|
749
|
+
}
|
|
750
|
+
function isInteractiveByDefault() {
|
|
751
|
+
const ci = process.env.CI;
|
|
752
|
+
const inCi = ci !== void 0 && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
|
|
753
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !inCi;
|
|
754
|
+
}
|
|
755
|
+
async function confirmRealCampaign(estimate, options = {}) {
|
|
756
|
+
printInfo(
|
|
757
|
+
`Real campaign: ${estimate.cells} cells \xD7 ${estimate.repeats} repeats \u2248 $${estimate.usd.toFixed(2)} (p50 of the #79 baseline).`
|
|
758
|
+
);
|
|
759
|
+
if (estimate.maxCost !== void 0) {
|
|
760
|
+
printInfo(`Cost ceiling: $${estimate.maxCost.toFixed(2)}.`);
|
|
761
|
+
}
|
|
762
|
+
if (estimate.maxDuration !== void 0) {
|
|
763
|
+
printInfo(`Duration ceiling: ${estimate.maxDuration}ms.`);
|
|
764
|
+
}
|
|
765
|
+
if (options.yes === true) {
|
|
766
|
+
printInfo("--yes: starting the campaign.");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const interactive = options.interactive ?? isInteractiveByDefault();
|
|
770
|
+
if (!interactive) {
|
|
771
|
+
throw new BenchConfirmationError(
|
|
772
|
+
"A real campaign spends money and the terminal is not interactive. Re-run with --yes."
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const stdin = options.stdin ?? process.stdin;
|
|
776
|
+
const stdout = options.stdout ?? process.stdout;
|
|
777
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
778
|
+
try {
|
|
779
|
+
const answer = await new Promise((resolve) => {
|
|
780
|
+
rl.question("Proceed with a paid campaign? [y/N] ", resolve);
|
|
781
|
+
});
|
|
782
|
+
if (!/^\s*y(es)?\s*$/i.test(answer)) {
|
|
783
|
+
throw new BenchConfirmationError("Cancelled: the campaign was not confirmed.");
|
|
784
|
+
}
|
|
785
|
+
} finally {
|
|
786
|
+
rl.close();
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function parseTasks(values) {
|
|
790
|
+
if (values === void 0 || values.length === 0) return [...TASK_CLASSES];
|
|
791
|
+
const tasks = [];
|
|
792
|
+
for (const value of values) {
|
|
793
|
+
if (!TASK_CLASSES.includes(value)) {
|
|
794
|
+
throw new BenchConfirmationError(
|
|
795
|
+
`Unknown task class '${value}'. Must be one of: ${TASK_CLASSES.join(", ")}.`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
if (!tasks.includes(value)) tasks.push(value);
|
|
799
|
+
}
|
|
800
|
+
return tasks;
|
|
801
|
+
}
|
|
802
|
+
function printSynthetic() {
|
|
803
|
+
const rows = runSyntheticCorpus();
|
|
804
|
+
printInfo("Synthetic corpus (orchestration only; harness is a number).");
|
|
805
|
+
for (const row of rows) {
|
|
806
|
+
printInfo(
|
|
807
|
+
`${row.task} duration=${row.taskDurationMs.toFixed(1)}ms overhead=${row.orchestrationOverheadMs.toFixed(1)}ms verdict=${row.verdict}`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async function runBench(options = {}) {
|
|
812
|
+
const mode = options.mode ?? "synthetic";
|
|
813
|
+
if (mode !== "synthetic" && mode !== "real") {
|
|
814
|
+
printError(`Unknown bench mode '${mode}'. Use synthetic or real.`);
|
|
815
|
+
return 1;
|
|
816
|
+
}
|
|
817
|
+
if (mode === "synthetic") {
|
|
818
|
+
printSynthetic();
|
|
819
|
+
return 0;
|
|
820
|
+
}
|
|
821
|
+
if (options.repo !== void 0) {
|
|
822
|
+
printWarning(
|
|
823
|
+
"--repo is an investigation escape: the row is not publishable. A disposable fixture is the only source of a comparable line."
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
let tasks;
|
|
827
|
+
try {
|
|
828
|
+
tasks = parseTasks(options.task);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
831
|
+
return 1;
|
|
832
|
+
}
|
|
833
|
+
const arms = options.arm !== void 0 && options.arm.length > 0 ? options.arm : ["baseline"];
|
|
834
|
+
const repeats = options.repeats ?? 5;
|
|
835
|
+
const usd = estimateCampaignUsd(tasks, arms.length, repeats);
|
|
836
|
+
try {
|
|
837
|
+
await confirmRealCampaign(
|
|
838
|
+
{
|
|
839
|
+
cells: tasks.length * arms.length,
|
|
840
|
+
repeats,
|
|
841
|
+
usd,
|
|
842
|
+
...options.maxCost === void 0 ? {} : { maxCost: options.maxCost },
|
|
843
|
+
...options.maxDuration === void 0 ? {} : { maxDuration: options.maxDuration }
|
|
844
|
+
},
|
|
845
|
+
options
|
|
846
|
+
);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
if (error instanceof BenchConfirmationError) {
|
|
849
|
+
printError(error.message);
|
|
850
|
+
return error.exitCode;
|
|
851
|
+
}
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
const harnessVersion = options.harnessVersion ?? await collectHarnessVersion("claude");
|
|
855
|
+
const runner = options.runner ?? createLiveRepeatRunner({
|
|
856
|
+
runPipeline: options.runPipeline ?? (async (issue) => {
|
|
857
|
+
const { runPipeline } = await import("./run-YU4TR62R.js");
|
|
858
|
+
return runPipeline(issue, "auto", void 0, true, false, { yes: true, only: true });
|
|
859
|
+
})
|
|
860
|
+
});
|
|
861
|
+
const campaign = await runRealCorpus({
|
|
862
|
+
tasks,
|
|
863
|
+
arms,
|
|
864
|
+
repeats,
|
|
865
|
+
tupleBase: {
|
|
866
|
+
harness: "claude",
|
|
867
|
+
harnessVersion,
|
|
868
|
+
model: "default",
|
|
869
|
+
modelVersion: null,
|
|
870
|
+
effort: "default",
|
|
871
|
+
verification: "none",
|
|
872
|
+
strategy: "pipeline",
|
|
873
|
+
settingSourcesPinned: true,
|
|
874
|
+
fallbackModelPassed: false
|
|
875
|
+
},
|
|
876
|
+
...options.maxCost === void 0 ? {} : { maxCostUsd: options.maxCost },
|
|
877
|
+
...options.maxDuration === void 0 ? {} : { maxDurationMs: options.maxDuration },
|
|
878
|
+
runner
|
|
879
|
+
});
|
|
880
|
+
if (campaign.stop.reason !== "completed") {
|
|
881
|
+
printWarning(
|
|
882
|
+
`Campaign stopped on ${campaign.stop.reason} (spent ${campaign.stop.spent}, ceiling ${campaign.stop.ceiling}). Report is partial.`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const markdown = renderCampaignMarkdown(campaign, arms.join("-vs-"));
|
|
886
|
+
if (options.out) {
|
|
887
|
+
await mkdir3(dirname2(options.out), { recursive: true });
|
|
888
|
+
await writeFile2(options.out, markdown, "utf-8");
|
|
889
|
+
printInfo(`Wrote ${options.out}`);
|
|
890
|
+
} else {
|
|
891
|
+
(options.stdout ?? process.stdout).write(markdown);
|
|
892
|
+
}
|
|
893
|
+
if (options.json === true) {
|
|
894
|
+
(options.stdout ?? process.stdout).write(`${JSON.stringify(campaign, null, 2)}
|
|
895
|
+
`);
|
|
896
|
+
}
|
|
897
|
+
return campaign.stop.reason === "completed" ? 0 : 2;
|
|
898
|
+
}
|
|
899
|
+
export {
|
|
900
|
+
BASELINE_COST_P50_USD,
|
|
901
|
+
BenchConfirmationError,
|
|
902
|
+
confirmRealCampaign,
|
|
903
|
+
estimateCampaignUsd,
|
|
904
|
+
runBench
|
|
905
|
+
};
|
|
906
|
+
//# sourceMappingURL=bench-SA3BOME5.js.map
|