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
+35
View File
@@ -0,0 +1,35 @@
# Skills
Коллекция скиллов для DeepSeek Harness.
## agent-swarm
Большие задачи, которые делятся на 8+ независимых кусков: миграции, аудиты, research,
многосекционные документы. Лид планирует и интегрирует, воркеры работают по пакетам с
непересекающимися файлами, независимые верификаторы проверяют артефакт, а не отчёт воркера.
- [Описание на русском](agent-swarm/README.ru.md)
- [Инструкции скилла](agent-swarm/SKILL.md)
- [Runbook](agent-swarm/references/playbook.md)
- [Cookbook по `workflow`](agent-swarm/references/orchestration.md)
### Установка
```sh
git clone https://git.shanaty.ru/Emil_Shanaty/skills.git
mkdir -p ~/.dsh/skills
cp -r skills/agent-swarm ~/.dsh/skills/
```
Затем вызовите `/agent-swarm <задача>` или опишите задачу — скилл подключится по описанию.
### Что внутри
```
agent-swarm/
├── SKILL.md # основной протокол
├── README.ru.md # краткое описание
├── references/ # playbook, packets, orchestration
├── assets/ # шаблоны, схемы, workflow-скрипты
└── scripts/audit-scope.mjs # проверка ownership
```
+42
View File
@@ -0,0 +1,42 @@
# agent-swarm
Скилл для больших задач, которые делятся на 8+ независимых кусков: миграции, аудиты, research, документы. Лид (главный агент) планирует, пакует контекст, запускает волны через `workflow`, проверяет и интегрирует. Воркеры не пересекаются по файлам, каждый получает свой пакет и возвращает структурный отчёт.
## Вызов
```
/agent-swarm <задача>
```
или просто опишите задачу — скилл подключится по описанию.
## Что происходит
1. **Fit-check.** Если задача не делится на 8+ независимых юнитов, скилл скажет не использовать swarm.
2. **Recon.** 2–4 агента картографируют репозиторий; лид пишет `.swarm/brief.md`.
3. **План.** Задачи, владельцы файлов, контракты, критерии приёмки. Гейт: без этого волны не стартуют.
4. **Волны.** Воркеры идут батчами по 6–8 (реальный лимит активных детей узнаётся перед запуском и записывается в `state.json`; recon не должен занимать слоты). Затем 4–6 независимых верификаторов на «замороженном» дереве, затем интегратор.
5. **Аудит.** `scripts/audit-scope.mjs` сравнивает изменения с картой владения.
6. **Отчёт.** `.swarm/final-report.md`.
## Артефакты
Всё лежит в `.swarm/` в корне рабочей папки: бриф, план, карточки задач, отчёты, ревью, baseline, финальный отчёт. `.swarm/` можно удалить после задачи или добавить в `.gitignore`.
## Ограничения
- Это инструкция, а не плагин: запуск идёт через инструмент `workflow`.
- Одновременно выполняется столько агентов, сколько разрешает runtime (часто 8, не 14). Лимит проверяется preflight-ом; `wave.workflow.js` запускает задачи батчами, а не одним большим fan-out.
- Дети не общаются между собой и не делегируют дальше; координация — через файлы и лида.
- Верификаторы работают на замороженном дереве; полный разбор пишется в `.swarm/reviews/Vxx.md`, а workflow возвращает только `{task_id, verdict, summary, review_file}`.
- `blocked`-воркер не заменяется: лид правит контракт/shared-файл и продолжает того же агента через `send_message`.
- Стоимость: 30–40 агентов ≈ 0.5–1.5M токенов.
- Для задачи, которую один агент делает за 15 минут, swarm не нужен.
## Настройка
- Шаблоны: `assets/`.
- Подробный runbook: `references/playbook.md`.
- Оркестрация: `references/orchestration.md`.
- Проверка скоупа: `node scripts/audit-scope.mjs --init` перед стартом, затем `node scripts/audit-scope.mjs`.
- Для директорий в `plan.json` используйте glob `path/**`, особенно для ещё не созданных папок.
+105
View File
@@ -0,0 +1,105 @@
---
name: agent-swarm
description: Use for large tasks that split into 8+ independent work units (codebase-wide changes, audits, migrations, multi-source research, multi-section documents) to run dozens of coordinated subagents in waves under one lead, with file ownership, packed context, and independent verification. Not for single-file, sequential, or highly coupled work.
whenToUse: The task is broad, parallelizable, and has executable acceptance checks; one agent would take hours and separate agents can finish units without touching the same files.
---
# Agent Swarm
Run a large task as waves of coordinated subagents. The lead plans and integrates; workers execute one work packet each; verifiers check independently. A swarm is only as fast as its briefs are clear and its write scopes are disjoint.
## Fit check — before spawning anything
Swarm only when all are true:
- The task splits into **8+ independent units** with disjoint write scopes.
- Each unit has an **executable acceptance check** (command, test, or verifiable criterion).
- Wall-clock is dominated by **breadth**, not by one deep sequential chain.
- You can state the interfaces between units **before** dispatch.
Otherwise: one agent for small work; 2–5 `subagent` calls for a few long-lived roles you need to steer. Never swarm one bug, one design decision, or work where every unit edits the same files.
## Roles
- **Lead** — you. Owns the plan, packets, contracts, gates, integration, and the final report. Does not implement during waves.
- **Recon** (2–4) — maps files, commands, conventions, and hazards. Does not design the solution.
- **Worker** — one task card, one write scope, one structured report.
- **Verifier** — fresh context; checks the artifact against acceptance criteria, never the worker's summary.
- **Integrator** (1–2) — owns shared files and cross-cutting fixes after the parallel waves. During a wave, the lead/integrator may make a small contract/shared-file change to unblock a task, then resume that same worker with `send_message`; do not spawn a replacement.
## Protocol
0. **Fit check & preflight** — go/no-go, budget (`max_agents`, `max_waves`), and **discover the real active-child cap** before sizing a wave (see Sizing). Record the cap, current active children, and planned batch size in `state.json`.
1. **Recon** — 2–4 parallel agents return findings with paths and line numbers. Timebox them, merge into `.swarm/brief.md` and `.swarm/map.md`, then stop them; recon must not hold slots during the build wave.
2. **Plan gate** — write `.swarm/plan.json`, `plan.md`, `ownership.md`, `contracts.md`, and one `tasks/Txx.md` per unit. Every success criterion maps to a task and a check. No two tasks write the same path. Include a `LEAD`/`INTEGRATOR` task owning lead-owned shared files, screenshots, licenses and the final report. Freeze interfaces. **Do not dispatch until this gate passes.**
3. **Build wave** — one `workflow` call per build wave, using the batched runner in `assets/wave.workflow.js`; its `concurrency` must fit the available slots (see Sizing). Retry `null`/`failed` once; if the runtime reports an active-child limit, halve the next batch and dispatch the remaining tasks — never relaunch the same oversubscribed fan-out.
4. **Audit** — run `scripts/audit-scope.mjs` from the skill base directory; revert or reassign out-of-scope changes. Run the build/test gate.
5. **Verify wave** — **freeze source changes**, then run 4–6 fresh verifiers by risk cluster. Use `assets/verdict.schema.json` and `assets/verify.workflow.js`; the review file on disk is the source of truth, not the workflow return. Verifiers never fix. If you fix findings, re-run the affected verifier and mark the old review stale.
6. **Integrate** — one integrator for shared files and cross-cutting fixes; re-run the full acceptance suite; then a final verification pass on the integrated build.
7. **Close** — freeze the build and run a final independent review against the original request (not the plan), then write `final-report.md`.
## Hard rules
1. **One writer per path.** A path appears in at most one task's `write` list. Shared files (`package.json`, lockfiles, barrels, schemas, migrations, CI) belong to the integrator only.
2. **Packet, not transcript.** Each worker reads `.swarm/brief.md`, its `tasks/Txx.md`, and `.swarm/contracts.md`. Never fork the lead's conversation for dozens of workers.
3. **Structured reports.** Workers return `{task_id, status, summary, files_changed, evidence, acceptance, issues, assumptions}` and write the same object to `.swarm/reports/Txx.json`. Verifiers write the full review to `.swarm/reviews/Vxx.md` and return the minimal `{task_id, verdict, summary, review_file}`. Prose is not a report.
4. **No self-verification.** The worker's own checks are a smoke test; a different agent verifies the artifact.
5. **No global side effects.** Workers do not run installs, repo-wide format/lint/codegen, migrations, dev servers, or any git write command. They do not edit other tasks' files or reports. The lead owns git and commits only after the final review.
6. **Respect the runtime slot cap.** Active children include recon, verifiers, integrators and any other subagents. A spawn-limit error is a scheduling bug, not a worker failure: shrink the batch, keep completed results, and dispatch the remainder.
7. **Freeze during verification.** Once the verify wave starts, do not edit source/tests/docs except to fix a verified finding. After a fix, re-run the affected verifier; an old review is stale.
8. **Stop rules.** Two waves with no net progress → stop and re-plan. Verification failure rate > 30% → fix briefs/contracts, not agent count. Budget exhausted → report partial and list unverified areas.
## Sizing
| Independent units | Batch size | Batches | Agents | Notes |
|---|---|---|---|---|
| < 8 | — | — | — | Do not swarm |
| 8–11 | 6–8 | 1 + verify | ~12–14 | Small swarm; one batch |
| 12–24 | 6–8 | 2 | ~20 | Default; 2 batches per build wave |
| 25–48 | 6–8 | 3–4 | ~40 | Large; watch the lead's context |
| 49+ | 6–8 | 5+ | 60+ | Only if truly independent; the lead becomes the bottleneck |
**The wave size is not fixed by this table.** The runtime active-child cap is host/policy-specific and may be as low as 8; do not assume 14 or any default. Discover it before dispatch (runtime diagnostics or a tiny trial), subtract active recon/other children, and leave at least one slot free for retries. If the cap is unknown, start with 6 workers. The batched runner in `assets/wave.workflow.js` starts tasks in chunks, so a failure to spawn one chunk does not discard completed work.
## Workspace
```
.swarm/
brief.md shared context: goal, stack, commands, conventions, hazards
map.md repository map from recon: paths, entry points, hazards
contracts.md frozen interfaces between tasks
plan.json tasks, deps, write scopes, waves, status
plan.md human-readable plan + coverage matrix
ownership.md path -> task map
tasks/T01.md one work packet per task
reports/T01.json worker report (worker writes)
reviews/V01.md verifier report (verifier writes)
baseline.json pre-swarm file manifest (`--init`)
report.schema.json report contract for workers
verdict.schema.json minimal contract for verifiers
state.json wave, budget spent, open issues
final-report.md
tmp/T01/ worker scratch, never shared
```
## Coordination loop
Between waves the lead does only four things: **continue**, **answer blocked tasks and retry**, **re-scope**, or **stop**. Read structured reports and review files, not raw logs. Keep `state.json` current, including the real slot cap, retries and re-verifications. The lead is the only agent that talks to the user.
For a `blocked` task: apply the smallest contract/shared-file change, update `contracts.md`, record it in `state.json`, then `send_message` to the **same** worker with the answer and let it continue. Do not spawn a replacement for a worker that only needed a decision.
## References
- `references/playbook.md` — phase-by-phase runbook, gates, checklists, failure recovery.
- `references/packets.md` — how to write briefs, task cards, and verifier cards; examples and common mistakes.
- `references/orchestration.md` — `workflow` cookbook: script patterns, schema subset, retries, background runs, archetypes (code / research / content).
- `assets/` — copyable brief, task, and verifier templates; `report.schema.json`, `verdict.schema.json`; `wave.workflow.js` (batched workers), `verify.workflow.js` (frozen verification).
- `scripts/audit-scope.mjs` — compares changed files to the ownership map.
## Signals that the swarm is failing
- Workers ask the same question or edit the same file → the plan or contracts are underspecified.
- Reports are long on prose and short on evidence → the task card lacks executable checks.
- The lead spends more tokens reading reports than the workers spend working → waves are too large or reports too verbose.
- Verification finds the same class of defect repeatedly → fix the brief, not the worker.
- A workflow ends with `no usable verdict` although reviews exist on disk → the verifier schema is too strict; read the review files and switch to `assets/verdict.schema.json`.
- A wave dies with `active child limit reached` → the batch was sized from the wrong cap; halve it, subtract recon, and dispatch only unstarted tasks.
+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
};
+185
View File
@@ -0,0 +1,185 @@
# Workflow orchestration
The `workflow` tool runs one JavaScript body with top-level `await` and returns a JSON value. It is the only way to fan out dozens of agents without flooding the lead's context.
## Call shape
```json
{
"meta": {"name": "swarm-wave-1", "description": "Build wave", "phases": [{"title": "wave 1"}]},
"script": "<script body>",
"args": {
"wave": 1,
"tasks": [{"id": "T01", "write": ["src/a.ts"]}],
"schema": "<paste the contents of assets/report.schema.json>",
"concurrency": 6
},
"run_in_background": false
}
```
- `meta.phases` is optional; use one phase per wave and `phase()` inside the script.
- `args` is data, never evaluated. The script cannot read files; pass paths and small structured facts.
- Pass `args.schema` as the parsed schema object, not a path or a string.
- Pass `args.concurrency` explicitly. It must be ≤ the free active-child slots; do not rely on the engine to queue.
- The tool blocks until every child settles. Use `run_in_background` for long waves; poll with `job_output`, cancel with `job_kill`.
## agent()
`agent(prompt, opts)` starts one child and resolves with:
- its final text when no schema is passed;
- a validated object when `schema` is passed;
- `null` when the child fails, returns a non-completed stop reason, or produces no structured value.
Options: `label`, `phase`, `schema`, `provider`, `model`. Unknown or deferred options kill the script.
`agent()` can also **throw** when the runtime cannot start a child (for example `active child limit reached`). That throw is not a `null`; if it happens inside `parallel`, it can abort the whole run. Wrap each `agent()` call in `try/catch` inside the batched runner and record a scheduling failure for that task instead of losing the batch.
The child is disposed after the call; there is no follow-up message inside a workflow. Design tasks as one-shot packets.
## parallel vs pipeline
- `parallel([thunks])` — start every thunk together; await all. It is **not a queue**: it does not limit concurrency. Use it only for a chunk whose size fits the free slots.
- `pipeline(items, ...stages)` — per-item chain with no barrier. Use only when each item must pass through stages without waiting for the wave (for example, extract → summarize per source). It multiplies agent count by the number of stages.
- For more items than slots, run batches sequentially and use `parallel` inside each batch (`assets/wave.workflow.js`).
Do not use `parallel` for dependent tasks; use separate waves and let the lead inspect results.
## Schema subset
The engine validates a strict subset. Keep schemas boring:
- Object root (`"type": "object"`).
- `properties`, `required` (names must exist in `properties`), `additionalProperties` boolean.
- Arrays with `items`.
- Strings/numbers/integers/booleans/null, `enum` of scalars, `const`.
- `oneOf` only with exactly-one semantics and no sibling keywords.
- No `minItems`, `maxLength`, `pattern`, `format`, `$ref`, `anyOf`, `allOf`.
A schema miss returns `null`. Verifiers should use the **minimal** schema in `assets/verdict.schema.json`:
`{task_id, verdict, summary, review_file}`. Put the detailed findings in `.swarm/reviews/Vxx.md`; do not make a long findings array required in the workflow schema. If a schema miss happens, read the review file on disk before deciding that the verification failed, then retry with the smaller schema.
## Concurrency and caps
- **Do not hardcode a concurrency default.** The active-child cap is host/policy-specific. Some runtimes allow 14; this environment rejected 8 workers when other children were active. Treat the cap as runtime data.
- **Preflight:** read the runtime diagnostics or `list_agents`; record `slot_cap` and `active_at_dispatch` in `.swarm/state.json`. Recon, verifiers, integrators and any other active children all count.
- **Batch size:** `min(8, slot_cap - active_at_dispatch - 1)`, at least 2. If you do not know the cap, use 6. Leave at least one free slot for retries.
- Each child holds a slot for its whole life. A `parallel` call starts every thunk immediately; if the batch exceeds free slots, the spawns throw.
- Slots are FIFO; a slow task delays later ones. Keep tasks in a batch similar in size.
- `maxTotalAgents`: 1000 per run. `maxItemsPerCall`: 4096 per `parallel`/`pipeline`.
### Batched worker runner
Use `assets/wave.workflow.js`. It splits `args.tasks` into chunks of `args.concurrency`, runs one `parallel` chunk at a time, catches `agent()` throws per task, and returns a structured failure for scheduling errors instead of killing the run. If a chunk reports a limit error, rerun only the unstarted tasks with a smaller concurrency.
## Retry pattern
Full template: `assets/wave.workflow.js`. Retry only when the result is `null` or `status === "failed"`. Do not retry `blocked` — that needs a lead decision. Do not retry a spawn/limit error immediately; shrink the batch or let the lead dispatch the remainder.
```js
async function runOne(t) {
for (let attempt = 1; attempt <= 2; attempt++) {
try {
const r = await agent(promptFor(t, attempt), {
label: t.id,
phase: "wave 1",
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);
if (!/(limit|spawn|could not start|active child|capacity|start a child)/i.test(message)) {
throw error; // invalid options/schema must fail loudly
}
return failed(t, `scheduling error: ${message}`);
}
}
return failed(t);
}
```
On the second attempt, add one line: what failed and what to check. Do not repeat the whole brief. Catch **only** spawn/scheduling failures around `agent()`; invalid hook options or an unsupported schema should still fail loudly so you fix the script.
## Prompt assembly
Build the prompt from `args`, not from prose in the script:
```js
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.`,
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");
```
The task card carries the details; the prompt carries the role, scope, and protocol. Do not duplicate the card in the prompt.
## Verification runner
Use `assets/verify.workflow.js` for the verify wave (after freezing the tree). It embeds the minimal verdict schema and fetches reviews in batches:
```json
{
"script": "<contents of assets/verify.workflow.js>",
"args": {
"wave": 3,
"tasks": [{"id": "V01", "review": ".swarm/reviews/V01.md"}],
"concurrency": 4
},
"run_in_background": true
}
```
The verifier writes the review file first and returns only `{task_id, verdict, summary, review_file}`. If the workflow reports `null`/`no usable verdict`, check the review file; then re-run with the minimal schema. Code is frozen during this wave.
## Background runs
Use `run_in_background: true` when a wave may exceed ~10 minutes or you want live progress in the job list. The call returns a job id; `job_output` with `wait: true` blocks until settlement. If a wave hangs, `job_kill` cancels the run and disposes children. A background run's result arrives once, at settlement.
## Archetypes
### Code
- Units: module/feature/file cluster, each with its own tests.
- Ownership: source + test files per unit; shared types and barrels are integrator-only.
- Acceptance: targeted test command, typecheck, lint on changed files.
- Verify: a fresh agent runs the tests and reads the diff; no self-review.
### Research
- Units: one question or one source cluster per agent; output files under `research/`.
- Ownership: one file per agent; no shared notes file.
- Acceptance: source URLs resolve, claims are dated, every criterion covered.
- Verify: citation check and a consistency pass; a second agent samples claims.
### Content / documents
- Units: one section per agent, plus one editor-integrator.
- Ownership: sections are separate files; the editor owns the assembled document.
- Acceptance: section brief, word budget, terminology list, citations.
- Verify: one editor pass for voice and transitions; a fact-checker for claims.
## Verification runs
Use `assets/verify.workflow.js` with `assets/verdict.schema.json` after freezing the tree. Each verifier:
1. writes its full review to `.swarm/reviews/Vxx.md` first;
2. returns only `{task_id, verdict, summary, review_file}`;
3. reports `pass`, `pass_with_risks`, or `fail` plus evidence in the file.
If the workflow returns `null`/`no usable verdict`, check the review file before treating it as a product failure; then retry with the minimal schema or message the verifier for a concise verdict. After any fix, re-run the affected verifier and mark the old review stale.
## Failure handling
- `null` per item: ordinary child failure; retry or mark failed in the result. Do not fail the whole wave.
- Spawn/limit error inside `agent()`: catch per task, keep completed results, and rerun only unstarted tasks with a smaller batch. This is a scheduling failure, not a worker failure.
- A thrown hook error (bad option, unsupported schema, unknown hook) kills the run: fix the script. Do not hide it in a broad try/catch.
- Cancellation: stop the run, inspect partial reports on disk; do not trust the tool result.
## Cost
Each child carries the system prompt, tool schemas, packet, code reads, and its output: typically 10–40k tokens. A 12-agent wave is roughly 150–400k tokens, but a host capped at 8 cannot run 12 at once; batches reduce peak concurrency, not total cost. Budget before dispatch and count retries/re-verifications; stop at `max_agents`.
+147
View File
@@ -0,0 +1,147 @@
# Work packets
A packet is everything one agent needs and nothing else. The lead writes packets; workers read them. Packets live on disk so the workflow prompt stays short and every worker sees the same facts.
## The context ladder
1. `.swarm/brief.md` — shared by all tasks: goal, success criteria, stack, commands, conventions, hazards, forbidden actions.
2. `.swarm/tasks/Txx.md` — one task: objective, inputs, deliverables, write scope, contract, acceptance, stop conditions.
3. `.swarm/contracts.md` — frozen interfaces; read the relevant section only.
4. Pointers — `path:line` ranges, not pasted code.
5. Examples — one short example only where the format is ambiguous.
If a worker needs the lead's conversation, the packet is incomplete. If a packet exceeds one page, split the task.
## Brief
Copy `assets/brief.md.tmpl`. Rules:
- Goal is one paragraph; success criteria are checkable.
- List the exact build/test/lint commands. Workers should not guess.
- Map only the paths a worker may need; link the rest.
- Hazards: generated files, migrations, flaky tests, shared ports, secrets. Name what not to touch.
- Global forbidden actions are identical in every packet; keep them in the brief to save prompt space.
- No solution design, no task list, no worker-specific details.
## Task card
Copy `assets/task.md.tmpl`. Rules:
- Objective: what must exist when done, in 1–3 sentences.
- Inputs: brief, contract section, and exact code ranges.
- Deliverables: paths, including tests.
- Write scope: globs. Everything else is read-only.
- Contract: exact signature or output shape. If it is not written, it is not frozen.
- Acceptance: 1–3 commands a verifier can re-run. "Code is clean" is not a check.
- Stop conditions: time/attempt limit (25 min for S/M, 45–60 for L) plus "ask, do not guess" for load-bearing unknowns.
- Never include another task's internal details; workers do not need the whole plan.
## Verifier card
Copy `assets/verifier.md.tmpl`. Rules:
- State plainly: you did not write this; do not fix it.
- State that the tree is frozen: no source/test/docs/manifest edits during verification.
- Give criteria, changed paths, and commands; withhold the worker's rationale.
- Require a table: criterion, command, observed, pass/fail.
- Ask for scope, contract, and test-quality checks.
- Require the full review file **first**, then a minimal `{task_id, verdict, summary, review_file}` return (`assets/verdict.schema.json`). The review file is the source of truth; a schema miss must not erase the verification work.
## Report
Copy `assets/report.schema.json` to `.swarm/report.schema.json` and `assets/verdict.schema.json` to `.swarm/verdict.schema.json` during bootstrap; workers/verifiers read those copies. Field discipline:
- `status`: `done` (acceptance met), `partial` (some criteria met, listed), `blocked` (needs a lead decision), `failed` (attempted, cannot proceed).
- `verdict` (verifiers only): `pass`, `pass_with_risks`, or `fail`; the detailed findings belong in the review file, not a required workflow array.
- `files_changed`: exact deliverable/code paths, no globs. Coordination artifacts under `.swarm/` are implicit and may be omitted.
- `evidence`: commands and observed results, one line each.
- `acceptance`: per check, `pass|fail|not_run` plus evidence.
- `assumptions`: load-bearing assumptions the lead must confirm.
- `blocked_question`: one precise question, only when `status=blocked`.
Keep `summary` under ~300 characters. The lead reads summaries first and opens a report only for `blocked`/`failed`.
## Common mistakes
| Mistake | Fix |
|---|---|
| Pasting the whole conversation into every prompt | Write a brief; point to files |
| Vague acceptance ("works well") | A command and an expected result |
| No non-goals | List what not to touch |
| Two tasks own one file | Serialize or merge |
| Task needs 2+ hours | Split by deliverable, not by file count |
| Worker invents an interface | Freeze it in `contracts.md` |
| Report is an essay | Schema + one-line evidence |
| No stop condition | S/M 25 min, L 45–60 min / 3 attempts, then `blocked` |
| Verifier reads the worker's summary | Give the artifact and the criteria only |
| Verifier returns prose and loses its verdict | Write review file first; minimal verdict schema |
| Source edited during verification | Freeze, then re-run the affected verifier |
| Lead re-reads every report | Read summary + evidence; open only exceptions |
## Example: code task
```markdown
# T04 — Add retry to the HTTP client
Role: worker | Wave: 1 | Depends on: T02 | Effort: M
## Objective
`fetchJson` retries idempotent GET requests up to 2 times on 5xx and network errors, with a 100 ms base delay and no retry on 4xx.
## Inputs
- `.swarm/brief.md`
- `.swarm/contracts.md` §HTTP client
- `src/http/client.ts:40-120`
- `src/http/client.test.ts:1-60`
## Deliverables
- `src/http/client.ts` — retry loop
- `src/http/client.test.ts` — 3 cases: 5xx retry, 4xx no retry, network retry
## Write scope
- `src/http/client.ts`
- `src/http/client.test.ts`
## Read-only
- `src/http/types.ts`, `package.json`
## Contract
`fetchJson(url: string, init?: RequestInit): Promise<Json>` — unchanged signature; retries only when `init.method` is absent or `GET`.
## Constraints
- No new dependencies; use the existing `sleep` helper.
- Do not change error types or messages.
## Acceptance
1. `pnpm vitest run src/http/client.test.ts` → all pass.
2. `pnpm tsc --noEmit` → no new errors.
## Stop conditions
25 minutes for S/M (45–60 for L) or 3 failed attempts at the same step; a needed change outside the write scope → `blocked`.
```
## Example: research task
```markdown
# T07 — Compare three vendors on data residency
Role: worker | Wave: 1 | Depends on: — | Effort: S
## Objective
A 400–600 word comparison of Vendor A/B/C on data residency, with a source URL for every factual claim.
## Inputs
- `.swarm/brief.md`
- `.swarm/contracts.md` §Report format
- `research/questions.md` §2
## Deliverables
- `research/vendors/data-residency.md`
## Write scope
- `research/vendors/data-residency.md`
## Acceptance
1. Every factual claim has a source URL that resolves.
2. All three vendors covered on: storage region, subprocessors, deletion policy.
3. No claims older than 18 months without a date.
## Stop conditions
If a vendor publishes nothing on a criterion, write "not disclosed" with the source checked; do not infer.
```
+188
View File
@@ -0,0 +1,188 @@
# Swarm playbook
Detailed runbook for `agent-swarm`. Read this after the fit check passes. Keep `.swarm/state.json` current; it is the resume point if the session restarts.
## 0. Fit check and budget
Answer these before spawning:
- **Units.** Can you list 8+ deliverables that different agents can finish without waiting for each other?
- **Writes.** Can every unit get a disjoint write scope? Name the shared files now.
- **Checks.** Does every unit have a command or criterion a verifier can re-run?
- **Interfaces.** Can you freeze signatures, data shapes, and file names before dispatch?
- **Breadth.** Is the time dominated by many independent pieces, not one deep chain?
Budget:
- `agents ≈ units + ceil(units / 5) verifiers + 1–2 integrators + 2–4 recon`; add retries and re-verifications explicitly.
- Tokens ≈ `agents × 20k` (10–40k per agent depending on code reads).
- Waves ≈ `ceil(units / batch_size)` + verify + integrate, where `batch_size` comes from the **real** active-child cap, not from a documented default.
- Stop at `max_agents` or `max_waves`, whichever comes first. Record both, plus `slot_cap`, `active_at_dispatch`, `batch_size`, retries and re-verifications in `state.json`.
Go: a 40-file migration with per-module tests, a 30-source research report, a 20-section document with a style guide.
No-go: one failing test, one architectural decision, a refactor where every file imports the same core module.
## 1. Recon (2–4 agents)
Give each recon agent one question, not the whole task:
- Structure: entry points, modules, build/test commands.
- Conventions: naming, error handling, test style, formatting.
- Hazards: generated files, migrations, flaky tests, shared caches, secrets.
- Prior work: existing branches, TODOs, partial implementations.
Require findings as `path:line — fact`, no proposals. Merge into `.swarm/brief.md` and `.swarm/map.md`. Cap recon at ~10% of the budget and **10–15 minutes per recon agent**; on a small repo, do it yourself.
Recon lifecycle matters: each recon agent writes its report and stops. It must kill GUI/probe processes it started, must not run the full test suite, and must not keep a slot while the build wave starts. If a recon agent is still active after its report exists, interrupt it; otherwise it silently consumes one of the active-child slots.
## 2. Plan gate
Resolve `<skill>` to the base directory reported by the skill loader. If the workspace is a git repository, add `.swarm/` to `.gitignore` before the baseline so coordination files never reach the diff. Bootstrap once:
```sh
mkdir -p .swarm/tasks .swarm/reports .swarm/reviews .swarm/tmp
cp <skill>/assets/brief.md.tmpl .swarm/brief.md
cp <skill>/assets/report.schema.json .swarm/report.schema.json
cp <skill>/assets/verdict.schema.json .swarm/verdict.schema.json
node <skill>/scripts/audit-scope.mjs --init
```
Write the plan before any worker starts.
**Decomposition.** One task = one deliverable, one writer, one acceptance check, at most one page. Budget **5–25 minutes for S/M** and **40–60 minutes for L**; the old blanket 25-minute rule underestimates large UI/refactor files. If a task is expected to produce a single source file over ~1,500 lines, split it by module or accept that it will be a long, integration-heavy task. If it needs two writers, split it; if it needs the whole repo, it is not a task.
**Ownership.** Every path in exactly one `write` list. Add an explicit `LEAD` task whose `write` list contains lead/integrator-owned shared files, screenshots, licenses and the final report; without it, audit-scope reports the lead's own integration edits as unowned. Shared files are integrator-only: `package.json`, lockfiles, `tsconfig`, CI config, barrel/index files, schemas, migrations, i18n catalogs, snapshots, generated code. If two tasks must touch one file, serialize them with `depends_on` or merge them.
**Contracts.** Freeze what crosses a boundary: function signatures, types, file names, CLI flags, error behavior, output format. Put them in `.swarm/contracts.md`; workers may not change them without returning `blocked`.
**Coverage matrix.** Each success criterion → task(s) → check. A criterion with no task is a hole; a task with no criterion is scope creep.
**Visual acceptance.** For a GUI or visual product, define a deterministic screenshot/artifact command before dispatch (fixed window sizes, output path, clean pointer state) and put it in the acceptance matrix. Do not rely on desktop portal capture; it is unreliable on shared sessions and under Wayland.
**Waves.** Topologically sort by `depends_on`, then balance effort across batches. Do **not** plan 8–12 workers from a documented default: discover the runtime active-child cap first. Compute `batch_size = clamp(slot_cap - active_children - 1, 2, 8)`; recon and any other subagents count as active children. If the cap is unknown, use 6. Put foundational or risky tasks in batch 1; verify after the last build batch.
**Gate checklist:**
- [ ] 8+ tasks, each with objective, inputs, write scope, acceptance, stop condition.
- [ ] No path in two write scopes.
- [ ] Contracts frozen for every cross-task interface.
- [ ] Coverage matrix complete.
- [ ] `slot_cap`, `active_at_dispatch`, and `batch_size` are recorded and `batch_size` fits the available slots.
- [ ] Recon agents have stopped (or have been interrupted); no extra children hold slots.
- [ ] A `LEAD`/`INTEGRATOR` task owns shared files, screenshots, licenses and the final report.
- [ ] For a visual/GUI product, a deterministic screenshot command and sizes are in the acceptance matrix.
- [ ] `.swarm/brief.md`, `plan.json`, `plan.md`, `ownership.md`, `contracts.md`, `tasks/*.md` exist.
- [ ] `audit-scope.mjs --init` recorded the baseline.
If any box is empty, fix the plan; do not dispatch.
Minimal `plan.json` shape:
```json
{
"goal": "one line",
"tasks": [
{
"id": "LEAD",
"title": "lead-owned shared files, screenshots, licenses, final report",
"wave": 0,
"status": "in_progress",
"depends_on": [],
"write": ["package.json", "docs/**", "LICENSE*"],
"read": []
},
{
"id": "T01",
"title": "short title",
"wave": 1,
"status": "pending",
"depends_on": [],
"write": ["src/feature/**"],
"read": ["src/types.ts"],
"acceptance": ["pnpm vitest run src/feature"]
}
]
}
```
`audit-scope.mjs` reads only `id` and `write`. Keep those accurate. Use `path/**` for directories, including directories the task will create.
## 3. Build wave
Dispatch one `workflow` call per build wave (see `references/orchestration.md`). Use `assets/wave.workflow.js`, which splits tasks into batches, catches spawn errors instead of discarding the whole wave, and carries `args.concurrency`.
Before the call:
- [ ] Task cards are on disk and reference real paths.
- [ ] `args.tasks` carries `{id, write}` for each task.
- [ ] The report schema is passed as `args.schema`, and `args.concurrency` is at most the free slot count.
- [ ] Every task has a stop condition.
- [ ] Baseline exists and `state.json` records the cap.
After the call:
- [ ] Triage structured reports: done / partial / blocked / failed.
- [ ] Treat `spawn/runtime error` and `active child limit` results as scheduling failures, not worker failures: keep completed results and dispatch only the unstarted remainder in a smaller batch.
- [ ] Run `node <skill>/scripts/audit-scope.mjs` (the lead runs it; workers do not); revert or reassign out-of-scope changes.
- [ ] Run the build/test gate yourself; do not ask workers to fix shared breakage.
- [ ] Update `plan.json` and `state.json`.
- [ ] Decide: continue, answer blocked, re-scope, or stop.
Do not paste reports into the next prompt. Summarize decisions into the brief or contracts. Open a full report only for `blocked` and `failed`.
### Answering a blocked task
A `blocked` report is a request for a lead decision, not a failure. Fast path:
1. Read the exact `blocked_question` and the relevant contract section.
2. Make the smallest possible shared/contract change yourself (or via the integrator), and update `.swarm/contracts.md` and `state.json`.
3. `send_message` to the **same** worker with the decision and any exact signatures; ask it to continue under the same write scope.
4. Do not spawn a replacement until the original worker cannot continue, and do not retry a blocked task with the same unanswered prompt.
If the block reveals an underspecified interface shared by several tasks, update the contract once and message all affected workers. If two blocked tasks need conflicting contracts, stop and re-plan.
## 4. Verify wave
Choose verifiers by risk cluster, not one per task: 4–6 verifiers for 8–12 tasks. A verifier gets the task cards, the changed files (or diff), and the acceptance commands. It does not get the worker's rationale and does not fix anything.
**Freeze before verify.** Once you start this wave, do not edit source, tests or docs. If you already started and then fixed something, mark the in-flight review stale and re-run that verifier on the new build. A verifier's verdict is only valid for the exact tree it saw.
Use `assets/verify.workflow.js` and `assets/verdict.schema.json`, not the full worker report schema. The verifier's detailed review **must be written to `.swarm/reviews/Vxx.md` before it returns**; the workflow return is only `{task_id, verdict, summary, review_file}`. Do not treat a workflow `null`/`no usable verdict` as a product failure if the review file exists — read the file, then re-run with the minimal schema if needed.
Evidence standard: for every criterion, a command and its observed result. `pass` without evidence is `not_run`. Accept `pass_with_risks` only with named risks and a follow-up task. Verifiers should copy or hash-check generated files rather than rewriting the tree; if a required gate script rewrites generated artifacts, report the hash delta.
On failure, create a fix task with the verifier's repro; do not send the verifier back to fix its own finding. After fixes, re-run the affected verifier and mark the previous review stale; repeat until the remaining findings are minor/documented. Then run a final integrated verification pass.
## 5. Integrate
One integrator (two only for disjoint areas) owns:
- shared files and cross-cutting edits;
- merge conflicts and import/barrel fixes;
- one formatting/lint pass over changed files;
- the full acceptance suite.
No parallel workers during integration. The integrator may read all task cards and reviews; it writes `.swarm/integration.md` with what changed and why. Freeze the tree while the final verification runs; any post-verification edit invalidates the affected evidence.
## 6. Close
Final review is against the **original request**, not the plan: a fresh agent checks the success criteria, the diff, and the evidence. Then the lead writes `.swarm/final-report.md` with these sections:
- Shipped — what exists now, with paths.
- Not shipped / deferred — and why.
- Evidence — commands and observed results.
- Risks and open questions.
- Cost — agents, waves, wall-clock.
- Artifacts — `.swarm/...` paths.
## Failure recovery
| Signal | Likely cause | Action |
|---|---|---|
| Workers edit the same file | ownership map missed a shared file | stop, reassign, serialize, re-audit |
| Many `blocked` on the same interface | contract underspecified | answer once in `contracts.md`, retry those tasks only |
| Many schema `null`s | schema too strict or prompt unclear | loosen schema, add one example to the task card |
| Wave slower than serial | units too small or too coupled | merge units, reduce batch size |
| Workflow dies with `active child limit reached` | batch sized from wrong cap; recon still active | shrink batch, interrupt finished recon, dispatch only unstarted tasks |
| `no usable verdict` but review files exist | verifier schema miss / prose return | read review files; switch to `verdict.schema.json`; re-run if stale |
| Verifier reviewed an older build | source edited during verify | freeze, re-run affected verifier, mark old review stale |
| `blocked` task with a shared-file need | contract omits an integration point | lead applies the change, messages the same worker, no replacement |
| Verifiers find the same defect | brief missing a convention | fix the brief, add a check, re-run affected tasks |
| Lead context filling up | reading full reports | read summary + evidence; open only exceptions |
| A worker hangs | no per-agent timeout exists | kill the job, split the task, add a tighter stop condition |
## Time and cost
Parallel speedup is bounded by the sequential share and by the real slot cap, not by the number of tasks. Push sequential work down: recon once, contracts before dispatch, verification in parallel. A 36-unit task with 10-minute units and a 6-slot batch is ~6 batches × 10 min ≈ 60 min of agent work, plus plan, verify and integration; do not promise a 3× speedup when the host caps active children at 8 or fewer.
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env node
/**
* audit-scope.mjs — compare workspace changes against the .swarm ownership map.
*
* Usage:
* node scripts/audit-scope.mjs --init [--root <dir>]
* node scripts/audit-scope.mjs [--root <dir>] [--json]
*
* --init writes .swarm/baseline.json (run before the first worker starts).
* The audit lists every changed path with its owning task(s) and fails when a
* path is unowned or owned by more than one task.
*
* Exit codes: 0 clean, 1 violations, 2 usage or I/O error.
*/
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readFileSync,
readdirSync,
statSync,
writeFileSync
} from "node:fs";
import { join, relative, resolve, sep } from "node:path";
const IGNORED_DIRS = new Set([
".git",
".swarm",
"node_modules",
"dist",
"build",
".next",
".nuxt",
".venv",
"venv",
"__pycache__",
".cache",
"target",
"coverage",
".idea",
".vscode"
]);
const HASH_LIMIT = 512 * 1024;
function parseArgs(argv) {
const opts = { init: false, json: false, root: process.cwd() };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--init") opts.init = true;
else if (arg === "--json") opts.json = true;
else if (arg === "--root") {
const value = argv[++i];
if (!value) throw new Error("--root requires a directory");
opts.root = resolve(value);
} else if (arg === "-h" || arg === "--help") {
console.log("Usage: node audit-scope.mjs [--init] [--root <dir>] [--json]");
process.exit(0);
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
if (!existsSync(opts.root) || !statSync(opts.root).isDirectory()) {
throw new Error(`root is not a directory: ${opts.root}`);
}
return opts;
}
function walk(dir, base, out) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (IGNORED_DIRS.has(entry.name)) continue;
walk(join(dir, entry.name), base, out);
} else if (entry.isFile()) {
const full = join(dir, entry.name);
const st = statSync(full);
const path = relative(base, full).split(sep).join("/");
const record = { size: st.size, mtime: Math.round(st.mtimeMs) };
if (st.size <= HASH_LIMIT) {
record.hash = createHash("sha1").update(readFileSync(full)).digest("hex").slice(0, 16);
}
out[path] = record;
}
}
}
function snapshot(root) {
const files = {};
walk(root, root, files);
return { version: 1, createdAt: new Date().toISOString(), files };
}
function same(a, b) {
if (!a || !b) return false;
if (a.hash && b.hash) return a.hash === b.hash && a.size === b.size;
return a.size === b.size && a.mtime === b.mtime;
}
function globToRegExp(glob) {
let out = "^";
for (let i = 0; i < glob.length; i++) {
const c = glob[i];
if (c === "*") {
if (glob[i + 1] === "*") {
i++;
if (glob[i + 1] === "/") {
i++;
out += "(?:[^/]+/)*";
} else {
out += ".*";
}
} else {
out += "[^/]*";
}
} else if (c === "?") {
out += "[^/]";
} else {
out += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
}
}
return new RegExp(out + "$");
}
function normalizeGlobs(task, root) {
const globs = Array.isArray(task.write) ? task.write : [];
return globs
.map((raw) => {
let glob = String(raw).trim().replace(/^\.\//, "");
if (!glob) return null;
const abs = resolve(root, glob);
const isDir = !/[*?[]/.test(glob) && existsSync(abs) && statSync(abs).isDirectory();
if (isDir) glob = glob.replace(/\/+$/, "") + "/**";
return { raw, re: globToRegExp(glob) };
})
.filter(Boolean);
}
function main() {
const opts = parseArgs(process.argv.slice(2));
const root = opts.root;
const swarmDir = join(root, ".swarm");
const baselinePath = join(swarmDir, "baseline.json");
const planPath = join(swarmDir, "plan.json");
if (opts.init) {
mkdirSync(swarmDir, { recursive: true });
const snap = snapshot(root);
writeFileSync(baselinePath, JSON.stringify(snap, null, 2) + "\n");
console.log(
`baseline: ${Object.keys(snap.files).length} files -> ${relative(root, baselinePath) || baselinePath}`
);
return 0;
}
if (!existsSync(baselinePath)) {
throw new Error(`missing ${baselinePath}; run with --init before the swarm starts`);
}
if (!existsSync(planPath)) {
throw new Error(`missing ${planPath}; the plan gate must pass before auditing`);
}
const baseline = JSON.parse(readFileSync(baselinePath, "utf8"));
const plan = JSON.parse(readFileSync(planPath, "utf8"));
const current = snapshot(root);
const tasks = (plan.tasks || []).map((task) => ({
id: task.id,
globs: normalizeGlobs(task, root)
}));
const changed = [];
for (const [path, record] of Object.entries(current.files)) {
if (!same(record, baseline.files[path])) changed.push({ path, deleted: false });
}
for (const path of Object.keys(baseline.files)) {
if (!(path in current.files)) changed.push({ path, deleted: true });
}
changed.sort((a, b) => a.path.localeCompare(b.path));
const rows = changed.map(({ path, deleted }) => {
const owners = tasks
.filter((task) => task.globs.some((glob) => glob.re.test(path)))
.map((task) => task.id);
const status = owners.length === 0 ? "unowned" : owners.length > 1 ? "conflict" : "ok";
return { path, deleted, owners, status };
});
const summary = {
changed: rows.length,
ok: rows.filter((row) => row.status === "ok").length,
unowned: rows.filter((row) => row.status === "unowned").length,
conflict: rows.filter((row) => row.status === "conflict").length
};
if (opts.json) {
console.log(JSON.stringify({ summary, changes: rows }, null, 2));
} else {
console.log(
`changed: ${summary.changed} | ok: ${summary.ok} | unowned: ${summary.unowned} | conflict: ${summary.conflict}`
);
for (const row of rows) {
const mark = row.status === "ok" ? "ok " : row.status === "unowned" ? "BAD " : "CONF";
const owners = row.owners.length ? row.owners.join(", ") : "-";
console.log(` ${mark} ${row.path}${row.deleted ? " [deleted]" : ""} ${owners}`);
}
}
return summary.unowned + summary.conflict > 0 ? 1 : 0;
}
try {
process.exitCode = main();
} catch (error) {
console.error(`audit-scope: ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 2;
}