89 lines
3.7 KiB
JavaScript
89 lines
3.7 KiB
JavaScript
// agent-swarm verification runner — copy into the `script` field of the workflow tool.
|
|
// args: {
|
|
// wave?: number,
|
|
// tasks: [{ id: string, review: string }], // review = .swarm/reviews/Vxx.md
|
|
// concurrency?: number // verifier slots; default 4, keep free slots
|
|
// }
|
|
//
|
|
// The detailed review is written to disk BEFORE the structured verdict is
|
|
// returned. The workflow schema is intentionally tiny: a long findings array is
|
|
// easy to get wrong and a schema miss must not destroy the verification work.
|
|
|
|
const VERDICT = {
|
|
type: "object",
|
|
properties: {
|
|
task_id: { type: "string" },
|
|
verdict: { type: "string", enum: ["pass", "pass_with_risks", "fail"] },
|
|
summary: { type: "string" },
|
|
review_file: { type: "string" }
|
|
},
|
|
required: ["task_id", "verdict", "summary", "review_file"]
|
|
};
|
|
|
|
const CONCURRENCY = Math.max(
|
|
1,
|
|
Math.min(Number(args.concurrency) || 4, 8)
|
|
);
|
|
|
|
const promptFor = (t, attempt) => [
|
|
`You are verifier ${t.id}. You did not write this work; do not fix it and do not edit product source, tests, docs or manifests.`,
|
|
`Read .swarm/brief.md, the relevant .swarm/tasks/*.md cards, .swarm/contracts.md, and the implementation.`,
|
|
`Work only in ${t.review} and .swarm/tmp/${t.id}/. Copy or hash-check generated files instead of rewriting the tree.`,
|
|
`Verify against the original acceptance criteria, not the worker's summary. Run the commands yourself and record exact commands + observed results. Findings need file:line, reproduction, expected vs actual.`,
|
|
`Freeze assumption: the tree is not being edited during this wave. If you see a stale/inconsistent build, say so; do not fix it.`,
|
|
`FIRST write the full review to ${t.review}. THEN return only {task_id, verdict, summary, review_file}; keep summary <= 2000 chars. The review file is the source of truth and must exist even if the structured reply fails.`,
|
|
`Verdict rules: "fail" for critical/major product gaps; "pass_with_risks" for minor or documented-scope risks; "pass" only when no material issue remains.`,
|
|
attempt > 1
|
|
? `The previous structured reply was unusable. Preserve the review file; return the four required fields only.`
|
|
: ""
|
|
].filter(Boolean).join("\n");
|
|
|
|
const failed = (t, why) => ({
|
|
task_id: t.id,
|
|
verdict: "fail",
|
|
summary: why,
|
|
review_file: t.review
|
|
});
|
|
|
|
async function runOne(t) {
|
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
try {
|
|
const r = await agent(promptFor(t, attempt), {
|
|
label: t.id,
|
|
phase: `verify ${args.wave || ""}`.trim(),
|
|
schema: VERDICT
|
|
});
|
|
if (r && r.verdict) return r;
|
|
log(`${t.id}: attempt ${attempt} produced no usable verdict`);
|
|
} catch (error) {
|
|
const message = String(error && error.message ? error.message : error);
|
|
if (/(limit|spawn|could not start|active child|capacity|start a child)/i.test(message)) {
|
|
log(`${t.id}: scheduling error: ${message}`);
|
|
return failed(t, `scheduling error: ${message}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
return failed(t, "no usable structured verdict; see review file");
|
|
}
|
|
|
|
phase(`verify${args.wave ? ` ${args.wave}` : ""}`);
|
|
log(`Dispatching ${args.tasks.length} verifier(s) in batches of ${CONCURRENCY}`);
|
|
|
|
const results = [];
|
|
for (let start = 0; start < args.tasks.length; start += CONCURRENCY) {
|
|
const batch = args.tasks.slice(start, start + CONCURRENCY);
|
|
const batchResults = await parallel(batch.map((t) => () => runOne(t)));
|
|
results.push(...batchResults);
|
|
}
|
|
|
|
return {
|
|
wave: args.wave || null,
|
|
concurrency: CONCURRENCY,
|
|
total: results.length,
|
|
pass: results.filter((r) => r && r.verdict === "pass").length,
|
|
pass_with_risks: results.filter((r) => r && r.verdict === "pass_with_risks").length,
|
|
fail: results.filter((r) => !r || r.verdict === "fail").length,
|
|
results
|
|
};
|