@tea-agent/loop-agent 0.16.1-beta.2 → 0.16.2
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 +76 -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 +8 -46
- package/dist/executors/pi-sdk-executor.js +66 -3
- package/dist/executors/shell-executor.js +213 -30
- package/dist/executors/shell-presets.js +12 -2
- package/dist/executors/shell-write-guard.js +20 -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/shared/git-progress.js +9 -2
- 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/observability/read-model.js +56 -0
- package/dist/worker/observe/server.js +6 -3
- 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/backend-test-analysis-contract.js +87 -30
- package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
- package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
- package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
- package/dist/workflows/dag/backend-test-result-contract.js +6 -4
- package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
- package/dist/workflows/dag/budget-enforcement.js +67 -0
- package/dist/workflows/dag/context-policy.js +137 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
- package/dist/workflows/dag/failure-routing.js +8 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +32 -93
- package/dist/workflows/dag/init-hybrid.js +624 -172
- package/dist/workflows/dag/knowledge-curator.js +3 -0
- package/dist/workflows/dag/lifecycle.js +33 -2
- 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/scheduler.js +87 -17
- package/dist/workflows/dag/skill-snapshot.js +11 -7
- package/dist/workflows/dag/types.js +49 -1
- package/dist/workflows/dag/validate.js +35 -15
- 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 +25 -7
- package/docs/templates/agent-dag.supervised-implementation.json +23 -4
- package/docs/templates/backend-test-analysis.schema.json +9 -16
- package/docs/templates/backend-test-dag.json +493 -197
- package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
- package/docs/templates/backend-test-execution.schema.json +6 -1
- 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 +7 -4
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { campaignBudgetSchema } from "./budget.js";
|
|
5
|
+
import { computeBudgetHash, computeCampaignPlanHash, computeScoringRulesHash, } from "./campaign-hash.js";
|
|
6
|
+
import { showCandidate } from "./candidate.js";
|
|
7
|
+
import { showCorpus } from "./corpus.js";
|
|
8
|
+
import { listCampaignIds, readCampaignManifest, readCampaignPlan, registerCampaignManifest, writeCampaignPlan, } from "../../infrastructure/evaluation/campaign-store.js";
|
|
9
|
+
import { readPrivateVerifierManifest } from "../../infrastructure/evaluation/private-verifier-store.js";
|
|
10
|
+
import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
|
|
11
|
+
import { campaignManifestInputSchema, campaignManifestSchema, } from "./types.js";
|
|
12
|
+
const EXECUTABLE_LIFECYCLES = new Set([
|
|
13
|
+
"eligible",
|
|
14
|
+
"experimenting",
|
|
15
|
+
"accepted",
|
|
16
|
+
]);
|
|
17
|
+
function cellId(parts) {
|
|
18
|
+
const safeTask = parts.taskRef.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
19
|
+
return `${parts.candidateId}__${parts.split}__${safeTask}__s${parts.seed}`;
|
|
20
|
+
}
|
|
21
|
+
export function collectPrivateVerifierSecrets(manifest) {
|
|
22
|
+
const secrets = new Set();
|
|
23
|
+
secrets.add(manifest.privateVerifierId);
|
|
24
|
+
secrets.add(manifest.manifestHash);
|
|
25
|
+
for (const check of manifest.checks) {
|
|
26
|
+
secrets.add(check.checkId);
|
|
27
|
+
for (const token of check.command) {
|
|
28
|
+
if (token.trim().length >= 3)
|
|
29
|
+
secrets.add(token);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [...secrets];
|
|
33
|
+
}
|
|
34
|
+
export async function assertCandidateDoesNotLeakPrivateVerifier(input) {
|
|
35
|
+
const record = await showCandidate({
|
|
36
|
+
repoRoot: input.repoRoot,
|
|
37
|
+
candidateId: input.candidateId,
|
|
38
|
+
});
|
|
39
|
+
const secrets = collectPrivateVerifierSecrets(input.privateVerifier);
|
|
40
|
+
for (const ref of record.manifest.contentRefs) {
|
|
41
|
+
const absolute = path.resolve(input.repoRoot, ref.path);
|
|
42
|
+
const bytes = await readFile(absolute, "utf-8");
|
|
43
|
+
for (const secret of secrets) {
|
|
44
|
+
if (bytes.includes(secret)) {
|
|
45
|
+
throw new Error(`private verifier leak: candidate ${input.candidateId} contentRef ${ref.path} contains private token`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function verifyCampaignIntegrity(repoRoot, manifest) {
|
|
51
|
+
const corpus = await showCorpus({
|
|
52
|
+
repoRoot,
|
|
53
|
+
corpusId: manifest.corpusId,
|
|
54
|
+
});
|
|
55
|
+
if (corpus.manifest.corpusHash !== manifest.corpusHash) {
|
|
56
|
+
throw new Error(`campaign corpusHash drifted: declared ${manifest.corpusHash}, registry ${corpus.manifest.corpusHash}`);
|
|
57
|
+
}
|
|
58
|
+
const privateVerifier = await readPrivateVerifierManifest(repoRoot, manifest.privateVerifierId);
|
|
59
|
+
if (privateVerifier.manifestHash !== manifest.privateVerifierManifestHash) {
|
|
60
|
+
throw new Error(`campaign privateVerifierManifestHash drifted: declared ${manifest.privateVerifierManifestHash}, registry ${privateVerifier.manifestHash}`);
|
|
61
|
+
}
|
|
62
|
+
const budgetHash = computeBudgetHash(manifest.budget);
|
|
63
|
+
if (budgetHash !== manifest.evaluatorIntegrity.budgetHash) {
|
|
64
|
+
throw new Error(`campaign budgetHash drifted: declared ${manifest.evaluatorIntegrity.budgetHash}, recomputed ${budgetHash}`);
|
|
65
|
+
}
|
|
66
|
+
if (corpus.manifest.corpusHash !== manifest.evaluatorIntegrity.corpusHash) {
|
|
67
|
+
throw new Error("campaign evaluatorIntegrity.corpusHash mismatch");
|
|
68
|
+
}
|
|
69
|
+
if (privateVerifier.manifestHash !==
|
|
70
|
+
manifest.evaluatorIntegrity.privateVerifierManifestHash) {
|
|
71
|
+
throw new Error("campaign evaluatorIntegrity.privateVerifierManifestHash mismatch");
|
|
72
|
+
}
|
|
73
|
+
const scoringRulesHash = computeScoringRulesHash(manifest.policyVersion);
|
|
74
|
+
if (scoringRulesHash !== manifest.evaluatorIntegrity.scoringRulesHash) {
|
|
75
|
+
throw new Error(`campaign scoringRulesHash drifted for policyVersion ${manifest.policyVersion}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function buildCampaignPlan(input) {
|
|
79
|
+
await verifyCampaignIntegrity(input.repoRoot, input.manifest);
|
|
80
|
+
const corpus = await showCorpus({
|
|
81
|
+
repoRoot: input.repoRoot,
|
|
82
|
+
corpusId: input.manifest.corpusId,
|
|
83
|
+
});
|
|
84
|
+
const privateVerifier = await readPrivateVerifierManifest(input.repoRoot, input.manifest.privateVerifierId);
|
|
85
|
+
const candidateIds = [
|
|
86
|
+
input.manifest.incumbentCandidateId,
|
|
87
|
+
...input.manifest.challengerCandidateIds,
|
|
88
|
+
];
|
|
89
|
+
const cells = [];
|
|
90
|
+
for (const candidateId of candidateIds) {
|
|
91
|
+
await assertCandidateDoesNotLeakPrivateVerifier({
|
|
92
|
+
repoRoot: input.repoRoot,
|
|
93
|
+
candidateId,
|
|
94
|
+
privateVerifier,
|
|
95
|
+
});
|
|
96
|
+
const record = await showCandidate({
|
|
97
|
+
repoRoot: input.repoRoot,
|
|
98
|
+
candidateId,
|
|
99
|
+
});
|
|
100
|
+
if (!EXECUTABLE_LIFECYCLES.has(record.status)) {
|
|
101
|
+
throw new Error(`campaign candidate ${candidateId} lifecycle ${record.status} is not executable`);
|
|
102
|
+
}
|
|
103
|
+
for (const task of corpus.manifest.tasks) {
|
|
104
|
+
const seeds = input.manifest.seeds && input.manifest.seeds.length > 0
|
|
105
|
+
? input.manifest.seeds.filter((seed) => task.seeds.includes(seed))
|
|
106
|
+
: task.seeds;
|
|
107
|
+
if (seeds.length === 0) {
|
|
108
|
+
throw new Error(`campaign seeds do not intersect task ${task.taskRef} seeds`);
|
|
109
|
+
}
|
|
110
|
+
for (const seed of seeds) {
|
|
111
|
+
const split = task.split;
|
|
112
|
+
const executionRole = split === "public"
|
|
113
|
+
? "candidate_run"
|
|
114
|
+
: split === "private"
|
|
115
|
+
? "private_verify"
|
|
116
|
+
: "promotion_gate_only";
|
|
117
|
+
cells.push({
|
|
118
|
+
cellId: cellId({
|
|
119
|
+
candidateId,
|
|
120
|
+
split,
|
|
121
|
+
taskRef: task.taskRef,
|
|
122
|
+
seed,
|
|
123
|
+
}),
|
|
124
|
+
candidateId,
|
|
125
|
+
bundleHash: record.manifest.bundleHash,
|
|
126
|
+
taskRef: task.taskRef,
|
|
127
|
+
seed,
|
|
128
|
+
split,
|
|
129
|
+
executionRole,
|
|
130
|
+
privateVerifierAttached: executionRole === "private_verify",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
cells.sort((a, b) => (a.cellId < b.cellId ? -1 : a.cellId > b.cellId ? 1 : 0));
|
|
136
|
+
const planHash = computeCampaignPlanHash(input.manifest.campaignId, cells);
|
|
137
|
+
return {
|
|
138
|
+
schemaVersion: 1,
|
|
139
|
+
campaignId: input.manifest.campaignId,
|
|
140
|
+
planHash,
|
|
141
|
+
createdAt: new Date().toISOString(),
|
|
142
|
+
cells,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export async function materializeCampaignManifest(input) {
|
|
146
|
+
const parsed = campaignManifestInputSchema.parse(input.raw);
|
|
147
|
+
const budget = campaignBudgetSchema.parse(parsed.budget);
|
|
148
|
+
if (budget.mode !== "hard") {
|
|
149
|
+
throw new Error("campaign budget.mode must be hard");
|
|
150
|
+
}
|
|
151
|
+
const corpus = await showCorpus({
|
|
152
|
+
repoRoot: input.repoRoot,
|
|
153
|
+
corpusId: parsed.corpusId,
|
|
154
|
+
});
|
|
155
|
+
if (corpus.manifest.corpusHash !== parsed.corpusHash) {
|
|
156
|
+
throw new Error(`corpusHash mismatch: declared ${parsed.corpusHash}, registry ${corpus.manifest.corpusHash}`);
|
|
157
|
+
}
|
|
158
|
+
const privateVerifier = await readPrivateVerifierManifest(input.repoRoot, parsed.privateVerifierId);
|
|
159
|
+
if (privateVerifier.manifestHash !== parsed.privateVerifierManifestHash) {
|
|
160
|
+
throw new Error(`privateVerifierManifestHash mismatch: declared ${parsed.privateVerifierManifestHash}, registry ${privateVerifier.manifestHash}`);
|
|
161
|
+
}
|
|
162
|
+
const candidateIds = [
|
|
163
|
+
parsed.incumbentCandidateId,
|
|
164
|
+
...parsed.challengerCandidateIds,
|
|
165
|
+
];
|
|
166
|
+
for (const candidateId of candidateIds) {
|
|
167
|
+
const record = await showCandidate({
|
|
168
|
+
repoRoot: input.repoRoot,
|
|
169
|
+
candidateId,
|
|
170
|
+
});
|
|
171
|
+
if (!EXECUTABLE_LIFECYCLES.has(record.status)) {
|
|
172
|
+
throw new Error(`campaign candidate ${candidateId} lifecycle ${record.status} is not executable`);
|
|
173
|
+
}
|
|
174
|
+
await assertCandidateDoesNotLeakPrivateVerifier({
|
|
175
|
+
repoRoot: input.repoRoot,
|
|
176
|
+
candidateId,
|
|
177
|
+
privateVerifier,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const identity = resolveRunningControllerIdentity();
|
|
181
|
+
const controllerFingerprint = parsed.controllerFingerprint ??
|
|
182
|
+
(identity
|
|
183
|
+
? `${identity.packageName}@${identity.packageVersion}+${identity.packageFingerprint.value}`
|
|
184
|
+
: "unresolved-controller");
|
|
185
|
+
const evaluatorIntegrity = {
|
|
186
|
+
corpusHash: corpus.manifest.corpusHash,
|
|
187
|
+
privateVerifierManifestHash: privateVerifier.manifestHash,
|
|
188
|
+
budgetHash: computeBudgetHash(budget),
|
|
189
|
+
scoringRulesHash: computeScoringRulesHash(parsed.policyVersion),
|
|
190
|
+
};
|
|
191
|
+
if (parsed.evaluatorIntegrity) {
|
|
192
|
+
if (JSON.stringify(parsed.evaluatorIntegrity) !==
|
|
193
|
+
JSON.stringify(evaluatorIntegrity)) {
|
|
194
|
+
throw new Error("evaluatorIntegrity mismatch with recomputed corpus/privateVerifier/budget/scoring hashes");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return campaignManifestSchema.parse({
|
|
198
|
+
...parsed,
|
|
199
|
+
budget,
|
|
200
|
+
controllerFingerprint,
|
|
201
|
+
evaluatorIntegrity,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
export async function createCampaign(input) {
|
|
205
|
+
const resolved = path.isAbsolute(input.manifestPath)
|
|
206
|
+
? input.manifestPath
|
|
207
|
+
: path.resolve(input.repoRoot, input.manifestPath);
|
|
208
|
+
const raw = JSON.parse(await readFile(resolved, "utf-8"));
|
|
209
|
+
const manifest = await materializeCampaignManifest({
|
|
210
|
+
repoRoot: input.repoRoot,
|
|
211
|
+
raw,
|
|
212
|
+
});
|
|
213
|
+
const registered = await registerCampaignManifest({
|
|
214
|
+
repoRoot: input.repoRoot,
|
|
215
|
+
manifest,
|
|
216
|
+
});
|
|
217
|
+
const plan = await buildCampaignPlan({
|
|
218
|
+
repoRoot: input.repoRoot,
|
|
219
|
+
manifest: registered.manifest,
|
|
220
|
+
});
|
|
221
|
+
const planPath = await writeCampaignPlan({
|
|
222
|
+
repoRoot: input.repoRoot,
|
|
223
|
+
plan,
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
manifest: registered.manifest,
|
|
227
|
+
plan,
|
|
228
|
+
idempotent: registered.idempotent,
|
|
229
|
+
manifestPath: registered.manifestPath,
|
|
230
|
+
planPath,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export async function validateCampaign(input) {
|
|
234
|
+
const resolved = path.isAbsolute(input.manifestPath)
|
|
235
|
+
? input.manifestPath
|
|
236
|
+
: path.resolve(input.repoRoot, input.manifestPath);
|
|
237
|
+
const raw = JSON.parse(await readFile(resolved, "utf-8"));
|
|
238
|
+
const manifest = await materializeCampaignManifest({
|
|
239
|
+
repoRoot: input.repoRoot,
|
|
240
|
+
raw,
|
|
241
|
+
});
|
|
242
|
+
const plan = await buildCampaignPlan({
|
|
243
|
+
repoRoot: input.repoRoot,
|
|
244
|
+
manifest,
|
|
245
|
+
});
|
|
246
|
+
return { manifest, plan };
|
|
247
|
+
}
|
|
248
|
+
export async function showCampaign(input) {
|
|
249
|
+
const manifest = await readCampaignManifest(input.repoRoot, input.campaignId);
|
|
250
|
+
await verifyCampaignIntegrity(input.repoRoot, manifest);
|
|
251
|
+
const plan = await readCampaignPlan(input.repoRoot, input.campaignId);
|
|
252
|
+
return {
|
|
253
|
+
manifest,
|
|
254
|
+
plan,
|
|
255
|
+
summary: summarizeCampaign(manifest, plan),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
export async function listCampaigns(input) {
|
|
259
|
+
const ids = await listCampaignIds(input.repoRoot);
|
|
260
|
+
const rows = [];
|
|
261
|
+
for (const campaignId of ids) {
|
|
262
|
+
const manifest = await readCampaignManifest(input.repoRoot, campaignId);
|
|
263
|
+
const plan = await readCampaignPlan(input.repoRoot, campaignId);
|
|
264
|
+
rows.push(summarizeCampaign(manifest, plan));
|
|
265
|
+
}
|
|
266
|
+
return rows;
|
|
267
|
+
}
|
|
268
|
+
export async function planCampaign(input) {
|
|
269
|
+
const manifest = await readCampaignManifest(input.repoRoot, input.campaignId);
|
|
270
|
+
const plan = await buildCampaignPlan({
|
|
271
|
+
repoRoot: input.repoRoot,
|
|
272
|
+
manifest,
|
|
273
|
+
});
|
|
274
|
+
const existing = await readCampaignPlan(input.repoRoot, input.campaignId);
|
|
275
|
+
if (existing && existing.planHash !== plan.planHash) {
|
|
276
|
+
throw new Error(`campaign planHash drifted for ${input.campaignId}: stored ${existing.planHash}, recomputed ${plan.planHash}`);
|
|
277
|
+
}
|
|
278
|
+
const planPath = await writeCampaignPlan({
|
|
279
|
+
repoRoot: input.repoRoot,
|
|
280
|
+
plan: existing ?? plan,
|
|
281
|
+
});
|
|
282
|
+
return { manifest, plan: existing ?? plan, planPath };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* M4 campaign run: re-verify integrity, ensure plan repeatability, and execute
|
|
286
|
+
* controller-only private verifier checks. Does not run Pi candidate DAGs and
|
|
287
|
+
* never auto-promotes.
|
|
288
|
+
*/
|
|
289
|
+
export async function runCampaignPrivateChecks(input) {
|
|
290
|
+
const planned = await planCampaign(input);
|
|
291
|
+
const privateVerifier = await readPrivateVerifierManifest(input.repoRoot, planned.manifest.privateVerifierId);
|
|
292
|
+
const results = [];
|
|
293
|
+
for (const cell of planned.plan.cells) {
|
|
294
|
+
if (cell.executionRole !== "private_verify")
|
|
295
|
+
continue;
|
|
296
|
+
if (!cell.privateVerifierAttached) {
|
|
297
|
+
throw new Error(`private_verify cell ${cell.cellId} missing privateVerifierAttached`);
|
|
298
|
+
}
|
|
299
|
+
for (const check of privateVerifier.checks) {
|
|
300
|
+
const cwd = path.resolve(input.repoRoot, check.cwd);
|
|
301
|
+
const [bin, ...args] = check.command;
|
|
302
|
+
const spawned = spawnSync(bin, args, {
|
|
303
|
+
cwd,
|
|
304
|
+
encoding: "utf-8",
|
|
305
|
+
timeout: check.timeoutMs,
|
|
306
|
+
env: process.env,
|
|
307
|
+
});
|
|
308
|
+
const exitCode = typeof spawned.status === "number" ? spawned.status : null;
|
|
309
|
+
results.push({
|
|
310
|
+
cellId: cell.cellId,
|
|
311
|
+
checkId: check.checkId,
|
|
312
|
+
ok: exitCode === check.expectedExitCode,
|
|
313
|
+
exitCode,
|
|
314
|
+
stderr: (spawned.stderr ?? "").slice(0, 2_000),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
manifest: planned.manifest,
|
|
320
|
+
plan: planned.plan,
|
|
321
|
+
results,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function summarizeCampaign(manifest, plan) {
|
|
325
|
+
return {
|
|
326
|
+
campaignId: manifest.campaignId,
|
|
327
|
+
status: manifest.status,
|
|
328
|
+
corpusId: manifest.corpusId,
|
|
329
|
+
incumbentCandidateId: manifest.incumbentCandidateId,
|
|
330
|
+
challengerCount: manifest.challengerCandidateIds.length,
|
|
331
|
+
...(plan
|
|
332
|
+
? { cellCount: plan.cells.length, planHash: plan.planHash }
|
|
333
|
+
: {}),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
export function formatCampaignMarkdown(input) {
|
|
337
|
+
const { manifest, plan } = input;
|
|
338
|
+
const lines = [
|
|
339
|
+
`# Campaign: ${manifest.campaignId}`,
|
|
340
|
+
"",
|
|
341
|
+
`- status: \`${manifest.status}\``,
|
|
342
|
+
`- corpus: \`${manifest.corpusId}\` (\`${manifest.corpusHash}\`)`,
|
|
343
|
+
`- incumbent: \`${manifest.incumbentCandidateId}\``,
|
|
344
|
+
`- challengers: ${manifest.challengerCandidateIds.map((id) => `\`${id}\``).join(", ")}`,
|
|
345
|
+
`- privateVerifier: \`${manifest.privateVerifierId}\``,
|
|
346
|
+
`- budget.mode: \`${manifest.budget.mode}\``,
|
|
347
|
+
`- policyVersion: \`${manifest.policyVersion}\``,
|
|
348
|
+
"",
|
|
349
|
+
"## Evaluator integrity",
|
|
350
|
+
"",
|
|
351
|
+
`- corpusHash: \`${manifest.evaluatorIntegrity.corpusHash}\``,
|
|
352
|
+
`- privateVerifierManifestHash: \`${manifest.evaluatorIntegrity.privateVerifierManifestHash}\``,
|
|
353
|
+
`- budgetHash: \`${manifest.evaluatorIntegrity.budgetHash}\``,
|
|
354
|
+
`- scoringRulesHash: \`${manifest.evaluatorIntegrity.scoringRulesHash}\``,
|
|
355
|
+
"",
|
|
356
|
+
"> M4: private verifier is controller-only; held_out cells are promotion_gate_only; promotion remains human-gated.",
|
|
357
|
+
"",
|
|
358
|
+
];
|
|
359
|
+
if (plan) {
|
|
360
|
+
const counts = {
|
|
361
|
+
candidate_run: 0,
|
|
362
|
+
private_verify: 0,
|
|
363
|
+
promotion_gate_only: 0,
|
|
364
|
+
};
|
|
365
|
+
for (const cell of plan.cells)
|
|
366
|
+
counts[cell.executionRole] += 1;
|
|
367
|
+
lines.push(`## Plan`, "", `- planHash: \`${plan.planHash}\``, `- cells: ${plan.cells.length} (candidate_run=${counts.candidate_run}, private_verify=${counts.private_verify}, promotion_gate_only=${counts.promotion_gate_only})`, "");
|
|
368
|
+
}
|
|
369
|
+
return `${lines.join("\n")}\n`;
|
|
370
|
+
}
|
|
@@ -1,33 +1,50 @@
|
|
|
1
1
|
import { loadManifestInputFromPath, listCandidateIds, materializeManifest, readCandidateRecord, registerCandidateManifest, transitionCandidateLifecycle, } from "../../infrastructure/evaluation/candidate-store.js";
|
|
2
|
+
import { isCandidatePromotionApplied } from "./alias.js";
|
|
3
|
+
async function withPromotionFlag(repoRoot, record) {
|
|
4
|
+
const promotionApplied = await isCandidatePromotionApplied({
|
|
5
|
+
repoRoot,
|
|
6
|
+
candidateId: record.manifest.candidateId,
|
|
7
|
+
});
|
|
8
|
+
return { ...record, promotionApplied };
|
|
9
|
+
}
|
|
2
10
|
export async function registerCandidate(input) {
|
|
3
11
|
const raw = await loadManifestInputFromPath(input.repoRoot, input.manifestPath);
|
|
4
12
|
const manifest = await materializeManifest(input.repoRoot, raw);
|
|
5
|
-
|
|
13
|
+
const result = await registerCandidateManifest({
|
|
6
14
|
repoRoot: input.repoRoot,
|
|
7
15
|
manifest,
|
|
8
16
|
now: input.now,
|
|
9
17
|
});
|
|
18
|
+
return {
|
|
19
|
+
...result,
|
|
20
|
+
record: await withPromotionFlag(input.repoRoot, result.record),
|
|
21
|
+
};
|
|
10
22
|
}
|
|
11
23
|
export async function showCandidate(input) {
|
|
12
|
-
|
|
24
|
+
const record = await readCandidateRecord(input.repoRoot, input.candidateId);
|
|
25
|
+
return withPromotionFlag(input.repoRoot, record);
|
|
13
26
|
}
|
|
14
27
|
export async function listCandidates(input) {
|
|
15
28
|
const ids = await listCandidateIds(input.repoRoot);
|
|
16
29
|
const rows = [];
|
|
17
30
|
for (const candidateId of ids) {
|
|
18
|
-
const record = await
|
|
31
|
+
const record = await showCandidate({
|
|
32
|
+
repoRoot: input.repoRoot,
|
|
33
|
+
candidateId,
|
|
34
|
+
});
|
|
19
35
|
rows.push({
|
|
20
36
|
candidateId: record.manifest.candidateId,
|
|
21
37
|
bundleHash: record.manifest.bundleHash,
|
|
22
38
|
candidateKind: record.manifest.candidateKind,
|
|
23
39
|
status: record.status,
|
|
24
|
-
promotionApplied:
|
|
40
|
+
promotionApplied: record.promotionApplied,
|
|
25
41
|
});
|
|
26
42
|
}
|
|
27
43
|
return rows;
|
|
28
44
|
}
|
|
29
45
|
export async function transitionCandidate(input) {
|
|
30
|
-
|
|
46
|
+
const record = await transitionCandidateLifecycle(input);
|
|
47
|
+
return withPromotionFlag(input.repoRoot, record);
|
|
31
48
|
}
|
|
32
49
|
export function formatCandidateMarkdown(record) {
|
|
33
50
|
const lines = [
|
|
@@ -37,7 +54,7 @@ export function formatCandidateMarkdown(record) {
|
|
|
37
54
|
`- kind: \`${record.manifest.candidateKind}\``,
|
|
38
55
|
`- bundleHash: \`${record.manifest.bundleHash}\``,
|
|
39
56
|
`- parent: \`${record.manifest.parentCandidateId ?? "null"}\``,
|
|
40
|
-
`- promotionApplied: \`
|
|
57
|
+
`- promotionApplied: \`${record.promotionApplied}\` (true only when an incumbent alias points here)`,
|
|
41
58
|
"",
|
|
42
59
|
"## Content refs",
|
|
43
60
|
"",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { formatContentSha, normalizeContentSha, stableStringify, } from "./candidate-hash.js";
|
|
3
|
+
function normalizeTask(task) {
|
|
4
|
+
return {
|
|
5
|
+
taskRef: task.taskRef.replace(/\\/g, "/"),
|
|
6
|
+
class: task.class,
|
|
7
|
+
split: task.split,
|
|
8
|
+
seeds: [...task.seeds].sort((a, b) => a - b),
|
|
9
|
+
featureId: task.featureId ?? null,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Canonical payload for corpusHash.
|
|
14
|
+
* Excludes corpusId, createdAt, description, corpusHash, and free-text notes.
|
|
15
|
+
*/
|
|
16
|
+
export function buildCanonicalCorpusPayload(manifest) {
|
|
17
|
+
const tasks = manifest.tasks
|
|
18
|
+
.map((task) => normalizeTask(task))
|
|
19
|
+
.sort((a, b) => (a.taskRef < b.taskRef ? -1 : a.taskRef > b.taskRef ? 1 : 0));
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
tasks,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function computeCorpusHash(manifest) {
|
|
26
|
+
const payload = buildCanonicalCorpusPayload(manifest);
|
|
27
|
+
const digest = createHash("sha256")
|
|
28
|
+
.update(stableStringify(payload))
|
|
29
|
+
.digest("hex");
|
|
30
|
+
return formatContentSha(digest);
|
|
31
|
+
}
|
|
32
|
+
export function assertCorpusHashMatches(manifest) {
|
|
33
|
+
const expected = computeCorpusHash(manifest);
|
|
34
|
+
const actual = formatContentSha(normalizeContentSha(manifest.corpusHash));
|
|
35
|
+
if (expected !== actual) {
|
|
36
|
+
throw new Error(`corpusHash mismatch: expected ${expected}, got ${actual}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { loadCorpusManifestFromPath, listCorpusIds, materializeCorpusManifest, readCorpusManifest, registerCorpusManifest, summarizeCorpus, } from "../../infrastructure/evaluation/corpus-store.js";
|
|
2
|
+
export async function validateCorpus(input) {
|
|
3
|
+
const raw = await loadCorpusManifestFromPath(input.repoRoot, input.manifestPath);
|
|
4
|
+
const manifest = await materializeCorpusManifest(raw);
|
|
5
|
+
return { manifest, summary: summarizeCorpus(manifest) };
|
|
6
|
+
}
|
|
7
|
+
export async function registerCorpus(input) {
|
|
8
|
+
const validated = await validateCorpus(input);
|
|
9
|
+
const result = await registerCorpusManifest({
|
|
10
|
+
repoRoot: input.repoRoot,
|
|
11
|
+
manifest: validated.manifest,
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
manifest: result.manifest,
|
|
15
|
+
summary: summarizeCorpus(result.manifest),
|
|
16
|
+
idempotent: result.idempotent,
|
|
17
|
+
manifestPath: result.manifestPath,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function showCorpus(input) {
|
|
21
|
+
const manifest = await readCorpusManifest(input.repoRoot, input.corpusId);
|
|
22
|
+
return { manifest, summary: summarizeCorpus(manifest) };
|
|
23
|
+
}
|
|
24
|
+
export async function listCorpora(input) {
|
|
25
|
+
const ids = await listCorpusIds(input.repoRoot);
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (const corpusId of ids) {
|
|
28
|
+
const manifest = await readCorpusManifest(input.repoRoot, corpusId);
|
|
29
|
+
rows.push(summarizeCorpus(manifest));
|
|
30
|
+
}
|
|
31
|
+
return rows;
|
|
32
|
+
}
|
|
33
|
+
export function formatCorpusMarkdown(input) {
|
|
34
|
+
const { manifest, summary } = input;
|
|
35
|
+
const lines = [
|
|
36
|
+
`# Corpus: ${manifest.corpusId}`,
|
|
37
|
+
"",
|
|
38
|
+
`- corpusHash: \`${manifest.corpusHash}\``,
|
|
39
|
+
`- taskCount: \`${summary.taskCount}\``,
|
|
40
|
+
`- splits: public=${summary.splits.public} private=${summary.splits.private} held_out=${summary.splits.held_out}`,
|
|
41
|
+
`- classes: ${summary.classes.map((value) => `\`${value}\``).join(", ")}`,
|
|
42
|
+
`- createdAt: \`${manifest.createdAt}\``,
|
|
43
|
+
"",
|
|
44
|
+
"> Contract-only corpus. Registration does not execute live campaigns or authorize promotion.",
|
|
45
|
+
"",
|
|
46
|
+
"## Tasks",
|
|
47
|
+
"",
|
|
48
|
+
"| taskRef | class | split | seeds | featureId |",
|
|
49
|
+
"|---|---|---|---|---|",
|
|
50
|
+
];
|
|
51
|
+
for (const task of manifest.tasks) {
|
|
52
|
+
lines.push(`| \`${task.taskRef}\` | ${task.class} | ${task.split} | ${task.seeds.join(",")} | ${task.featureId ?? "-"} |`);
|
|
53
|
+
}
|
|
54
|
+
lines.push("");
|
|
55
|
+
return `${lines.join("\n")}\n`;
|
|
56
|
+
}
|