A spec-driven development PM agent that orchestrates multiple coding CLIs using GitHub Issues as state
Project description
ghswarm
A development-PM agent that, on a foundation of spec-driven development, treats GitHub Issues as "state" and orchestrates multiple coding CLIs (Claude Code / Codex / Cursor) with mutual exclusion enforced via labels.
It runs no server (no Webhook/FastAPI); all state is persisted on GitHub (labels + metadata in the Issue body + checkboxes) and in spec files. Even if the process dies, reading the Issue lets it resume from where it left off.
Two-line architecture
flowchart TB
subgraph L1["Line 1 · human in the loop (skill: ghswarm-spec, outside the loop)"]
direction LR
A1[draft in tmp/spec/] --> A2[AI review] --> A3[human review] --> A4[create Issue] --> A5[move to .specs/<br/>+ open draft PR]
end
subgraph L2["Line 2 · ghswarm resident loop (autonomous, this CLI)"]
direction LR
B1[implement on PR branch] --> B2[AI review] --> B3[mark PR ready] --> B4[wait CI/approve] --> B5[auto squash merge]
end
L1 --> L2
- Line 1 is handled by the Claude Code skill
ghswarm-spec. The spec draft is placed intmp/spec/to await human approval; after approval it moves to.specs/YYYY-MM-DD-issue-N.md, is committed on branchissue-N, and opens a draft spec PR along with an Issue carrying a task checklist. - Line 2 is ghswarm itself. It takes over that spec PR's branch, stacks implementation commits onto the same PR, and drives it all the way to auto-merge. Because the spec and the implementation end up in a single PR, it is easy to review.
Design highlights
- Mutual exclusion via labels —
status: busy-<agent>/idle/blocked/completed. Only one CLI runs at a time within the same repository. The lock lives on GitHub, so it is shared across processes. - State persisted in the Issue body — saved as JSON in an HTML comment at the end of the body
(
<!-- GHSWARM_STATE_START ... GHSWARM_STATE_END -->), holdingnext_action/branch_name/spec_path/pr_numberand so on. - Progress via checkboxes —
- [ ] taskitems in the Issue body are worked through and updated to- [x]. Incomplete tasks are handed to a single CLI run all at once (because each CLI is a one-shot headless invocation that loses its context every time, launching one per task would force it to re-read the codebase). Once verify passes, all items are checked. - Spec-driven — the spec file's body is injected into each implement/review prompt. The agent follows the spec.
- CLI/model fixed per phase — the CLI and model used in the
implement/reviewphases are specified explicitly in the config (thecommandunderagents.implement/agents.review). There is no dynamic routing by an LLM. - Self-healing — CLI run → tests → on failure, retry with the log attached (up to N times). If that fails,
git reset --hardrolls back, setsstatus: blocked, and escalates to a human. - Clarification — if the spec is unclear during implementation, the agent writes
.agent_question.mdand exits. ghswarm comments on the Issue, setsstatus: blocked, and once an answer is posted it resumes with--resume. - CI/approve gate → auto-merge → post-merge CI gate — after the PR is created, it polls as
wait_ci. Once all CI succeeds and the PR review is approved (require_approval), it squash-merges. On CI failure it blocks. Whenmergeable=CONFLICTING, it mergesorigin/<base_branch>in the worktree to auto-resolve, then re-runs CI after pushing (can be disabled withauto_resolve_conflicts). After merging it does not close the Issue but proceeds toverify_merge, closing the Issue only when the CI of the merge commit on the merge target branch (base_branch) is green (regression detection; can be disabled withpost_merge_ci). - blocked notifications — when transitioning to
blockedor awaiting clarification, it pushes to a configured Slack webhook or macOS notification (thenotifysection, disabled by default). Consecutive re-transitions for the same reason are suppressed, and a re-block after leaving blocked notifies again. Send failures only produce a warning log; the main process continues.
State machine (Line 2)
stateDiagram-v2
state "wait_for_clarification (status: blocked)" as clarify
state "blocked (human step-in)" as blocked
[*] --> implement
implement --> implement: tasks remain
implement --> ai_review: tasks done
implement --> clarify: spec unclear
ai_review --> create_pr
create_pr --> wait_ci
wait_ci --> verify_merge: CI green + approve
wait_ci --> blocked: CI failed
verify_merge --> done: post-merge CI green
verify_merge --> blocked: post-merge CI failed
clarify --> implement: answer + --resume
done --> [*]
Installation
uv tool install ghswarm # recommended (isolated CLI install)
# or: pipx install ghswarm
# or: pip install ghswarm
From source (for development):
git clone https://github.com/toyama0919/ghswarm && cd ghswarm && uv sync --extra dev
Prerequisites: gh (already gh auth logined), and each coding CLI you use being authenticated and able to run headless.
Claude Code skills
ghswarm ships its user-facing Claude Code skills with the package, so they always match the installed CLI version. Install them into Claude Code with:
ghswarm skills install # -> ~/.claude/skills (available in any repo)
ghswarm skills install --project # -> ./.claude/skills (this repo only)
ghswarm skills install --force # overwrite/update already-installed skills
ghswarm-spec— Line 1: draft a spec, review it, create the Issue and draft spec PR.ghswarm-check— diagnose Issues stuck atstatus: blockedand return safe ones toidle.
Re-run ghswarm skills install --force after upgrading ghswarm to pull in the latest skill versions.
From a checkout you can also install them with the skills CLI:
npx skills add ./ghswarm --skill ghswarm-spec --skill ghswarm-check --agent claude-code
Maintainer-only skills (e.g. ghswarm-release, which publishes ghswarm itself to PyPI) are not
shipped with the package. They live under this repository's .claude/skills/ and are available
automatically as project-scoped skills when working inside the ghswarm repo.
Setup
ghswarm init && $EDITOR ~/.ghswarm.yaml # edit repositories / agents, etc.
Config file lookup order
Only the first file found is used (no merging).
--config <path>(explicit)~/.ghswarm.yaml~/.ghswarm.yml
Config is centralized in ~/.ghswarm.yaml at home. Declare multiple repositories under repositories: as a
mapping keyed by alias, and put shared settings under defaults:. Each repo entry requires
repo (owner/repo) and path (local clone).
max_parallel_repos limits the number of repos run concurrently during loop (default 3).
daemon_log / daemon_pid set the log base path and PID file for daemon residency (defaults are
~/.ghswarm/ghswarm.log / ~/.ghswarm/ghswarm.pid). When started with -d, the start date
-YYYY-MM-DD is inserted just before the log base name's extension (e.g. ~/.ghswarm/ghswarm-2026-07-18.log). No rotation is done.
Event log (for observation)
The result of each step (action / detail) is, by default, also recorded in SQLite at ~/.ghswarm/events.db
(changeable via event_db, empty string disables it). The labels and Issue body on GitHub are the source of truth,
and the event DB is derived data for observation/auditing (losing it does not affect resumability). Do not lean state on the DB.
dry_run and action=skipped (passes such as lock held / waiting on CI) are not recorded. There is no fixed aggregation command
(such as metrics); the intent is to accumulate raw events and later have an AI skill write SQL for ad-hoc analysis.
In v1, ghswarm history only opens a single DB (when -r is unspecified, the event_db of the first repo).
If you split event_db per repo, events from other DBs are not visible from history.
If you place event_db inside the repository, add *.db / *.db-wal / *.db-shm to .gitignore
(not needed under the default ~/.ghswarm/).
Config reference
A list of all keys. For a template, see config.example.yaml (ghswarm init copies it
to ~/.ghswarm.yaml). Within string values, ${VAR} / ${VAR:-default} expand to environment variables
(${VAR} errors if unset, ${VAR:-default} uses default when unset).
The shared settings under defaults: and each repositories.<alias> entry are deep-merged key by key
(nested mappings merge recursively; scalars/lists are replaced by the repo side). The RepoConfig items below can be written
in defaults: as well as in each repo entry.
Top level
| Key | Default | Description |
|---|---|---|
repositories |
(required) | Mapping of repo configs keyed by alias. Select with -r <alias> |
defaults |
{} |
Fallback shared by all repos (takes the RepoConfig items below) |
max_parallel_repos |
3 |
Upper bound on repos run concurrently during loop (1 or more) |
daemon_log |
~/.ghswarm/ghswarm.log |
Log base path for loop -d (start date -YYYY-MM-DD is appended) |
daemon_pid |
~/.ghswarm/ghswarm.pid |
PID file for loop -d |
repositories.<alias> (= RepoConfig; can also go in defaults)
| Key | Default | Description |
|---|---|---|
repo |
(required) | GitHub owner/repo |
path |
(required) | Absolute path to the local clone (~ allowed) |
base_branch |
"" |
The PR's merge target. Work branches are also cut from here. Empty means auto-detect via gh |
branch_prefix |
"issue-" |
Prefix for work branch names (issue-N) |
spec_dir |
".specs" |
Directory where spec files are placed |
test_command |
"" |
Test command run at verify. Empty skips test verification |
poll_interval |
60 |
Polling interval for loop (seconds) |
question_file |
".agent_question.md" |
File the agent writes clarifications to |
merge_method |
"squash" |
squash / merge / rebase |
require_approval |
true |
Whether auto-merge requires a PR review approval |
address_pr_reviews |
true |
Whether to have the review agent address PR review comments (human / bot) |
delete_branch_on_merge |
true |
Whether to delete the work branch after merge |
post_merge_ci |
true |
Whether to confirm the base branch's CI is green after merge before closing the Issue |
post_merge_ci_grace |
180 |
Grace before treating post-merge CI as "no CI" when there are 0 checks (seconds) |
worktree_dir |
"" |
Location for per-issue git worktrees. Empty means ../<repo-name>-worktrees |
worktree_setup |
"" |
Command run once, immediately after a worktree is newly created |
auto_resolve_conflicts |
true |
Whether to merge base to auto-resolve when the PR is CONFLICTING |
conflict_max_retries |
3 |
Retry cap for conflict resolution across loops |
auto_fix_ci |
true |
Whether to fetch logs on PR CI failure and auto-fix in implement |
ci_fix_max_retries |
3 |
Retry cap for CI fixes across loops |
transient_error_patterns |
default pattern set | Regexes treated as transient errors (case-insensitive). resource_exhausted / rate limit / \b(429|503)\b, etc. |
transient_max_retries |
5 |
Retry cap for transient errors across loops |
max_retries |
3 |
Self-healing retry cap within a single run |
issue_max_agent_runs |
10 |
Cumulative agent-run cap per Issue. 0 means unlimited |
lock_ttl |
14400 |
Absolute TTL of the busy lock (seconds). On the same host, pid check takes precedence |
event_db |
~/.ghswarm/events.db |
SQLite path for the structured event log. Empty string disables it |
activity_dir |
~/.ghswarm/activity |
Location for activity files while the daemon runs. Empty string disables it |
env |
{} |
Environment variables injected into gh calls for this repo (~ expansion applies) |
agents |
(required) | Phase → CLI invocation definition (below) |
labels |
defaults below | Status label names |
target |
{} |
Filter conditions for the Issues to pick up |
notify |
all disabled | Push notifications for blocked, etc. |
sandbox |
driver: none |
Docker execution of setup / verify (opt-in) |
agents (required)
The two phases implement / review are required. Each phase is a mapping with a command.
command is a single string, or a fallback chain (a list, with the primary command first).
The {prompt} placeholder is replaced with the shlex.quoted prompt.
agents:
implement:
command:
- "cursor-agent -p {prompt} --model auto"
- "claude -p {prompt} --model opus --dangerously-skip-permissions"
review:
command: "claude -p {prompt} --model sonnet --dangerously-skip-permissions"
labels
| Key | Default |
|---|---|
idle |
"status: idle" |
blocked |
"status: blocked" |
completed |
"status: completed" |
busy_prefix |
"status: busy-" (busy labels are generated dynamically as status: busy-<agent>) |
target (filter for open Issues to pick up)
| Key | Default | Description |
|---|---|---|
labels |
[] |
Required labels (string / list). Narrows targets by labels other than status labels |
assignee |
"" |
Filter by assignee |
milestone |
"" |
Filter by milestone |
notify (push notifications on blocked / awaiting clarification / completion)
| Key | Default | Description |
|---|---|---|
slack_webhook_url |
null |
Slack Incoming Webhook URL. Unset disables Slack |
slack_mention |
null |
Mention string to attach to the Slack body |
macos |
false |
Whether to emit macOS notifications |
on_completed |
true |
Whether to also notify on merge completion |
sandbox (Docker execution of setup / verify, opt-in)
| Key | Default | Description |
|---|---|---|
driver |
"none" |
none / docker |
image |
"" |
Required when driver: docker |
network |
"default" |
default / none |
user |
"auto" |
auto (= $(id -u):$(id -g)) / "1000:1000" / "" (omit --user) |
env |
{} |
Environment variables inside the container (~ expansion applies) |
env_passthrough |
[] |
Names of environment variables passed through from the host (e.g. GH_TOKEN) |
volumes |
[] |
Additional mounts (name:path) |
isolate_dirs |
[] |
Isolate this path inside the worktree via a container-only writable mount (e.g. .venv / node_modules). Relative to the worktree root, no .. |
Usage
# Line 1: prepare the spec and Issue (in Claude Code)
/ghswarm-spec ...request...
# Line 2: ghswarm (cwd is arbitrary; the configured path is the base for git operations)
ghswarm status # Issue state across all repos
ghswarm status -r main # only alias main
ghswarm history # local event log (latest 50)
ghswarm history -r main --issue 42 # filter by repo / Issue
ghswarm run 42 -r main --dry-run # inspect the plan for the next single step
ghswarm run 42 -r main # run Issue #42 to completion
ghswarm run 42 -r main --resume # resume from awaiting-clarification
ghswarm loop # resident parallel polling across all repos
ghswarm loop -d # background residency (does not occupy the terminal)
ghswarm loop --stop # stop the resident daemon
ghswarm loop -r a -r b # only aliases a, b
ghswarm loop --once # one pass over all repos (for cron)
# After starting the daemon, tail the actual log path shown in the banner
# tail -f ~/.ghswarm/ghswarm-2026-07-18.log
# cron example (every 5 minutes): */5 * * * * ghswarm loop --once >> ~/.ghswarm/ghswarm-cron.log 2>&1
Choosing a residency mode: loop -d (daemon residency) and loop --once × cron are mutually exclusive. Use only one.
Daemon logs accumulate one file per start date. Manage size at your discretion with logrotate or similar.
Notes
- Within the same repository, Issues are processed serially (one active at a time). Multiple repositories can run in parallel via
loop. - Each coding CLI's headless auto-approval flag (
--dangerously-skip-permissions, etc.) is set at your own risk. - With
require_approval: true, a PR is not merged until it gets an approval. In setups with no approver, set it tofalse, or gate on an approve from another agent/human. - Place the spec on the work branch
issue-N(= the spec PR's branch), and keep the spec PR as a draft; do not merge it. ghswarm finds and takes over origin's spec PR by branch name. If you merge it first, the spec lands on default, so implementation itself can continue, but the implementation splits off into a separate new PR.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ghswarm-0.2.0.tar.gz.
File metadata
- Download URL: ghswarm-0.2.0.tar.gz
- Upload date:
- Size: 123.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd77e8c65d18c79df2a175842c3c44fdd66dbcd28c3d55c9e89bbba2da4f1743
|
|
| MD5 |
850f9b358835fdd62fcbff5fcb6e9d36
|
|
| BLAKE2b-256 |
bd44f57e790872c25231af263955cff928048949f9671e6ad6073b3994291d3e
|
Provenance
The following attestation bundles were made for ghswarm-0.2.0.tar.gz:
Publisher:
publish.yml on toyama0919/ghswarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ghswarm-0.2.0.tar.gz -
Subject digest:
bd77e8c65d18c79df2a175842c3c44fdd66dbcd28c3d55c9e89bbba2da4f1743 - Sigstore transparency entry: 2240946117
- Sigstore integration time:
-
Permalink:
toyama0919/ghswarm@c65f9f1240844fb41a6bc2f09b04f546774f9794 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/toyama0919
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c65f9f1240844fb41a6bc2f09b04f546774f9794 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ghswarm-0.2.0-py3-none-any.whl.
File metadata
- Download URL: ghswarm-0.2.0-py3-none-any.whl
- Upload date:
- Size: 83.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7fc68f4d756ff57c72d1cf87d32136322771187b4388135fa792f015a111f768
|
|
| MD5 |
2e76c65773e8461e7d6ea7994d6f0b20
|
|
| BLAKE2b-256 |
7b70f6f067839dab0216b9f2ea09166e945ca48df01d2d0eb189a92d91e7151a
|
Provenance
The following attestation bundles were made for ghswarm-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on toyama0919/ghswarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ghswarm-0.2.0-py3-none-any.whl -
Subject digest:
7fc68f4d756ff57c72d1cf87d32136322771187b4388135fa792f015a111f768 - Sigstore transparency entry: 2240946557
- Sigstore integration time:
-
Permalink:
toyama0919/ghswarm@c65f9f1240844fb41a6bc2f09b04f546774f9794 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/toyama0919
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c65f9f1240844fb41a6bc2f09b04f546774f9794 -
Trigger Event:
push
-
Statement type: