Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ or miss one that does. A test fails the build when it drifts.
| `moshcode agents` | engines | list engines, open their agent view, or launch autonomously |
| `moshcode start` | engines | launch an engine with its native defaults |
| `moshcode herd` | runtime | run agent sessions that outlive this terminal |
| `moshcode swarm` | runtime | one task, a herd of agents, one answer — plan, fan out, verify, synthesise (PRD 0015) |
| `moshcode ps` | runtime | list herd sessions and what each one is doing |
| `moshcode cost` <br>`usage` | runtime | what each session is spending, read from the engines' own logs |
| `moshcode attach` | runtime | attach this terminal to a herd session |
Expand Down Expand Up @@ -472,6 +473,35 @@ const first = await herdWait(["api", "web", "docs"], { any: true });
await herdWait(["api", "web"], { states: ["done"] });
```

### Swarm — one task, a herd of agents, one answer

Claude Code calls it ultracode: a prompt that becomes a workflow of agents. The
herd already had every part of that, so this is the verb that composes them,
on any engine moshcode can start:

```sh
moshcode swarm "port the auth routes and the dashboard to the new API"
· plan — claude is splitting the task into up to 4 pieces
1 auth routes
2 dashboard
3 shared API client
· swarm — 3 sessions, 3 at a time (claude, herd swarm)
✓ swarm-port-the-auth-1 idle · t-01
✓ swarm-port-the-auth-2 idle · t-02
✓ swarm-port-the-auth-3 idle · t-03
· synthesis — claude is folding 3 pieces into one answer
```

One headless call splits the task into pieces that do not touch the same
files. Each piece runs in its own herd session, `--agents` of them at a time
(default 4, the same cap the claude engine's defaults put on Claude's own
workflows), prompted and waited on exactly as `herd prompt --wait` is, so every
piece is a task in the ledger. One more call folds the outputs into the answer
you read. `--verify` adds a skeptic per piece whose verdict the synthesis sees;
`--plan-only` shows the split and starts nothing; `--keep` leaves the sessions
in `moshcode ps`. A plan that does not parse runs the task as one piece rather
than not at all.

### Let the engine say what it is doing

Reading a screen works and it rots — engines change their wording between
Expand Down
7 changes: 7 additions & 0 deletions bin/moshcode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,13 @@ async function main() {
process.exitCode = (await herdCommand(rest)) || 0;
return;
}
// A swarm (PRD 0015): the herd's parts, composed. Imported here because a
// plain launch never needs it.
if (cmd === "swarm") {
const { swarmCommand } = await import("../src/swarm.mjs");
process.exitCode = (await swarmCommand(rest)) || 0;
return;
}
if (["ps", "attach", "kill", "wait", "restore", "cost", "usage"].includes(cmd)) {
process.exitCode = (await herdCommand([cmd === "usage" ? "cost" : cmd, ...rest])) || 0;
return;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "moshcode",
"version": "0.96.0",
"version": "0.97.0",
"type": "module",
"description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
"repository": {
Expand Down
88 changes: 88 additions & 0 deletions prd/0015-swarm-one-task-a-herd-of-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
openprd: "0.3"
id: "0015"
title: "Swarm — one task, a herd of agents, one answer"
status: Draft
authors:
- anthony@profullstack.com
created: 2026-09-13
updated: 2026-09-13
repo: https://github.com/moshcoder/moshcode
discussion:
implementation: src/swarm.mjs
tags:
- herd
- agents
- workflow
- ultracode
supersedes:
superseded-by:
---

## Problem

Claude Code has ultracode: put the keyword in a prompt and the prompt becomes a workflow of agents that is planned, fanned out, verified and synthesised. It is the single most useful thing that CLI does at scale, and it is one vendor's.

moshcode's herd is the same idea with the vendor removed. Sessions outlive the terminal, `herd prompt --wait` hands an agent work and blocks until it lands, the task ledger keeps what every session did, and moshscript can already wire a fan-out by hand: `herdStart` three times, `herdPrompt` three times, `herdWait`. Nobody does, because it is a script to write every time, and the pieces that make a swarm worth running, splitting the task so the agents do not collide and folding what they did back into one answer, are the pieces the script does not give you.

So agents at scale exist in moshcode as parts. This PRD is the verb.

## Goals

- One command turns a task into a swarm: `moshcode swarm "<task>"`.
- Any engine moshcode can start can be the swarm, not one vendor. Whatever `ai()` can run headlessly can plan and synthesise; whatever `herd start` can run can be a member.
- The operator sees each phase as it happens, and every piece of work is a task in the ledger they can open afterwards.
- A swarm is bounded by default. Four agents at a time, the same number the claude engine's settings defaults cap Claude's own workflows at, so one swarm cannot eat the box the rest of the herd runs on.
- A swarm degrades rather than fails. A plan that does not parse becomes one piece; a piece whose session never became ready is reported as failed and the synthesis says so; a synthesis that fails still leaves the pieces in the ledger.
- The orchestration is testable without tmux or a model on the box.

## Non-Goals

- A general workflow language. Ultracode's script API (pipeline, barriers, budgets) is out of scope; moshscript already exists for anyone who wants to compose the herd by hand.
- Cross-piece coordination while the swarm runs. Pieces are planned not to touch the same files; if the plan gets that wrong, the synthesis reports the contradiction and the operator resolves it.
- Remote members. A swarm runs on this box's substrate in v1; PRD 0011's remote members are the obvious next step and this design does not preclude them.
- Cost accounting beyond what `moshcode cost` already does per session.

## Users

- An operator with a task too wide for one session who does not want to write the moshscript.
- A herd already running that wants a burst of parallel work without losing track of what each burst did.
- moshcode itself: the swarm is what the pit reaches for when a task is an "all of these at once" task.

## Requirements

R1. `moshcode swarm "<task>"` plans the task, fans it out, and prints one synthesised answer. It works from the CLI, the pit (`/swarm`) and moshscript (`swarm(...)`).

R2. Planning is one headless engine call (`ai()`'s path, `aiExecArgs`). The engine is asked for a JSON array of at most `--agents` pieces, each a self-contained prompt that names the files it may touch and ends with a SUMMARY section. The first JSON array in the reply is the plan. A reply that does not parse, or a call that fails, degrades to one piece holding the whole task, and says so.

R3. Fan-out starts one herd session per piece with `herd start <engine> --agent`, named `swarm-<slug>-<n>`, in the herd `swarm` (`--herd` overrides), at most `--agents` at a time (default 4, maximum 16). Each session is waited on until it draws its prompt, then prompted exactly as `herd prompt --wait` does, so the ledger holds the piece's output.

R4. `--verify` runs one headless skeptic per piece, prompted to refute it and to default to refuted when unsure. Its verdict is attached to the piece and shown to the synthesis; it never drops a piece on its own.

R5. Synthesis is one headless call over the pieces' outputs, truncated per piece, that writes the answer the operator should read: what was done, found, unfinished or contradicted, and what to do next.

R6. Sessions are ended when the swarm is done. `--keep` leaves them for inspection and names them. The ledger is never pruned by a swarm.

R7. `--plan-only` prints the plan and starts nothing. `--json` prints the whole run as data: engine, plan, one row per piece with session, task id, state, outcome, output and verdict, and the synthesis.

R8. Exit codes follow the herd's: 0 when every piece finished and the synthesis was written, 1 otherwise.

R9. The engine, the substrate and the ledger reach the orchestration only through an injectable dependency object, so `test/swarm.test.mjs` exercises planning, throttling, degradation, verification and synthesis with fakes.

R10. `moshcode help swarm`, the README and the pit's `/help` document the verb, and the README command table is regenerated from the schema.

## Design

`src/swarm.mjs`. `parseSwarmArgs` is the flag grammar. `planPrompt`, `verifyPrompt` and `synthesisPrompt` are the three prompts, exported so their wording is testable. `parsePlan` and `parseVerdict` read the model's replies leniently (first array, first object). `throttled` is the concurrency gate. `runHeadless` is the engine call, with the engine's `stripEnv` applied so a swarm started from inside a Claude session does not inherit nested-session markers. `liveDeps` builds the real dependency object out of `herdStart`, `waitFor`, `herdPrompt` and `herdKill` with a capturing writer, and `findTask` for the ledger artifact. `runSwarm` is the four phases as data; `swarmCommand` is the CLI face.

## Open questions

- Whether the pit should offer a keyword trigger the way Claude Code does (a leading `swarm:` on a prompt line). The verb is enough to start with.
- Whether a remote member should be eligible for a piece. Nothing in the design stops it once `herd start` can target one.

## Acceptance

- `moshcode swarm "…" --plan-only` prints a numbered plan and starts nothing.
- `moshcode swarm "…" --agents 2` on a box with tmux and one installed engine ends with a synthesis, two closed tasks in the ledger, and no swarm sessions in `moshcode ps`.
- `moshcode swarm "…" --keep` ends with the sessions still in `moshcode ps`.
- `node --test test/swarm.test.mjs` passes without tmux or an engine.
2 changes: 2 additions & 0 deletions prd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ Start one with `moshcode prd "<idea>"` (TUI: `/prd`).
| [0011](0011-herd-agent-protocol.md) | Teach the herd the agent protocol — hooks-first state, a task ledger, and an A2A surface for local and remote agents | Draft |
| [0012](0012-billing-baked-into-the-agent-cli.md) | Bake billing into the agent CLI — timer, clients, teams, rates, invoices, rails | Draft |
| [0013](0013-persistent-ssh-workspaces.md) | Add persistent SSH workspaces for humans and agents | Draft |
| [0014](0014-remote-mcp-session-gateway.md) | Expose live Moshcode sessions over remote MCP | Draft |
| [0015](0015-swarm-one-task-a-herd-of-agents.md) | Swarm — one task, a herd of agents, one answer | Draft |
<!-- PRD-INDEX:END -->
28 changes: 28 additions & 0 deletions src/cli-schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ export const CORE_CLI_COMMANDS = [
+ "sessions live in a tmux server moshcode owns, or under script(1) when there is no tmux. "
+ "with neither, launches stay in the foreground and say so.",
},
{
name: "swarm",
group: "runtime",
description: "one task, a herd of agents, one answer — plan, fan out, verify, synthesise (PRD 0015)",
synopsis: [["moshcode swarm \"<task>\" [--agents 4] [--engine claude] [--verify] [--plan-only] [--keep]", ""]],
flags: [
["--agents <n>", "sessions at a time, and the most pieces the plan may have", "4"],
["--engine <engine>", "which engine plans, works and synthesises", "the first installed"],
["--cwd <dir>", "where every session works", "."],
["--herd <name>", "the herd the sessions join", "swarm"],
["--verify", "one skeptic per piece, prompted to refute it; the synthesis sees the verdicts", ""],
["--plan-only", "print the plan and start nothing", ""],
["--keep", "leave the sessions running afterwards", ""],
["--timeout <dur>", "how long one piece may take", "30m"],
["--json", "the whole run as data: plan, one row per piece, synthesis", ""],
],
examples: [
["moshcode swarm \"port the auth routes and the dashboard to the new API\"", "planned, run 4 at a time, one answer"],
["moshcode swarm \"audit src/ for unhandled promise rejections\" --agents 8 --verify", "wider, and reviewed"],
["moshcode swarm \"…\" --plan-only", "see how it would split first"],
],
seeAlso: ["herd", "ps", "wait", "run"],
note: "the same thing claude code calls ultracode, on any engine moshcode can start: one headless call splits the task into pieces that "
+ "do not touch the same files, each piece runs in its own herd session (`moshcode herd task <id>` afterwards), and one more call "
+ "folds the outputs into the answer. sessions are ended when it is done unless --keep. a plan that does not parse runs the task as one piece.",
},
{
name: "ps",
group: "runtime",
Expand Down Expand Up @@ -1559,6 +1585,8 @@ export const PIT_COMMANDS = [
description: "sessions that keep running when you leave" },
{ name: "ps", cli: "ps",
description: "what the herd is running, and which one wants you" },
{ name: "swarm", args: "<task> [--agents 4] [--verify]", cli: "swarm",
description: "one task, a herd of agents, one answer" },
{ name: "cost", aliases: ["usage"], args: "[name] [--all]", cli: "cost",
description: "what the herd is spending, from the engines' own logs" },
{ name: "attach", args: "<name>", cli: "attach",
Expand Down
1 change: 1 addition & 0 deletions src/commands.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,7 @@ const COMMANDS = [
cliVerb("agents", "open the native agent view or launch autonomously (moshcode agents <engine>)"),
cliVerb("herd", "drive the herd (moshcode herd <verb>) — see herdStart/herdWait for values"),
cliVerb("ps", "print the herd roster"),
cliVerb("swarm", "one task, a herd of agents, one answer (moshcode swarm \"<task>\" [--agents 4])"),
cliVerb("cost", "print what the herd is spending (moshcode cost [name] [--all])"),
cliVerb("start", "raw-launch an engine (moshcode start <engine>)"),
cliVerb("install", "install an engine or workflow tool"),
Expand Down
30 changes: 26 additions & 4 deletions src/engines.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,37 @@ export const ENGINES = {
"env.CLAUDE_CODE_WORKFLOW_MAX_CONCURRENT_AGENTS": "4 agents at once, hard cap",
},
},
// Dialogs the engine puts up BEFORE any work, and the keys that answer them
// the way an unattended session wants (PRD 0015). The workspace trust
// check is the one that matters: its default is "No, exit", so a bare
// Enter — the reflex answer to "something is blocking" — ends the engine.
// Down, then Enter, picks "Yes, I trust this folder". Matched against the
// screen with ANSI stripped; a swarm answers each once and then waits for
// the prompt.
boot: [
{ pattern: /\bIs this a project you created or one you trust\b/i, keys: ["Down", "Enter"], label: "trust this folder" },
],
state: {
// The permission dialog's own heading, and the selector on its first
// option — the generic numbered-menu pattern would catch the second only
// if the cursor happened to be resting there.
blocked: [/\bdo you want to (?:proceed|make this edit|create)\b/i, /^\s*❯\s*1\.\s*yes/im],
// if the cursor happened to be resting there. The trust check is blocked
// too: it waits on a human exactly as a permission prompt does, and the
// roster read it as "unknown" until it was listed here.
blocked: [
/\bdo you want to (?:proceed|make this edit|create)\b/i,
/^\s*❯\s*1\.\s*yes/im,
/\bIs this a project you created or one you trust\b/i,
],
// Claude Code parks "? for shortcuts" under the composer when it is
// waiting on you and nothing else, which is as close to an explicit
// "idle" as it publishes.
idle: [/\?\s+for shortcuts/i],
// "idle" as it publishes. Matched on its stem: on a narrow pane the
// status line is cut to "? for shortc…" and the member read as unknown.
// 2.1.x with permissions bypassed prints its mode footer there instead
// ("bypass permissions on (shift+tab to cycle)"), and an empty composer
// shows a placeholder ('❯ Try "refactor …"'). Both are checked after the
// shared working rules, so a footer that stays up while the engine
// works cannot outrank "esc to interrupt".
idle: [/\?\s+for shortc/i, /shift\+tab to cycle/i, /^\s*❯\s+Try\s+"/m],
},
},
codex: {
Expand Down
Loading
Loading