Add agent-swarm skill
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# Swarm brief — {{TASK}}
|
||||
|
||||
## Goal
|
||||
{{One paragraph. What must exist when the swarm is done.}}
|
||||
|
||||
## Success criteria
|
||||
- [ ] {{Criterion}} — verified by {{check or command}}
|
||||
- [ ] {{Criterion}} — verified by {{check or command}}
|
||||
|
||||
Scope is checked by the lead with the skill's `scripts/audit-scope.mjs`; workers do not run it.
|
||||
|
||||
## Non-goals
|
||||
- {{What must not change or be attempted.}}
|
||||
|
||||
## Stack and commands
|
||||
| Action | Command |
|
||||
|---|---|
|
||||
| build | `{{command}}` |
|
||||
| test | `{{command}}` |
|
||||
| lint | `{{command}}` |
|
||||
| run | `{{command}}` |
|
||||
|
||||
## Repository map
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `{{path}}` | {{one line}} |
|
||||
|
||||
## Conventions and invariants
|
||||
- {{Naming, error handling, test style, formatting.}}
|
||||
- {{Data or API invariants that must not break.}}
|
||||
|
||||
## Hazards
|
||||
- {{Generated files, migrations, flaky tests, shared ports, secrets, caches.}}
|
||||
- {{Generated or vendored paths that must not be edited.}}
|
||||
|
||||
## Frozen contracts
|
||||
See `.swarm/contracts.md`. Do not change an interface without returning `blocked`.
|
||||
|
||||
## Global forbidden actions
|
||||
- No installs, repo-wide format/lint/codegen, migrations, dev servers, or git writes.
|
||||
- No edits outside your task's write scope.
|
||||
- Do not edit `.swarm/brief.md`, `plan.*`, `contracts.md`, or another task's files or reports.
|
||||
|
||||
## Reporting
|
||||
Write `.swarm/reports/{{ID}}.json` and return the same object. Use `.swarm/report.schema.json`.
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": { "type": "string" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["done", "partial", "blocked", "failed"]
|
||||
},
|
||||
"verdict": {
|
||||
"type": "string",
|
||||
"enum": ["pass", "pass_with_risks", "fail"]
|
||||
},
|
||||
"summary": { "type": "string" },
|
||||
"files_changed": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"acceptance": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"check": { "type": "string" },
|
||||
"result": { "type": "string", "enum": ["pass", "fail", "not_run"] },
|
||||
"evidence": { "type": "string" }
|
||||
},
|
||||
"required": ["check", "result"]
|
||||
}
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"issues": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"assumptions": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"blocked_question": { "type": "string" }
|
||||
},
|
||||
"required": [
|
||||
"task_id",
|
||||
"status",
|
||||
"summary",
|
||||
"files_changed",
|
||||
"evidence",
|
||||
"acceptance"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# {{ID}} — {{title}}
|
||||
|
||||
Role: worker | Wave: {{n}} | Depends on: {{ids or —}} | Effort: {{S/M/L}}
|
||||
|
||||
## Objective
|
||||
{{1–3 sentences. What must exist when done.}}
|
||||
|
||||
## Why
|
||||
{{Which success criterion this serves.}}
|
||||
|
||||
## Inputs (read first)
|
||||
- `.swarm/brief.md`
|
||||
- `.swarm/contracts.md` §{{section}}
|
||||
- `{{path}}:{{start}}-{{end}}`
|
||||
|
||||
## Deliverables
|
||||
- `{{new or changed path}}` — {{what}}
|
||||
- `{{test path}}` — {{what it covers}}
|
||||
|
||||
## Write scope (only these)
|
||||
- `{{glob or exact path}}`
|
||||
|
||||
## Read-only
|
||||
- `{{path}}`
|
||||
|
||||
## Contract
|
||||
{{Exact signature, data shape, CLI behavior, or output format.}}
|
||||
|
||||
## Constraints
|
||||
- {{No new dependencies, style, performance, compatibility.}}
|
||||
|
||||
## Acceptance (run these)
|
||||
1. `{{command}}` → {{expected result}}
|
||||
2. `{{command}}` → {{expected result}}
|
||||
|
||||
## Report
|
||||
`.swarm/reports/{{ID}}.json` per `.swarm/report.schema.json`; status `done|partial|blocked|failed`.
|
||||
|
||||
## Stop conditions
|
||||
- Time box: 25 minutes for S/M, 45–60 minutes for L, or 3 failed attempts at the same step.
|
||||
- A load-bearing unknown, a needed file outside scope, or a broken contract → `blocked` with the exact question. Do not guess.
|
||||
- If a sibling task's in-flight file causes a compile/test failure outside your scope, wait briefly and report `partial` with the exact external error; never edit that file.
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# {{ID}} — verify {{task IDs}}
|
||||
|
||||
You did not write this work. Do not fix it. Do not trust worker summaries.
|
||||
|
||||
## Inputs
|
||||
- Task cards: {{paths}}
|
||||
- Changed files or diff: {{paths or command}}
|
||||
- Acceptance checks: {{commands}}
|
||||
|
||||
## For each criterion
|
||||
| Criterion | Command | Observed | Pass/Fail |
|
||||
|---|---|---|---|
|
||||
| {{criterion}} | `{{command}}` | {{observed}} | {{pass/fail}} |
|
||||
|
||||
## Also check
|
||||
- Scope: every changed file is owned by the task.
|
||||
- Contracts: signatures and data shapes match `.swarm/contracts.md`.
|
||||
- Tests: assert real behavior; no silent skips; no snapshot updates.
|
||||
- Edge cases named in the task card.
|
||||
|
||||
## Freeze
|
||||
The tree is frozen during verification. Do not edit product source, tests, docs or manifests. If you need a gate script that rewrites generated files, copy it/hash-check it instead of changing the tree.
|
||||
|
||||
## Verdict
|
||||
`pass` / `pass_with_risks` / `fail` — with the top risks and exact repro commands.
|
||||
|
||||
## Report
|
||||
FIRST write the full review to `.swarm/reviews/{{ID}}.md`. Then return only the minimal structured verdict:
|
||||
`{task_id, verdict, summary, review_file}` per `.swarm/verdict.schema.json`.
|
||||
The review file is the source of truth; it must exist even if the structured reply is lost. Do not return a long findings array in the workflow schema.
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
// agent-swarm batched worker runner — copy into the `script` field of the workflow tool.
|
||||
// args: {
|
||||
// wave: number,
|
||||
// tasks: [{ id: string, write: string[] }],
|
||||
// schema: object, // paste assets/report.schema.json
|
||||
// concurrency?: number // <= free active-child slots; default 6
|
||||
// }
|
||||
//
|
||||
// The runner starts tasks in chunks instead of one giant fan-out. This matters
|
||||
// because `parallel` is not a queue and the runtime active-child cap is
|
||||
// host-specific; a single spawn-limit throw inside an unbounded fan-out can
|
||||
// abort the whole wave. Each `agent()` call is guarded so a scheduling failure
|
||||
// becomes a structured result for that task, and completed chunks are kept.
|
||||
|
||||
const REPORT = args.schema;
|
||||
if (!REPORT) throw new Error("args.schema is required (paste assets/report.schema.json)");
|
||||
|
||||
const CONCURRENCY = Math.max(
|
||||
1,
|
||||
Math.min(Number(args.concurrency) || 6, 8)
|
||||
);
|
||||
|
||||
const promptFor = (t, attempt) => [
|
||||
`You are agent ${t.id} in a coordinated swarm.`,
|
||||
`Read .swarm/brief.md, .swarm/tasks/${t.id}.md, and .swarm/contracts.md first.`,
|
||||
`Write only within: ${(t.write || []).join(", ") || "(read-only task)"}, plus your own report, review, and scratch under .swarm/reports/, .swarm/reviews/, and .swarm/tmp/${t.id}/.`,
|
||||
`Do not edit other tasks' files or reports; do not run installs, repo-wide format/lint/codegen, migrations, dev servers, or git writes.`,
|
||||
`Run the acceptance checks and record exact commands and results.`,
|
||||
`Write .swarm/reports/${t.id}.json and return the same object.`,
|
||||
`If a load-bearing unknown or a needed file outside scope blocks you, return status "blocked" with blocked_question; do not guess.`,
|
||||
`If you hit a compile/test failure caused by a sibling task's in-flight file, wait briefly and retry; report status "partial" with the exact external error rather than editing outside your scope.`,
|
||||
attempt > 1
|
||||
? `A previous attempt produced no usable report. Re-read the task and fix the cause or report the exact blocker.`
|
||||
: ""
|
||||
].filter(Boolean).join("\n");
|
||||
|
||||
const failed = (t, why, scheduling = false) => ({
|
||||
task_id: t.id,
|
||||
status: "failed",
|
||||
summary: why,
|
||||
files_changed: [],
|
||||
evidence: scheduling ? [`scheduling failure: ${why}`] : [],
|
||||
acceptance: [],
|
||||
issues: [why],
|
||||
assumptions: [],
|
||||
spawn_error: scheduling
|
||||
});
|
||||
|
||||
async function runOne(t) {
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
try {
|
||||
const r = await agent(promptFor(t, attempt), {
|
||||
label: t.id,
|
||||
phase: `wave ${args.wave}`,
|
||||
schema: REPORT
|
||||
});
|
||||
if (r && r.status !== "failed") return r;
|
||||
log(`${t.id}: attempt ${attempt} produced no usable report`);
|
||||
} catch (error) {
|
||||
const message = String(error && error.message ? error.message : error);
|
||||
// A spawn/limit error is a scheduling problem: keep the batch, report it.
|
||||
// Any other throw (bad hook option, unsupported schema) should still fail
|
||||
// loudly so the script is fixed instead of silently swallowing a mistake.
|
||||
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}`, true);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return failed(t, "no usable report after 2 attempts");
|
||||
}
|
||||
|
||||
phase(`wave ${args.wave}`);
|
||||
log(`Dispatching ${args.tasks.length} task(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);
|
||||
log(`Batch ${Math.floor(start / CONCURRENCY) + 1}: ${batch.map((t) => t.id).join(", ")}`);
|
||||
const batchResults = await parallel(batch.map((t) => () => runOne(t)));
|
||||
results.push(...batchResults);
|
||||
}
|
||||
|
||||
const spawnErrors = results.filter((r) => r && r.spawn_error).length;
|
||||
if (spawnErrors > 0) {
|
||||
log(`${spawnErrors} task(s) hit a spawn/scheduling error. Dispatch only the unstarted tasks with a smaller concurrency.`);
|
||||
}
|
||||
|
||||
return {
|
||||
wave: args.wave,
|
||||
concurrency: CONCURRENCY,
|
||||
total: results.length,
|
||||
done: results.filter((r) => r && r.status === "done").length,
|
||||
partial: results.filter((r) => r && r.status === "partial").length,
|
||||
blocked: results.filter((r) => r && r.status === "blocked").length,
|
||||
failed: results.filter((r) => !r || r.status === "failed").length,
|
||||
spawn_errors: spawnErrors,
|
||||
results
|
||||
};
|
||||
Reference in New Issue
Block a user