@tea-agent/loop-agent 0.16.1-beta.2 → 0.16.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.
- package/AGENTS.md +4 -8
- package/CHANGELOG.md +55 -18
- package/README.md +76 -299
- package/dist/application/evaluation/alias.js +184 -0
- package/dist/application/evaluation/budget.js +192 -0
- package/dist/application/evaluation/campaign-hash.js +47 -0
- package/dist/application/evaluation/campaign-matrix.js +372 -0
- package/dist/application/evaluation/campaign-scorecard.js +135 -0
- package/dist/application/evaluation/campaign.js +370 -0
- package/dist/application/evaluation/candidate.js +23 -6
- package/dist/application/evaluation/corpus-hash.js +38 -0
- package/dist/application/evaluation/corpus.js +56 -0
- package/dist/application/evaluation/experiment.js +294 -0
- package/dist/application/evaluation/ignition.js +198 -0
- package/dist/application/evaluation/integrity-audit.js +162 -0
- package/dist/application/evaluation/outer-loop.js +132 -0
- package/dist/application/evaluation/pi-cell-executor.js +39 -0
- package/dist/application/evaluation/private-verifier.js +46 -0
- package/dist/application/evaluation/promotion-policy.js +151 -0
- package/dist/application/evaluation/proposer.js +98 -0
- package/dist/application/evaluation/types.js +522 -0
- package/dist/cli/command-definitions.js +19 -3
- package/dist/commands/dag-reconcile-run.js +3 -116
- package/dist/commands/eval.js +1176 -13
- package/dist/commands/init.js +7 -1
- package/dist/executors/dag-pi-executor.js +4 -44
- package/dist/executors/pi-sdk-executor.js +3 -3
- package/dist/executors/shell-executor.js +1 -1
- package/dist/infrastructure/evaluation/alias-store.js +199 -0
- package/dist/infrastructure/evaluation/campaign-store.js +154 -0
- package/dist/infrastructure/evaluation/corpus-store.js +181 -0
- package/dist/infrastructure/evaluation/experiment-store.js +124 -0
- package/dist/infrastructure/evaluation/ignition-store.js +82 -0
- package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
- package/dist/infrastructure/evaluation/proposer-store.js +78 -0
- package/dist/records/promotion.js +3 -1
- package/dist/worker/cli.js +83 -0
- package/dist/worker/delivery/git-transaction.js +75 -0
- package/dist/worker/delivery/verification-bundle.js +13 -2
- package/dist/worker/feature/review.js +3 -2
- package/dist/worker/observe/static/dag-helpers.js +0 -62
- package/dist/worker/observe/static/styles.css +18 -55
- package/dist/worker/observe/static/views/dag.js +13 -5
- package/dist/worker/outcomes/adapters.js +4 -1
- package/dist/worker/outcomes/declared-artifacts.js +103 -0
- package/dist/worker/outcomes/evidence-tokens.js +29 -0
- package/dist/worker/outcomes/gate.js +10 -11
- package/dist/worker/outcomes/projector.js +30 -4
- package/dist/worker/outcomes/types.js +3 -0
- package/dist/worker/pool/reconcile.js +285 -0
- package/dist/worker/run-task/run-task.js +81 -4
- package/dist/worker/runner/run-ready.js +25 -2
- package/dist/worker/task-graph/ready-planner.js +14 -8
- package/dist/worker/task-graph/task-graph-schema.js +5 -3
- package/dist/workflows/dag/budget-enforcement.js +67 -0
- package/dist/workflows/dag/context-policy.js +137 -0
- package/dist/workflows/dag/failure-routing.js +7 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +0 -77
- package/dist/workflows/dag/init-hybrid.js +33 -53
- package/dist/workflows/dag/knowledge-curator.js +3 -0
- package/dist/workflows/dag/node-execution.js +11 -4
- package/dist/workflows/dag/prompt.js +1 -1
- package/dist/workflows/dag/reconcile-run.js +121 -0
- package/dist/workflows/dag/report.js +12 -0
- package/dist/workflows/dag/runner.js +43 -16
- package/dist/workflows/dag/skill-snapshot.js +11 -7
- package/dist/workflows/dag/types.js +18 -1
- package/dist/workflows/dag/validate.js +15 -1
- package/docs/README.md +3 -1
- package/docs/architecture/runtime-boundaries.md +3 -2
- package/docs/init-surface.manifest.json +4 -0
- package/docs/local-development-environment.md +52 -0
- package/docs/templates/agent-dag.schema.json +0 -5
- package/docs/templates/agent-dag.supervised-implementation.json +23 -4
- package/docs/templates/branch-merge-report.md +14 -0
- package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
- package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
- package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
- package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
- package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
- package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
- package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
- package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
- package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
- package/docs/templates/product-line/AGENTS.md +1 -0
- package/docs/templates/product-line/README.md +17 -0
- package/docs/templates/product-line/acceptance.yaml +9 -0
- package/docs/templates/product-line/feature.yaml +11 -0
- package/docs/templates/product-line/task-graph.yaml +8 -0
- package/docs/templates/product-line/task.yaml +4 -0
- package/package.json +2 -1
- package/skills/frontend-implementation/references/node-contracts.md +3 -3
- package/skills/loop-agent/references/command-reference.md +5 -0
- package/skills/loop-agent/references/hybrid-dag.md +3 -3
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { campaignDir, readCampaignManifest, readCampaignPlan, writeCampaignScorecard, } from "../../infrastructure/evaluation/campaign-store.js";
|
|
4
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
|
+
import { planCampaign, runCampaignPrivateChecks, } from "./campaign.js";
|
|
6
|
+
import { evaluatePromotionPolicy } from "./promotion-policy.js";
|
|
7
|
+
import { matrixFixtureSchema, matrixRunSchema } from "./types.js";
|
|
8
|
+
function matrixJobsDir(repoRoot, campaignId) {
|
|
9
|
+
return path.join(campaignDir(repoRoot, campaignId), "matrix-jobs");
|
|
10
|
+
}
|
|
11
|
+
function matrixRunPath(repoRoot, campaignId) {
|
|
12
|
+
return path.join(campaignDir(repoRoot, campaignId), "matrix-run.json");
|
|
13
|
+
}
|
|
14
|
+
function evidenceDraftPath(repoRoot, campaignId) {
|
|
15
|
+
return path.join(campaignDir(repoRoot, campaignId), "evidence-draft.json");
|
|
16
|
+
}
|
|
17
|
+
export async function loadMatrixFixture(input) {
|
|
18
|
+
const absolute = path.resolve(input.repoRoot, input.fixturePath);
|
|
19
|
+
return matrixFixtureSchema.parse(JSON.parse(await readFile(absolute, "utf-8")));
|
|
20
|
+
}
|
|
21
|
+
function resolveStubResult(fixture, cell) {
|
|
22
|
+
for (const row of fixture.results) {
|
|
23
|
+
if (row.cellId && row.cellId === cell.cellId) {
|
|
24
|
+
return { verifyPassed: row.verifyPassed, runId: row.runId };
|
|
25
|
+
}
|
|
26
|
+
const match = (!row.candidateId || row.candidateId === cell.candidateId) &&
|
|
27
|
+
(!row.taskRef || row.taskRef === cell.taskRef) &&
|
|
28
|
+
(row.seed === undefined || row.seed === cell.seed) &&
|
|
29
|
+
(!row.split || row.split === cell.split);
|
|
30
|
+
if (match &&
|
|
31
|
+
(row.candidateId || row.taskRef || row.seed !== undefined || row.split)) {
|
|
32
|
+
return { verifyPassed: row.verifyPassed, runId: row.runId };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { verifyPassed: fixture.defaultVerifyPassed };
|
|
36
|
+
}
|
|
37
|
+
export function buildMatrixJobSpec(input) {
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
cellId: input.cell.cellId,
|
|
41
|
+
evaluation: {
|
|
42
|
+
campaignId: input.campaignId,
|
|
43
|
+
candidateId: input.cell.candidateId,
|
|
44
|
+
bundleHash: input.cell.bundleHash,
|
|
45
|
+
seed: input.cell.seed,
|
|
46
|
+
split: input.cell.split,
|
|
47
|
+
taskRef: input.cell.taskRef,
|
|
48
|
+
},
|
|
49
|
+
executor: "pi",
|
|
50
|
+
autoPromote: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function writeMatrixJob(input) {
|
|
54
|
+
const dir = matrixJobsDir(input.repoRoot, input.campaignId);
|
|
55
|
+
await mkdir(dir, { recursive: true });
|
|
56
|
+
const job = buildMatrixJobSpec({
|
|
57
|
+
cell: input.cell,
|
|
58
|
+
campaignId: input.campaignId,
|
|
59
|
+
});
|
|
60
|
+
const jobPath = path.join(dir, `${input.cell.cellId}.json`);
|
|
61
|
+
await writeJsonAtomic(jobPath, job, { repoRoot: input.repoRoot });
|
|
62
|
+
return {
|
|
63
|
+
jobPath: path.relative(input.repoRoot, jobPath).replace(/\\/g, "/"),
|
|
64
|
+
evaluation: job.evaluation,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Candidate matrix runner (M7 companion).
|
|
69
|
+
* Modes:
|
|
70
|
+
* - dry-run: plan public cells only
|
|
71
|
+
* - stub: resolve verifyPassed from fixture (tests / offline)
|
|
72
|
+
* - pi-plan: materialize per-cell Pi evaluation job specs (no spawn)
|
|
73
|
+
* - pi: require injected PiCellExecutor; never auto-promote
|
|
74
|
+
*/
|
|
75
|
+
export async function runCampaignMatrix(input) {
|
|
76
|
+
const planned = await planCampaign({
|
|
77
|
+
repoRoot: input.repoRoot,
|
|
78
|
+
campaignId: input.campaignId,
|
|
79
|
+
});
|
|
80
|
+
const publicCells = planned.plan.cells.filter((cell) => cell.executionRole === "candidate_run");
|
|
81
|
+
const limited = input.maxPublicCells !== undefined
|
|
82
|
+
? publicCells.slice(0, input.maxPublicCells)
|
|
83
|
+
: publicCells;
|
|
84
|
+
let fixture;
|
|
85
|
+
if (input.mode === "stub") {
|
|
86
|
+
if (!input.fixturePath) {
|
|
87
|
+
throw new Error("matrix mode=stub requires --fixture <path>");
|
|
88
|
+
}
|
|
89
|
+
fixture = await loadMatrixFixture({
|
|
90
|
+
repoRoot: input.repoRoot,
|
|
91
|
+
fixturePath: input.fixturePath,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (input.mode === "pi" && !input.piExecutor) {
|
|
95
|
+
throw new Error("matrix mode=pi requires an injected PiCellExecutor (CLI refuses live Pi by default; use pi-plan or stub)");
|
|
96
|
+
}
|
|
97
|
+
const cellResults = [];
|
|
98
|
+
for (const cell of limited) {
|
|
99
|
+
if (input.mode === "dry-run") {
|
|
100
|
+
cellResults.push({
|
|
101
|
+
cellId: cell.cellId,
|
|
102
|
+
candidateId: cell.candidateId,
|
|
103
|
+
taskRef: cell.taskRef,
|
|
104
|
+
seed: cell.seed,
|
|
105
|
+
split: cell.split,
|
|
106
|
+
executionRole: cell.executionRole,
|
|
107
|
+
mode: "dry-run",
|
|
108
|
+
skipped: true,
|
|
109
|
+
detail: "dry-run: would execute candidate_run cell",
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (input.mode === "stub") {
|
|
114
|
+
const resolved = resolveStubResult(fixture, cell);
|
|
115
|
+
cellResults.push({
|
|
116
|
+
cellId: cell.cellId,
|
|
117
|
+
candidateId: cell.candidateId,
|
|
118
|
+
taskRef: cell.taskRef,
|
|
119
|
+
seed: cell.seed,
|
|
120
|
+
split: cell.split,
|
|
121
|
+
executionRole: cell.executionRole,
|
|
122
|
+
mode: "stub",
|
|
123
|
+
verifyPassed: resolved.verifyPassed,
|
|
124
|
+
runId: resolved.runId,
|
|
125
|
+
skipped: false,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const job = await writeMatrixJob({
|
|
130
|
+
repoRoot: input.repoRoot,
|
|
131
|
+
campaignId: input.campaignId,
|
|
132
|
+
cell,
|
|
133
|
+
});
|
|
134
|
+
if (input.mode === "pi-plan") {
|
|
135
|
+
cellResults.push({
|
|
136
|
+
cellId: cell.cellId,
|
|
137
|
+
candidateId: cell.candidateId,
|
|
138
|
+
taskRef: cell.taskRef,
|
|
139
|
+
seed: cell.seed,
|
|
140
|
+
split: cell.split,
|
|
141
|
+
executionRole: cell.executionRole,
|
|
142
|
+
mode: "pi-plan",
|
|
143
|
+
jobPath: job.jobPath,
|
|
144
|
+
skipped: true,
|
|
145
|
+
detail: "pi-plan: job materialized; Pi not spawned",
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const executed = await input.piExecutor({
|
|
150
|
+
repoRoot: input.repoRoot,
|
|
151
|
+
cell,
|
|
152
|
+
jobPath: job.jobPath,
|
|
153
|
+
evaluation: job.evaluation,
|
|
154
|
+
});
|
|
155
|
+
cellResults.push({
|
|
156
|
+
cellId: cell.cellId,
|
|
157
|
+
candidateId: cell.candidateId,
|
|
158
|
+
taskRef: cell.taskRef,
|
|
159
|
+
seed: cell.seed,
|
|
160
|
+
split: cell.split,
|
|
161
|
+
executionRole: cell.executionRole,
|
|
162
|
+
mode: "pi",
|
|
163
|
+
verifyPassed: executed.verifyPassed,
|
|
164
|
+
runId: executed.runId,
|
|
165
|
+
jobPath: job.jobPath,
|
|
166
|
+
detail: executed.detail,
|
|
167
|
+
skipped: false,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Mark held_out / remaining public as skipped for transparency
|
|
171
|
+
for (const cell of planned.plan.cells) {
|
|
172
|
+
if (cell.executionRole === "candidate_run") {
|
|
173
|
+
if (limited.some((row) => row.cellId === cell.cellId))
|
|
174
|
+
continue;
|
|
175
|
+
cellResults.push({
|
|
176
|
+
cellId: cell.cellId,
|
|
177
|
+
candidateId: cell.candidateId,
|
|
178
|
+
taskRef: cell.taskRef,
|
|
179
|
+
seed: cell.seed,
|
|
180
|
+
split: cell.split,
|
|
181
|
+
executionRole: cell.executionRole,
|
|
182
|
+
mode: input.mode,
|
|
183
|
+
skipped: true,
|
|
184
|
+
detail: "skipped by maxPublicCells",
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (cell.executionRole === "promotion_gate_only") {
|
|
189
|
+
cellResults.push({
|
|
190
|
+
cellId: cell.cellId,
|
|
191
|
+
candidateId: cell.candidateId,
|
|
192
|
+
taskRef: cell.taskRef,
|
|
193
|
+
seed: cell.seed,
|
|
194
|
+
split: cell.split,
|
|
195
|
+
executionRole: cell.executionRole,
|
|
196
|
+
mode: input.mode,
|
|
197
|
+
skipped: true,
|
|
198
|
+
detail: "held_out remains promotion_gate_only (not executed in matrix)",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
let privateResults = [];
|
|
203
|
+
if (input.includePrivateChecks !== false && input.mode !== "dry-run") {
|
|
204
|
+
const privateRun = await runCampaignPrivateChecks({
|
|
205
|
+
repoRoot: input.repoRoot,
|
|
206
|
+
campaignId: input.campaignId,
|
|
207
|
+
});
|
|
208
|
+
privateResults = privateRun.results;
|
|
209
|
+
for (const cell of planned.plan.cells) {
|
|
210
|
+
if (cell.executionRole !== "private_verify")
|
|
211
|
+
continue;
|
|
212
|
+
const related = privateResults.filter((row) => row.cellId === cell.cellId);
|
|
213
|
+
const ok = related.length > 0 && related.every((row) => row.ok);
|
|
214
|
+
cellResults.push({
|
|
215
|
+
cellId: cell.cellId,
|
|
216
|
+
candidateId: cell.candidateId,
|
|
217
|
+
taskRef: cell.taskRef,
|
|
218
|
+
seed: cell.seed,
|
|
219
|
+
split: cell.split,
|
|
220
|
+
executionRole: "private_verify",
|
|
221
|
+
mode: input.mode,
|
|
222
|
+
verifyPassed: ok,
|
|
223
|
+
skipped: false,
|
|
224
|
+
detail: `private checks=${related.length}`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const now = input.now ?? new Date().toISOString();
|
|
229
|
+
let evidence;
|
|
230
|
+
let evidencePath;
|
|
231
|
+
if (input.writeEvidenceDraft !== false && input.mode !== "dry-run") {
|
|
232
|
+
evidence = buildEvidenceDraft({
|
|
233
|
+
campaignId: input.campaignId,
|
|
234
|
+
manifest: planned.manifest,
|
|
235
|
+
cellResults,
|
|
236
|
+
});
|
|
237
|
+
evidencePath = path
|
|
238
|
+
.relative(input.repoRoot, evidenceDraftPath(input.repoRoot, input.campaignId))
|
|
239
|
+
.replace(/\\/g, "/");
|
|
240
|
+
await writeJsonAtomic(evidenceDraftPath(input.repoRoot, input.campaignId), evidence, { repoRoot: input.repoRoot });
|
|
241
|
+
}
|
|
242
|
+
const matrix = matrixRunSchema.parse({
|
|
243
|
+
schemaVersion: 1,
|
|
244
|
+
campaignId: input.campaignId,
|
|
245
|
+
planHash: planned.plan.planHash,
|
|
246
|
+
mode: input.mode,
|
|
247
|
+
createdAt: now,
|
|
248
|
+
cells: cellResults,
|
|
249
|
+
privateCheckCount: privateResults.length,
|
|
250
|
+
privateCheckFailed: privateResults.filter((row) => !row.ok).length,
|
|
251
|
+
evidencePath,
|
|
252
|
+
autoPromote: false,
|
|
253
|
+
});
|
|
254
|
+
await writeJsonAtomic(matrixRunPath(input.repoRoot, input.campaignId), matrix, {
|
|
255
|
+
repoRoot: input.repoRoot,
|
|
256
|
+
});
|
|
257
|
+
return { matrix, privateResults, evidence };
|
|
258
|
+
}
|
|
259
|
+
function buildEvidenceDraft(input) {
|
|
260
|
+
const challengerCandidateId = input.manifest.challengerCandidateIds[0];
|
|
261
|
+
if (!challengerCandidateId) {
|
|
262
|
+
throw new Error("campaign has no challenger for evidence draft");
|
|
263
|
+
}
|
|
264
|
+
const cells = [];
|
|
265
|
+
for (const row of input.cellResults) {
|
|
266
|
+
if (row.verifyPassed === undefined)
|
|
267
|
+
continue;
|
|
268
|
+
if (row.executionRole !== "candidate_run" &&
|
|
269
|
+
row.executionRole !== "private_verify" &&
|
|
270
|
+
row.executionRole !== "promotion_gate_only") {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Only include executed public/private pairs; held_out needs external evidence.
|
|
274
|
+
if (row.executionRole === "promotion_gate_only")
|
|
275
|
+
continue;
|
|
276
|
+
cells.push({
|
|
277
|
+
cellId: row.cellId,
|
|
278
|
+
candidateId: row.candidateId,
|
|
279
|
+
taskRef: row.taskRef,
|
|
280
|
+
seed: row.seed,
|
|
281
|
+
split: row.split,
|
|
282
|
+
verifyPassed: row.verifyPassed,
|
|
283
|
+
runId: row.runId,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (cells.length === 0) {
|
|
287
|
+
// Minimal placeholder so draft is schema-valid for inspection; scorecard needs more.
|
|
288
|
+
cells.push({
|
|
289
|
+
candidateId: input.manifest.incumbentCandidateId,
|
|
290
|
+
taskRef: "matrix/placeholder",
|
|
291
|
+
seed: 0,
|
|
292
|
+
split: "public",
|
|
293
|
+
verifyPassed: false,
|
|
294
|
+
});
|
|
295
|
+
cells.push({
|
|
296
|
+
candidateId: challengerCandidateId,
|
|
297
|
+
taskRef: "matrix/placeholder",
|
|
298
|
+
seed: 0,
|
|
299
|
+
split: "public",
|
|
300
|
+
verifyPassed: false,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
schemaVersion: 1,
|
|
305
|
+
campaignId: input.campaignId,
|
|
306
|
+
challengerCandidateId,
|
|
307
|
+
cells,
|
|
308
|
+
diffs: [],
|
|
309
|
+
verifyCommands: { incumbent: [], challenger: [] },
|
|
310
|
+
budgetStatus: { incumbent: "missing", challenger: "missing" },
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
export async function readMatrixRun(repoRoot, campaignId) {
|
|
314
|
+
try {
|
|
315
|
+
const raw = await readFile(matrixRunPath(repoRoot, campaignId), "utf-8");
|
|
316
|
+
return matrixRunSchema.parse(JSON.parse(raw));
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
const code = error.code;
|
|
320
|
+
if (code === "ENOENT")
|
|
321
|
+
return undefined;
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
export function formatMatrixRunMarkdown(matrix) {
|
|
326
|
+
const executed = matrix.cells.filter((cell) => !cell.skipped);
|
|
327
|
+
const lines = [
|
|
328
|
+
`# Campaign Matrix: ${matrix.campaignId}`,
|
|
329
|
+
"",
|
|
330
|
+
`- mode: \`${matrix.mode}\``,
|
|
331
|
+
`- planHash: \`${matrix.planHash}\``,
|
|
332
|
+
`- cells: ${matrix.cells.length} (executed/recorded=${executed.length})`,
|
|
333
|
+
`- privateChecks: ${matrix.privateCheckCount} (failed=${matrix.privateCheckFailed})`,
|
|
334
|
+
`- evidencePath: \`${matrix.evidencePath ?? "(none)"}\``,
|
|
335
|
+
`- autoPromote: \`${matrix.autoPromote}\``,
|
|
336
|
+
"",
|
|
337
|
+
"## Cell results",
|
|
338
|
+
"",
|
|
339
|
+
];
|
|
340
|
+
for (const cell of matrix.cells.slice(0, 40)) {
|
|
341
|
+
lines.push(`- \`${cell.cellId}\` role=${cell.executionRole} skipped=${cell.skipped} passed=${cell.verifyPassed ?? "-"} ${cell.detail ?? ""}`);
|
|
342
|
+
}
|
|
343
|
+
if (matrix.cells.length > 40) {
|
|
344
|
+
lines.push(`- … ${matrix.cells.length - 40} more`);
|
|
345
|
+
}
|
|
346
|
+
lines.push("", "> Matrix never auto-promotes. Use `eval campaign scorecard` + human-gated `eval promote` / `eval experiment accept`.", "");
|
|
347
|
+
return `${lines.join("\n")}\n`;
|
|
348
|
+
}
|
|
349
|
+
/** Optional helper: build scorecard from matrix evidence draft when complete enough. */
|
|
350
|
+
export async function buildScorecardFromMatrixEvidence(input) {
|
|
351
|
+
const manifest = await readCampaignManifest(input.repoRoot, input.campaignId);
|
|
352
|
+
const plan = await readCampaignPlan(input.repoRoot, input.campaignId);
|
|
353
|
+
if (!plan)
|
|
354
|
+
throw new Error(`campaign ${input.campaignId} has no plan`);
|
|
355
|
+
const policy = evaluatePromotionPolicy({
|
|
356
|
+
campaignId: input.campaignId,
|
|
357
|
+
incumbentCandidateId: manifest.incumbentCandidateId,
|
|
358
|
+
evidence: input.evidence,
|
|
359
|
+
});
|
|
360
|
+
await writeCampaignScorecard({
|
|
361
|
+
repoRoot: input.repoRoot,
|
|
362
|
+
scorecard: {
|
|
363
|
+
schemaVersion: 1,
|
|
364
|
+
campaignId: input.campaignId,
|
|
365
|
+
planHash: plan.planHash,
|
|
366
|
+
createdAt: new Date().toISOString(),
|
|
367
|
+
evidenceCellCount: input.evidence.cells.length,
|
|
368
|
+
policy,
|
|
369
|
+
},
|
|
370
|
+
evidence: input.evidence,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readCampaignEvidence, readCampaignManifest, readCampaignPlan, readCampaignScorecard, writeCampaignScorecard, } from "../../infrastructure/evaluation/campaign-store.js";
|
|
4
|
+
import { evaluatePromotionPolicy } from "./promotion-policy.js";
|
|
5
|
+
import { campaignEvidenceSchema, } from "./types.js";
|
|
6
|
+
export async function loadCampaignEvidence(input) {
|
|
7
|
+
const absolute = path.resolve(input.repoRoot, input.evidencePath);
|
|
8
|
+
const raw = JSON.parse(await readFile(absolute, "utf-8"));
|
|
9
|
+
return campaignEvidenceSchema.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
export async function buildCampaignScorecard(input) {
|
|
12
|
+
const manifest = await readCampaignManifest(input.repoRoot, input.campaignId);
|
|
13
|
+
const plan = await readCampaignPlan(input.repoRoot, input.campaignId);
|
|
14
|
+
if (!plan) {
|
|
15
|
+
throw new Error(`campaign ${input.campaignId} has no plan; run eval campaign plan first`);
|
|
16
|
+
}
|
|
17
|
+
if (input.evidence.campaignId !== input.campaignId) {
|
|
18
|
+
throw new Error(`evidence campaignId mismatch: expected ${input.campaignId}, got ${input.evidence.campaignId}`);
|
|
19
|
+
}
|
|
20
|
+
const policy = evaluatePromotionPolicy({
|
|
21
|
+
campaignId: input.campaignId,
|
|
22
|
+
incumbentCandidateId: manifest.incumbentCandidateId,
|
|
23
|
+
evidence: input.evidence,
|
|
24
|
+
acknowledgeReview: input.acknowledgeReview,
|
|
25
|
+
});
|
|
26
|
+
const scorecard = {
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
campaignId: input.campaignId,
|
|
29
|
+
planHash: plan.planHash,
|
|
30
|
+
createdAt: new Date().toISOString(),
|
|
31
|
+
evidenceCellCount: input.evidence.cells.length,
|
|
32
|
+
policy,
|
|
33
|
+
};
|
|
34
|
+
if (input.persist !== false) {
|
|
35
|
+
await writeCampaignScorecard({
|
|
36
|
+
repoRoot: input.repoRoot,
|
|
37
|
+
scorecard,
|
|
38
|
+
evidence: input.evidence,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return scorecard;
|
|
42
|
+
}
|
|
43
|
+
export async function auditCampaignEvidence(input) {
|
|
44
|
+
const scorecard = await buildCampaignScorecard({
|
|
45
|
+
...input,
|
|
46
|
+
persist: false,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
findings: scorecard.policy.findings,
|
|
50
|
+
policy: scorecard.policy,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function requireCampaignPromotionGate(input) {
|
|
54
|
+
let evidence;
|
|
55
|
+
if (input.evidencePath) {
|
|
56
|
+
evidence = await loadCampaignEvidence({
|
|
57
|
+
repoRoot: input.repoRoot,
|
|
58
|
+
evidencePath: input.evidencePath,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
evidence = await readCampaignEvidence(input.repoRoot, input.campaignId);
|
|
63
|
+
}
|
|
64
|
+
let scorecard;
|
|
65
|
+
if (evidence) {
|
|
66
|
+
scorecard = await buildCampaignScorecard({
|
|
67
|
+
repoRoot: input.repoRoot,
|
|
68
|
+
campaignId: input.campaignId,
|
|
69
|
+
evidence,
|
|
70
|
+
acknowledgeReview: input.acknowledgeReview,
|
|
71
|
+
persist: true,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const existing = await readCampaignScorecard(input.repoRoot, input.campaignId);
|
|
76
|
+
if (!existing) {
|
|
77
|
+
throw new Error(`campaign ${input.campaignId} has no scorecard; run eval campaign scorecard --evidence <path> first`);
|
|
78
|
+
}
|
|
79
|
+
if (input.acknowledgeReview &&
|
|
80
|
+
existing.policy.gates.safetyOk === "review" &&
|
|
81
|
+
!existing.policy.promotionEligible) {
|
|
82
|
+
throw new Error(`campaign ${input.campaignId} scorecard needs re-evaluation with evidence; provide --evidence or re-run scorecard`);
|
|
83
|
+
}
|
|
84
|
+
scorecard = existing;
|
|
85
|
+
}
|
|
86
|
+
if (scorecard.policy.challengerCandidateId !== input.toCandidateId) {
|
|
87
|
+
throw new Error(`promotion target mismatch: scorecard challenger=${scorecard.policy.challengerCandidateId}, --to-candidate=${input.toCandidateId}`);
|
|
88
|
+
}
|
|
89
|
+
if (!scorecard.policy.promotionEligible) {
|
|
90
|
+
throw new Error(`campaign promotion policy rejected: ${scorecard.policy.reasons.join("; ")}`);
|
|
91
|
+
}
|
|
92
|
+
return scorecard;
|
|
93
|
+
}
|
|
94
|
+
export function formatCampaignScorecardMarkdown(scorecard) {
|
|
95
|
+
const { policy } = scorecard;
|
|
96
|
+
const lines = [
|
|
97
|
+
`# Campaign Scorecard: ${scorecard.campaignId}`,
|
|
98
|
+
"",
|
|
99
|
+
`- planHash: \`${scorecard.planHash}\``,
|
|
100
|
+
`- createdAt: \`${scorecard.createdAt}\``,
|
|
101
|
+
`- evidenceCells: ${scorecard.evidenceCellCount}`,
|
|
102
|
+
`- policy: \`${policy.policyVersion}\``,
|
|
103
|
+
`- promotionEligible: **${policy.promotionEligible}**`,
|
|
104
|
+
"",
|
|
105
|
+
"## Gates",
|
|
106
|
+
"",
|
|
107
|
+
`- privateImprovement: \`${policy.gates.privateImprovement}\``,
|
|
108
|
+
`- heldOutNonRegression: \`${policy.gates.heldOutNonRegression}\``,
|
|
109
|
+
`- budgetOk: \`${policy.gates.budgetOk}\``,
|
|
110
|
+
`- safetyOk: \`${policy.gates.safetyOk}\``,
|
|
111
|
+
`- humanRequired: \`${policy.gates.humanRequired}\``,
|
|
112
|
+
"",
|
|
113
|
+
"## Paired tallies",
|
|
114
|
+
"",
|
|
115
|
+
`- private: challengerWins=${policy.paired.private.challengerWins} incumbentWins=${policy.paired.private.incumbentWins} ties=${policy.paired.private.ties}`,
|
|
116
|
+
`- held_out: challengerWins=${policy.paired.held_out.challengerWins} incumbentWins=${policy.paired.held_out.incumbentWins} ties=${policy.paired.held_out.ties}`,
|
|
117
|
+
`- public: challengerWins=${policy.paired.public.challengerWins} incumbentWins=${policy.paired.public.incumbentWins} ties=${policy.paired.public.ties}`,
|
|
118
|
+
"",
|
|
119
|
+
];
|
|
120
|
+
if (policy.findings.length > 0) {
|
|
121
|
+
lines.push("## Integrity findings", "");
|
|
122
|
+
for (const finding of policy.findings) {
|
|
123
|
+
lines.push(`- [${finding.severity}] \`${finding.code}\`${finding.path ? ` @ ${finding.path}` : ""}: ${finding.message}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push("");
|
|
126
|
+
}
|
|
127
|
+
if (policy.reasons.length > 0) {
|
|
128
|
+
lines.push("## Reasons", "");
|
|
129
|
+
for (const reason of policy.reasons) {
|
|
130
|
+
lines.push(`- ${reason}`);
|
|
131
|
+
}
|
|
132
|
+
lines.push("");
|
|
133
|
+
}
|
|
134
|
+
return `${lines.join("\n")}\n`;
|
|
135
|
+
}
|