Docs / Plugins

🐝 Explore with Swarm (structupath.swarm)

Explore is the suite's ready first workflow. Swarm tries one bounded task several ways in parallel. Each agent gets a Swarm-owned git worktree and branch from a recorded base SHA; agents commit locally and the operator reviews and harvests selected work.

Repo: StructuPath/herdr-swarm · Detailed reference: the repo README

Pinned evidence

Field Value
Plugin release 0.5.0
Minimum Herdr 0.7.4
Explicitly tested Herdr 0.7.4, 0.7.5
Evidence commit f750bc7d61f99d857e2276c5493fbe91254c9e7f

Version 0.5.0 adds the workflow described under Pick the winner below. The pinned readiness record documents a 2026-10-04 live end-to-end run of that flow on Herdr 0.8.2:

That run used scripted stand-in agents, not real model agents. It did not cover the status pane, which opens through the plugin action, or Herdr 0.7.4. The broad tested-version record remains 0.7.4/0.7.5.

An earlier bounded 0.8.2 smoke (0.4.0) exercised fan-out, committed work, merge, automatic archive, abort, and prune dry-run. It found and fixed archive-state reconciliation and the newer runtime's done agent state.

An isolated Herdr 0.9.3/protocol 22 smoke (0.4.0-era) registered all five actions and used a disposable, no-remote repository with an inert local shell preset. Scripted fan-out created one worktree and started one pane; the attended abort closed that pane, removed the clean worktree, archived the run, and kept its branch. Local validation passed 259/259. This does not verify a real model agent, merge/harvest, or GitHub transport on 0.9.3, so the published tested-version record remains 0.7.4/0.7.5.

Actions

Action ID Behavior
structupath.swarm.fanout Prompt for task/slots and create one worktree, branch, and agent per slot
structupath.swarm.status Show each slot's state, branch, and committed/uncommitted counts
structupath.swarm.harvest Review and merge selected slot branches, or explicitly publish a branch for PR review
structupath.swarm.abort Stop agents and remove clean plugin-owned worktrees while keeping branches
structupath.swarm.prune Dry-run, then explicitly prune eligible merged branches or backup refs

Everyday flow: Fan out → Status → Harvest → Prune. Abort is available at any point.

First Explore run

Use a clean Git repository and choose a task small enough to review. The operator—not an agent's displayed state—is the merge gate.

1. Install and prove registration

Requirements: Herdr >=0.7.4, Git >=2.38 recommended, Node.js >=20, and macOS or Linux.

herdr plugin install StructuPath/herdr-swarm
herdr plugin list
herdr plugin action list

Ready branch: structupath.swarm is enabled and the five action IDs in the table above are visible.

Failure branch: do not fan out if the plugin or actions are missing. Verify the requirements, then inspect:

herdr plugin log list --plugin structupath.swarm
herdr status server

2. Open Fan out

Choose Fan out agents from Herdr's plugin actions menu. Herdr plugins cannot ship default keybindings; this optional user-owned binding opens the same action:

# ~/.config/herdr/config.toml
[[keys.command]]
key = "prefix+s"
type = "plugin_action"
command = "structupath.swarm.fanout"
description = "swarm fan-out"

3. Enter one bounded task and comparison criterion

Start with two or three slots. In the interactive prompt, finish the task with a single . on its own line. For example:

Add retry with exponential backoff to the HTTP client.
Keep the public API unchanged.
Add a test for two failures followed by success.
Run the existing test suite.

Comparison criterion: among passing candidates, choose the smallest diff
with the clearest failure-path test.
.

Fresh worktrees omit ignored dependencies, .env files, and caches. If every agent fails immediately, add a reviewed plugin-config setup.sh or tell agents to install required dependencies, then start a new run.

Setup logs preserve command output verbatim. Keep credentials out of setup output and review logs before sharing them.

4. Inspect every credible candidate

Open Status and use 1–9 to visit each slot. Compare committed and uncommitted counts against the recorded fork SHA. While Status is open, finish detection marks a slot finished when its agent writes the .swarm-done marker or exits, and shows elapsed time per slot. State is informative, not a completion gate.

Open Harvest. Press w for the ranked compare view, v to run your validate.sh on a slot, or preview each candidate's diff, run its tests, and compare it against the criterion declared before fan-out. Dirty slots offer WIP commit, skip, or discard; review the backup behavior before discarding.

5. Treat clean-slot selection as approval

Merge warning: selecting a clean slot in Harvest performs a review-first --no-ff merge. If the base branch is checked out, Swarm asks for explicit confirmation. If the base is not checked out anywhere, a plugin-owned detached worktree can advance the base ref after the preview without another confirmation screen. Treat slot selection as the approval action.

Use q to leave Status or Harvest without selecting a candidate. Invoke Abort to stop an active run: it closes Swarm-owned panes and removes clean plugin-owned worktrees, while keeping branches and anything questionable.

Success definition

A first Explore run is successful when one chosen branch is merged without conflict, its tests pass on the base, the resulting diff meets the stated comparison criterion, and skipped work remains recoverable on branches until deliberate pruning.

Pick the winner

Version 0.5.0 covers the whole path from fan-out to a landed winner. Each step is opt-in or read-only unless stated, and nothing merges without the operator's selection or confirmation. The pinned README is the detailed reference.

Step What it does
Prepare slots Fan-out clones the repository's ignored node_modules (or a configured clone-paths list) into each worktree with copy-on-write. On Linux it's skipped where copy-on-write is unavailable; on macOS cp -c falls back to a full copy. Each slot gets HERDR_SWARM_RUN_ID, HERDR_SWARM_SLOT, and its own port range (10 per slot from 4100). setup.sh always receives them, and the agent receives them on Herdr 0.7.5+.
Different tasks per slot <!-- swarm-slot: N --> lines split HERDR_SWARM_TASK_FILE into per-slot sections after a shared preamble. Before anything is created, fan-out warns when slots given different tasks name the same tracked file.
Know when done Status records a slot finished when its agent writes .swarm-done or exits, and sends one Herdr notification when all slots are done.
Check v (or harvest-step.sh validate <slot>) runs your plugin-config validate.sh against the slot's clean commit and records the result bound to that SHA. It can run automatically on finish if you opt in. The strict candidate handoff uses this result by default.
Compare w ranks slots by checks on their current commit, then commits, then finished. It never ranks by diff size. It shows files, +/-, dirty count, the files each pair both changed, and elapsed time. d shows the diff between two slots.
Merge the winner m merges the chosen slot at the exact commit compared, and refuses if it moved. Only if that merge lands are the finished losers skipped and archived. Branches are always kept.
Steer harvest-step.sh broadcast types one single-line message into every running slot's agent. It only does so where the slot's own agent program leads the pane, never into a shell, an editor, or an agent Herdr reports as blocked.
Resolve conflicts For a conflict in Swarm's own detached merge tree, g starts a resolver agent beside the slot (Herdr 0.7.5+). c reviews its commit read-only: it lists every path changed beyond git's own automatic merge and the full diffstat. Only y records exactly the reviewed commit and lands it through the compare-and-swap. A merge you resolve by hand concludes the same way.

Elapsed time is shown; token usage and cost aren't, because Herdr carries no usage data and agents do not report it.

Publish for PR review

In Harvest, press p, then select a slot to publish its branch. The script equivalent is scripts/harvest-step.sh publish <slot> from the installed plugin root. Publication uses an ordinary push to origin, or the explicit HERDR_SWARM_PUBLISH_REMOTE override; it never force-pushes and does not create a GitHub PR itself.

Explicit GitHub draft handoff

For a GitHub PR, press g, then select a slot in Harvest, or invoke publish-pr below. This selection or command authorizes both the audited-commit push and draft PR creation, with no second confirmation. It creates a draft or reuses the exact matching open PR; an existing ready-for-review PR stays ready. The five plugin actions remain unchanged.

From the trusted installed Swarm checkout in the same configured run context:

# Optional evidence must describe this slot's exact committed candidate.
HERDR_SWARM_VALIDATION_FILE=/absolute/private/qa/result.json \
  bash scripts/harvest-step.sh publish-pr 1

# Read GitHub PR/CI state without pushing or editing GitHub.
bash scripts/harvest-step.sh pr-status 1

Authenticate gh first. The named remote (default origin) must have exactly one fetch and push URL identifying the same GitHub.com repository. Fork destinations and GitHub Enterprise are outside this first release. The recorded run base branch and selected slot branch determine PR identity. Existing ownership, fork ancestry, non-empty-work, and ordinary-push guards remain active. Only committed work travels.

HERDR_SWARM_VALIDATION_FILE optionally accepts typed check statuses or a Browser QA result.json for the exact published SHA. Browser evidence must explicitly record a clean tree, no changes during the run, and all three scenario policies enabled: failOnConsoleError, failOnPageError, and failOnFailedRequest. Without a file, validation is not_run, never passed. Supplied evidence is unauthenticated; Swarm does not execute its checks. Failed or pending checks can accompany a draft. The PR includes bounded check names/statuses and identifiers, not task text, raw logs, URLs, screenshots, or local paths.

An existing exact repository/head/base open PR is reused without changing its title, body, or state. validation_attached: false on reuse means its body may describe an older SHA; review and update that evidence manually. Closed/merged matches and ambiguous discovery refuse handoff.

Imported Browser QA summaries must agree with per-run outcomes and counts. Unresolved requests or incomplete telemetry cannot produce a passing handoff check.

A failed push creates no PR. A GitHub failure after a successful push leaves the published branch and local receipt intact. Inspect the error and retry the same command; a lost creation response triggers exact-PR rediscovery. A mismatched GitHub head refuses success and requires inspection, never a force-push correction.

Press c, then a slot, for the pane's CI view. pr-status emits a ci_status tab-separated JSON record with passed, failed, pending, not_run, unknown, or no_pr status. Compare head_sha and local_head_sha: a passing remote check is not current candidate evidence when matches_local_head is false. Passing checks do not establish branch-protection completeness or authorize a merge. The command uses GitHub reads and the local run lock; it does not fetch, push, merge, or edit a PR.

See the pinned GitHub handoff contract for the exact evidence and output schemas. Console can display an explicitly saved PR-status observation alongside project and QA evidence.

Strict candidate handoff

Version 0.5.0 includes candidate-status <slot> and publish-candidate-pr <slot>. In the active run, select a clean committed slot, record typed passing validation checks (the result validate <slot> records is used when no file is named), a separately passing Browser QA result for the same SHA, and an explicit operator review bound to the run ID, slot, and SHA. Set HERDR_SWARM_CANDIDATE_VALIDATION_FILE, HERDR_SWARM_CANDIDATE_BROWSER_QA_FILE, and HERDR_SWARM_CANDIDATE_REVIEW_FILE to regular private files. The review JSON has schema_version: 1, kind: "herdr-swarm-operator-review", run_id, numeric slot, head_sha, and decision: "approved" or "rejected". Run bash scripts/harvest-step.sh candidate-status 1, optionally save its candidate_status TSV output for Console, then, after operator inspection, run bash scripts/harvest-step.sh publish-candidate-pr 1. The publish verb rereads every input and refuses missing, stale, failed, rejected, invalid, or dirty candidates before a push or PR creation. It never merges or authorizes Conductor apply.

The disposable live-CLI smoke observed a blocked preview and exit 36 without a Git push or gh, then a ready preview, audited push to a local bare Git repository, and a draft-PR result from a stubbed GitHub transport. It does not establish real GitHub authentication, CI, or remote PR creation. Caller-supplied evidence and the operator JSON are not attestations; an existing PR is reused without replacing its body, so reported evidence may not be attached to that PR. The pinned docs/github-handoff.md linked above defines the contract.

After a remote PR is merged, preview again. Harvest recognizes merge ancestry and squash merges whose tree changes are contained in the base. Prune uses ancestry, so squash-merged branches may remain for deliberate review.

Safety and trust boundary

Swarm isolates working trees and provides reviewable branches; it does not sandbox agents. Write agents are trusted same-user principals and can access anything their operating-system user can access. Agents are instructed to commit locally and never push, while the operator remains the merge gate.

Harvest uses review-first, --no-ff merges and checks base drift. Dirty discards first create backup refs. Abort keeps branches, and prune is dry-run/env-gated. Active runs resolve by physical repository identity; conflicting generations are refused rather than guessed.

Ignored files remain outside Git's safety net. Removal requires the exact one-use cleanup_approval JSON returned by the inventory preview, bound to the resource, path, run, generation, and inventory digest. A generic yes is insufficient, and changed inventory invalidates approval. This also applies to ignored files in detached merge worktrees, and to node_modules cloned into a slot. Large sets are summarized by top-level directory in the prompt, while the approval still covers every exact file.

Broadcast types text into agents, so it refuses multi-line text, control characters, and a leading -. It only types into a pane whose foreground program is the slot's own agent. On Herdr 0.7.5+, Swarm reports agent state itself, so an open approval prompt does not read as blocked: do not broadcast while an agent may be asking for approval.

The conflict resolver never handles a conflict in your own checked-out branch. Its merge tree is kept until Swarm can prove the resolver is gone; an uncertain state blocks abort and conclude. A resolution's second parent must be the exact slot commit the merge started with. Hunk-level changes inside conflicted files are not flagged separately, so review the conflicted files' content.

Scripted fan-out

Interactive fan-out uses the action. A zero-TTY run must invoke the pane script directly because action-spawned panes do not inherit caller environment in the pinned runtime:

HERDR_SWARM_SLOTS=3 \
HERDR_SWARM_PRESETS=claude,claude,codex \
HERDR_SWARM_TASK_FILE=/path/brief.md \
HERDR_SWARM_DETRITUS=rename \
HERDR_WORKSPACE_ID='replace-with-repo-workspace-id' \
  bash "$(herdr plugin list --json | jq -r '.plugins[] | select(.id=="structupath.swarm").root')/scripts/fanout-pane.sh"

For a different task per slot, split the task file with <!-- swarm-slot: N --> lines (see Pick the winner above). Without HERDR_SWARM_SLOTS, the highest section number sets the slot count. Sections that don't fit the run, or a near-miss marker, refuse before anything is created. If a zero-TTY input is missing, the script exits with a named-variable error instead of waiting on an unreadable prompt.

Safe exit and cleanup

Use the plugin README's Safety model and Uninstall / cleanup sections before destructive cleanup.