Add agent-swarm skill

This commit is contained in:
Emil Shanaty
2026-10-27 18:50:15 +03:00
commit 54a5c849d4
14 changed files with 1285 additions and 0 deletions
+45
View File
@@ -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`.
+52
View File
@@ -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"
]
}
+42
View File
@@ -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.
+10
View 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"]
}
+30
View 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.
+88
View File
@@ -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
};
+100
View File
@@ -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
};