ghswarm 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. ghswarm-0.1.0/LICENSE +21 -0
  2. ghswarm-0.1.0/PKG-INFO +298 -0
  3. ghswarm-0.1.0/README.md +280 -0
  4. ghswarm-0.1.0/pyproject.toml +52 -0
  5. ghswarm-0.1.0/setup.cfg +4 -0
  6. ghswarm-0.1.0/src/ghswarm/__init__.py +8 -0
  7. ghswarm-0.1.0/src/ghswarm/__main__.py +6 -0
  8. ghswarm-0.1.0/src/ghswarm/activity.py +137 -0
  9. ghswarm-0.1.0/src/ghswarm/cli.py +798 -0
  10. ghswarm-0.1.0/src/ghswarm/config.example.yaml +94 -0
  11. ghswarm-0.1.0/src/ghswarm/config.py +658 -0
  12. ghswarm-0.1.0/src/ghswarm/daemon.py +139 -0
  13. ghswarm-0.1.0/src/ghswarm/events.py +138 -0
  14. ghswarm-0.1.0/src/ghswarm/executor.py +309 -0
  15. ghswarm-0.1.0/src/ghswarm/git_ops.py +239 -0
  16. ghswarm-0.1.0/src/ghswarm/github.py +566 -0
  17. ghswarm-0.1.0/src/ghswarm/labels.py +137 -0
  18. ghswarm-0.1.0/src/ghswarm/logging_utils.py +40 -0
  19. ghswarm-0.1.0/src/ghswarm/notify.py +116 -0
  20. ghswarm-0.1.0/src/ghswarm/orchestrator.py +1149 -0
  21. ghswarm-0.1.0/src/ghswarm/questions.py +46 -0
  22. ghswarm-0.1.0/src/ghswarm/sandbox.py +190 -0
  23. ghswarm-0.1.0/src/ghswarm/spec.py +56 -0
  24. ghswarm-0.1.0/src/ghswarm/state.py +156 -0
  25. ghswarm-0.1.0/src/ghswarm.egg-info/PKG-INFO +298 -0
  26. ghswarm-0.1.0/src/ghswarm.egg-info/SOURCES.txt +53 -0
  27. ghswarm-0.1.0/src/ghswarm.egg-info/dependency_links.txt +1 -0
  28. ghswarm-0.1.0/src/ghswarm.egg-info/entry_points.txt +2 -0
  29. ghswarm-0.1.0/src/ghswarm.egg-info/requires.txt +5 -0
  30. ghswarm-0.1.0/src/ghswarm.egg-info/top_level.txt +1 -0
  31. ghswarm-0.1.0/tests/test_activity.py +41 -0
  32. ghswarm-0.1.0/tests/test_cli.py +1404 -0
  33. ghswarm-0.1.0/tests/test_cli_init.py +70 -0
  34. ghswarm-0.1.0/tests/test_config.py +1092 -0
  35. ghswarm-0.1.0/tests/test_daemon.py +150 -0
  36. ghswarm-0.1.0/tests/test_events.py +102 -0
  37. ghswarm-0.1.0/tests/test_executor.py +654 -0
  38. ghswarm-0.1.0/tests/test_git_ops.py +298 -0
  39. ghswarm-0.1.0/tests/test_github.py +360 -0
  40. ghswarm-0.1.0/tests/test_implement_batch.py +249 -0
  41. ghswarm-0.1.0/tests/test_labels.py +160 -0
  42. ghswarm-0.1.0/tests/test_notify.py +480 -0
  43. ghswarm-0.1.0/tests/test_orchestrator_budget.py +247 -0
  44. ghswarm-0.1.0/tests/test_orchestrator_ci_fix.py +214 -0
  45. ghswarm-0.1.0/tests/test_orchestrator_conflict.py +205 -0
  46. ghswarm-0.1.0/tests/test_orchestrator_events.py +137 -0
  47. ghswarm-0.1.0/tests/test_orchestrator_git_error.py +172 -0
  48. ghswarm-0.1.0/tests/test_orchestrator_lock.py +165 -0
  49. ghswarm-0.1.0/tests/test_orchestrator_pr.py +150 -0
  50. ghswarm-0.1.0/tests/test_orchestrator_transient.py +195 -0
  51. ghswarm-0.1.0/tests/test_review_address.py +346 -0
  52. ghswarm-0.1.0/tests/test_sandbox.py +319 -0
  53. ghswarm-0.1.0/tests/test_spec.py +80 -0
  54. ghswarm-0.1.0/tests/test_state.py +129 -0
  55. ghswarm-0.1.0/tests/test_verify_merge.py +197 -0
ghswarm-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hiroshi Toyama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ghswarm-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghswarm
3
+ Version: 0.1.0
4
+ Summary: A spec-driven development PM agent that orchestrates multiple coding CLIs using GitHub Issues as state
5
+ Author: Hiroshi Toyama
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/toyama0919/ghswarm
8
+ Project-URL: Repository, https://github.com/toyama0919/ghswarm
9
+ Project-URL: Issues, https://github.com/toyama0919/ghswarm/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: PyYAML>=6.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.0; extra == "dev"
16
+ Requires-Dist: ruff>=0.8; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # ghswarm
20
+
21
+ [![CI](https://github.com/toyama0919/ghswarm/actions/workflows/ci.yml/badge.svg)](https://github.com/toyama0919/ghswarm/actions/workflows/ci.yml)
22
+ [![PyPI](https://img.shields.io/pypi/v/ghswarm.svg)](https://pypi.org/project/ghswarm/)
23
+ [![Python versions](https://img.shields.io/pypi/pyversions/ghswarm.svg)](https://pypi.org/project/ghswarm/)
24
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
25
+
26
+ A development-PM agent that, on a foundation of **spec-driven development**, treats GitHub Issues as "state"
27
+ and **orchestrates multiple coding CLIs** (Claude Code / Codex / Cursor)
28
+ **with mutual exclusion enforced via labels**.
29
+
30
+ It runs no server (no Webhook/FastAPI); all state is persisted on GitHub (labels + metadata in the Issue body +
31
+ checkboxes) and in spec files. Even if the process dies, reading the Issue lets it **resume from where it left off**.
32
+
33
+ ## Two-line architecture
34
+
35
+ ```mermaid
36
+ flowchart TB
37
+ subgraph L1["Line 1 · human in the loop (skill: ghswarm-spec, outside the loop)"]
38
+ direction LR
39
+ A1[draft in tmp/spec/] --> A2[AI review] --> A3[human review] --> A4[create Issue] --> A5[move to .specs/<br/>+ open draft PR]
40
+ end
41
+ subgraph L2["Line 2 · ghswarm resident loop (autonomous, this CLI)"]
42
+ direction LR
43
+ B1[implement on PR branch] --> B2[AI review] --> B3[mark PR ready] --> B4[wait CI/approve] --> B5[auto squash merge]
44
+ end
45
+ L1 --> L2
46
+ ```
47
+
48
+ - **Line 1** is handled by the Claude Code skill [`ghswarm-spec`](.claude/skills/ghswarm-spec/SKILL.md).
49
+ The spec draft is placed in `tmp/spec/` to await human approval; after approval it moves to `.specs/YYYY-MM-DD-issue-N.md`,
50
+ is committed on branch `issue-N`, and opens a **draft spec PR** along with an Issue carrying a task checklist.
51
+ - **Line 2** is ghswarm itself. It takes over that spec PR's branch, **stacks implementation commits onto the same PR**,
52
+ 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.
53
+
54
+ ## Design highlights
55
+
56
+ - **Mutual exclusion via labels** — `status: busy-<agent>` / `idle` / `blocked` / `completed`.
57
+ Only one CLI runs at a time within the same repository. The lock lives on GitHub, so it is shared across processes.
58
+ - **State persisted in the Issue body** — saved as JSON in an HTML comment at the end of the body
59
+ (`<!-- GHSWARM_STATE_START ... GHSWARM_STATE_END -->`), holding `next_action` / `branch_name` / `spec_path` / `pr_number` and so on.
60
+ - **Progress via checkboxes** — `- [ ] task` items in the Issue body are worked through and updated to `- [x]`. Incomplete tasks are
61
+ **handed to a single CLI run all at once** (because each CLI is a one-shot headless invocation that loses its context every time,
62
+ launching one per task would force it to re-read the codebase). Once verify passes, all items are checked.
63
+ - **Spec-driven** — the spec file's body is injected into each implement/review prompt. The agent follows the spec.
64
+ - **CLI/model fixed per phase** — the CLI and model used in the `implement` / `review` phases are specified explicitly
65
+ in the config (the `command` under `agents.implement` / `agents.review`). There is no dynamic routing by an LLM.
66
+ - **Self-healing** — CLI run → tests → on failure, retry with the log attached (up to N times). If that fails, `git reset --hard`
67
+ rolls back, sets `status: blocked`, and escalates to a human.
68
+ - **Clarification** — if the spec is unclear during implementation, the agent writes `.agent_question.md` and exits. ghswarm comments on the Issue,
69
+ sets `status: blocked`, and once an answer is posted it resumes with `--resume`.
70
+ - **CI/approve gate → auto-merge → post-merge CI gate** — after the PR is created, it polls as `wait_ci`.
71
+ Once all CI succeeds and the PR review is approved (`require_approval`), it squash-merges. On CI failure it blocks.
72
+ When `mergeable=CONFLICTING`, it merges `origin/<base_branch>` in the worktree to auto-resolve, then
73
+ re-runs CI after pushing (can be disabled with `auto_resolve_conflicts`).
74
+ After merging it does not close the Issue but proceeds to `verify_merge`, closing the Issue only when the CI of the merge
75
+ commit on the merge target branch (`base_branch`) is green (regression detection; can be disabled with `post_merge_ci`).
76
+ - **blocked notifications** — when transitioning to `blocked` or awaiting clarification, it pushes to a configured Slack webhook or
77
+ macOS notification (the `notify` section, disabled by default). Consecutive re-transitions for the same reason are suppressed, and
78
+ a re-block after leaving blocked notifies again. Send failures only produce a warning log; the main process continues.
79
+
80
+ ## State machine (Line 2)
81
+
82
+ ```mermaid
83
+ stateDiagram-v2
84
+ state "wait_for_clarification (status: blocked)" as clarify
85
+ state "blocked (human step-in)" as blocked
86
+
87
+ [*] --> implement
88
+ implement --> implement: tasks remain
89
+ implement --> ai_review: tasks done
90
+ implement --> clarify: spec unclear
91
+ ai_review --> create_pr
92
+ create_pr --> wait_ci
93
+ wait_ci --> verify_merge: CI green + approve
94
+ wait_ci --> blocked: CI failed
95
+ verify_merge --> done: post-merge CI green
96
+ verify_merge --> blocked: post-merge CI failed
97
+ clarify --> implement: answer + --resume
98
+ done --> [*]
99
+ ```
100
+
101
+ ## Installation
102
+
103
+ ```bash
104
+ cd ghswarm && pip install -e .
105
+ ```
106
+
107
+ Prerequisites: `gh` (already `gh auth login`ed), and each coding CLI you use being authenticated and able to run headless.
108
+
109
+ ## Setup
110
+
111
+ ```bash
112
+ ghswarm init && $EDITOR ~/.ghswarm.yaml # edit repositories / agents, etc.
113
+ ```
114
+
115
+ ### Config file lookup order
116
+
117
+ Only the first file found is used (no merging).
118
+
119
+ 1. `--config <path>` (explicit)
120
+ 2. `~/.ghswarm.yaml`
121
+ 3. `~/.ghswarm.yml`
122
+
123
+ Config is **centralized in `~/.ghswarm.yaml` at home**. Declare multiple repositories under `repositories:` as a
124
+ mapping keyed by alias, and put shared settings under `defaults:`. Each repo entry requires
125
+ `repo` (owner/repo) and `path` (local clone).
126
+
127
+ `max_parallel_repos` limits the number of repos run concurrently during `loop` (default 3).
128
+
129
+ `daemon_log` / `daemon_pid` set the log base path and PID file for daemon residency (defaults are
130
+ `~/.ghswarm/ghswarm.log` / `~/.ghswarm/ghswarm.pid`). When started with `-d`, the start date
131
+ `-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.
132
+
133
+ ### Event log (for observation)
134
+
135
+ The result of each step (`action` / `detail`) is, by default, also recorded in SQLite at `~/.ghswarm/events.db`
136
+ (changeable via `event_db`, empty string disables it). **The labels and Issue body on GitHub are the source of truth**,
137
+ and the event DB is derived data for observation/auditing (losing it does not affect resumability). Do not lean state on the DB.
138
+
139
+ `dry_run` and `action=skipped` (passes such as lock held / waiting on CI) are not recorded. There is no fixed aggregation command
140
+ (such as `metrics`); the intent is to accumulate raw events and later have an AI skill write SQL for ad-hoc analysis.
141
+
142
+ In v1, `ghswarm history` **only opens a single DB** (when `-r` is unspecified, the `event_db` of the first repo).
143
+ If you split `event_db` per repo, events from other DBs are not visible from `history`.
144
+
145
+ If you place `event_db` inside the repository, add `*.db` / `*.db-wal` / `*.db-shm` to `.gitignore`
146
+ (not needed under the default `~/.ghswarm/`).
147
+
148
+ ## Config reference
149
+
150
+ A list of all keys. For a template, see [`config.example.yaml`](src/ghswarm/config.example.yaml) (`ghswarm init` copies it
151
+ to `~/.ghswarm.yaml`). Within string values, `${VAR}` / `${VAR:-default}` expand to environment variables
152
+ (`${VAR}` errors if unset, `${VAR:-default}` uses default when unset).
153
+
154
+ The shared settings under `defaults:` and each `repositories.<alias>` entry are **deep-merged key by key**
155
+ (nested mappings merge recursively; scalars/lists are replaced by the repo side). The RepoConfig items below can be written
156
+ in `defaults:` as well as in each repo entry.
157
+
158
+ ### Top level
159
+
160
+ | Key | Default | Description |
161
+ | --- | --- | --- |
162
+ | `repositories` | (required) | Mapping of repo configs keyed by alias. Select with `-r <alias>` |
163
+ | `defaults` | `{}` | Fallback shared by all repos (takes the RepoConfig items below) |
164
+ | `max_parallel_repos` | `3` | Upper bound on repos run concurrently during `loop` (1 or more) |
165
+ | `daemon_log` | `~/.ghswarm/ghswarm.log` | Log base path for `loop -d` (start date `-YYYY-MM-DD` is appended) |
166
+ | `daemon_pid` | `~/.ghswarm/ghswarm.pid` | PID file for `loop -d` |
167
+
168
+ ### repositories.&lt;alias&gt; (= RepoConfig; can also go in `defaults`)
169
+
170
+ | Key | Default | Description |
171
+ | --- | --- | --- |
172
+ | `repo` | (required) | GitHub `owner/repo` |
173
+ | `path` | (required) | Absolute path to the local clone (`~` allowed) |
174
+ | `base_branch` | `""` | The PR's merge target. Work branches are also cut from here. Empty means auto-detect via `gh` |
175
+ | `branch_prefix` | `"issue-"` | Prefix for work branch names (`issue-N`) |
176
+ | `spec_dir` | `".specs"` | Directory where spec files are placed |
177
+ | `test_command` | `""` | Test command run at verify. Empty skips test verification |
178
+ | `poll_interval` | `60` | Polling interval for `loop` (seconds) |
179
+ | `question_file` | `".agent_question.md"` | File the agent writes clarifications to |
180
+ | `merge_method` | `"squash"` | `squash` / `merge` / `rebase` |
181
+ | `require_approval` | `true` | Whether auto-merge requires a PR review approval |
182
+ | `address_pr_reviews` | `true` | Whether to have the review agent address PR review comments (human / bot) |
183
+ | `delete_branch_on_merge` | `true` | Whether to delete the work branch after merge |
184
+ | `post_merge_ci` | `true` | Whether to confirm the base branch's CI is green after merge before closing the Issue |
185
+ | `post_merge_ci_grace` | `180` | Grace before treating post-merge CI as "no CI" when there are 0 checks (seconds) |
186
+ | `worktree_dir` | `""` | Location for per-issue git worktrees. Empty means `../<repo-name>-worktrees` |
187
+ | `worktree_setup` | `""` | Command run once, immediately after a worktree is newly created |
188
+ | `auto_resolve_conflicts` | `true` | Whether to merge base to auto-resolve when the PR is CONFLICTING |
189
+ | `conflict_max_retries` | `3` | Retry cap for conflict resolution across loops |
190
+ | `auto_fix_ci` | `true` | Whether to fetch logs on PR CI failure and auto-fix in implement |
191
+ | `ci_fix_max_retries` | `3` | Retry cap for CI fixes across loops |
192
+ | `transient_error_patterns` | default pattern set | Regexes treated as transient errors (case-insensitive). `resource_exhausted` / `rate limit` / `\b(429\|503)\b`, etc. |
193
+ | `transient_max_retries` | `5` | Retry cap for transient errors across loops |
194
+ | `max_retries` | `3` | Self-healing retry cap within a single run |
195
+ | `issue_max_agent_runs` | `10` | Cumulative agent-run cap per Issue. `0` means unlimited |
196
+ | `lock_ttl` | `14400` | Absolute TTL of the busy lock (seconds). On the same host, pid check takes precedence |
197
+ | `event_db` | `~/.ghswarm/events.db` | SQLite path for the structured event log. Empty string disables it |
198
+ | `activity_dir` | `~/.ghswarm/activity` | Location for activity files while the daemon runs. Empty string disables it |
199
+ | `env` | `{}` | Environment variables injected into `gh` calls for this repo (`~` expansion applies) |
200
+ | `agents` | (required) | Phase → CLI invocation definition (below) |
201
+ | `labels` | defaults below | Status label names |
202
+ | `target` | `{}` | Filter conditions for the Issues to pick up |
203
+ | `notify` | all disabled | Push notifications for blocked, etc. |
204
+ | `sandbox` | `driver: none` | Docker execution of setup / verify (opt-in) |
205
+
206
+ ### agents (required)
207
+
208
+ The two phases `implement` / `review` are required. Each phase is a mapping with a `command`.
209
+ `command` is a single string, or a fallback chain (a list, with the primary command first).
210
+ The `{prompt}` placeholder is replaced with the `shlex.quote`d prompt.
211
+
212
+ ```yaml
213
+ agents:
214
+ implement:
215
+ command:
216
+ - "cursor-agent -p {prompt} --model auto"
217
+ - "claude -p {prompt} --model opus --dangerously-skip-permissions"
218
+ review:
219
+ command: "claude -p {prompt} --model sonnet --dangerously-skip-permissions"
220
+ ```
221
+
222
+ ### labels
223
+
224
+ | Key | Default |
225
+ | --- | --- |
226
+ | `idle` | `"status: idle"` |
227
+ | `blocked` | `"status: blocked"` |
228
+ | `completed` | `"status: completed"` |
229
+ | `busy_prefix` | `"status: busy-"` (busy labels are generated dynamically as `status: busy-<agent>`) |
230
+
231
+ ### target (filter for open Issues to pick up)
232
+
233
+ | Key | Default | Description |
234
+ | --- | --- | --- |
235
+ | `labels` | `[]` | Required labels (string / list). Narrows targets by labels **other than** status labels |
236
+ | `assignee` | `""` | Filter by assignee |
237
+ | `milestone` | `""` | Filter by milestone |
238
+
239
+ ### notify (push notifications on blocked / awaiting clarification / completion)
240
+
241
+ | Key | Default | Description |
242
+ | --- | --- | --- |
243
+ | `slack_webhook_url` | `null` | Slack Incoming Webhook URL. Unset disables Slack |
244
+ | `slack_mention` | `null` | Mention string to attach to the Slack body |
245
+ | `macos` | `false` | Whether to emit macOS notifications |
246
+ | `on_completed` | `true` | Whether to also notify on merge completion |
247
+
248
+ ### sandbox (Docker execution of setup / verify, opt-in)
249
+
250
+ | Key | Default | Description |
251
+ | --- | --- | --- |
252
+ | `driver` | `"none"` | `none` / `docker` |
253
+ | `image` | `""` | Required when `driver: docker` |
254
+ | `network` | `"default"` | `default` / `none` |
255
+ | `user` | `"auto"` | `auto` (= `$(id -u):$(id -g)`) / `"1000:1000"` / `""` (omit `--user`) |
256
+ | `env` | `{}` | Environment variables inside the container (`~` expansion applies) |
257
+ | `env_passthrough` | `[]` | Names of environment variables passed through from the host (e.g. `GH_TOKEN`) |
258
+ | `volumes` | `[]` | Additional mounts (`name:path`) |
259
+ | `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 `..` |
260
+
261
+ ## Usage
262
+
263
+ ```bash
264
+ # Line 1: prepare the spec and Issue (in Claude Code)
265
+ /ghswarm-spec ...request...
266
+
267
+ # Line 2: ghswarm (cwd is arbitrary; the configured path is the base for git operations)
268
+ ghswarm status # Issue state across all repos
269
+ ghswarm status -r main # only alias main
270
+ ghswarm history # local event log (latest 50)
271
+ ghswarm history -r main --issue 42 # filter by repo / Issue
272
+ ghswarm run 42 -r main --dry-run # inspect the plan for the next single step
273
+ ghswarm run 42 -r main # run Issue #42 to completion
274
+ ghswarm run 42 -r main --resume # resume from awaiting-clarification
275
+ ghswarm loop # resident parallel polling across all repos
276
+ ghswarm loop -d # background residency (does not occupy the terminal)
277
+ ghswarm loop --stop # stop the resident daemon
278
+ ghswarm loop -r a -r b # only aliases a, b
279
+ ghswarm loop --once # one pass over all repos (for cron)
280
+
281
+ # After starting the daemon, tail the actual log path shown in the banner
282
+ # tail -f ~/.ghswarm/ghswarm-2026-07-18.log
283
+
284
+ # cron example (every 5 minutes): */5 * * * * ghswarm loop --once >> ~/.ghswarm/ghswarm-cron.log 2>&1
285
+ ```
286
+
287
+ **Choosing a residency mode**: `loop -d` (daemon residency) and `loop --once` × cron are mutually exclusive. Use only one.
288
+ Daemon logs accumulate one file per start date. Manage size at your discretion with `logrotate` or similar.
289
+
290
+ ## Notes
291
+
292
+ - Within the same repository, Issues are processed serially (one active at a time). Multiple repositories can run in parallel via `loop`.
293
+ - Each coding CLI's headless auto-approval flag (`--dangerously-skip-permissions`, etc.) is set at your own risk.
294
+ - With `require_approval: true`, a PR is not merged until it gets an approval. In setups with no approver,
295
+ set it to `false`, or gate on an approve from another agent/human.
296
+ - 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**.
297
+ ghswarm finds and takes over origin's spec PR by branch name. If you merge it first, the spec lands on default, so implementation
298
+ itself can continue, but the implementation splits off into a separate new PR.
@@ -0,0 +1,280 @@
1
+ # ghswarm
2
+
3
+ [![CI](https://github.com/toyama0919/ghswarm/actions/workflows/ci.yml/badge.svg)](https://github.com/toyama0919/ghswarm/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/ghswarm.svg)](https://pypi.org/project/ghswarm/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/ghswarm.svg)](https://pypi.org/project/ghswarm/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ A development-PM agent that, on a foundation of **spec-driven development**, treats GitHub Issues as "state"
9
+ and **orchestrates multiple coding CLIs** (Claude Code / Codex / Cursor)
10
+ **with mutual exclusion enforced via labels**.
11
+
12
+ It runs no server (no Webhook/FastAPI); all state is persisted on GitHub (labels + metadata in the Issue body +
13
+ checkboxes) and in spec files. Even if the process dies, reading the Issue lets it **resume from where it left off**.
14
+
15
+ ## Two-line architecture
16
+
17
+ ```mermaid
18
+ flowchart TB
19
+ subgraph L1["Line 1 · human in the loop (skill: ghswarm-spec, outside the loop)"]
20
+ direction LR
21
+ A1[draft in tmp/spec/] --> A2[AI review] --> A3[human review] --> A4[create Issue] --> A5[move to .specs/<br/>+ open draft PR]
22
+ end
23
+ subgraph L2["Line 2 · ghswarm resident loop (autonomous, this CLI)"]
24
+ direction LR
25
+ B1[implement on PR branch] --> B2[AI review] --> B3[mark PR ready] --> B4[wait CI/approve] --> B5[auto squash merge]
26
+ end
27
+ L1 --> L2
28
+ ```
29
+
30
+ - **Line 1** is handled by the Claude Code skill [`ghswarm-spec`](.claude/skills/ghswarm-spec/SKILL.md).
31
+ The spec draft is placed in `tmp/spec/` to await human approval; after approval it moves to `.specs/YYYY-MM-DD-issue-N.md`,
32
+ is committed on branch `issue-N`, and opens a **draft spec PR** along with an Issue carrying a task checklist.
33
+ - **Line 2** is ghswarm itself. It takes over that spec PR's branch, **stacks implementation commits onto the same PR**,
34
+ 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.
35
+
36
+ ## Design highlights
37
+
38
+ - **Mutual exclusion via labels** — `status: busy-<agent>` / `idle` / `blocked` / `completed`.
39
+ Only one CLI runs at a time within the same repository. The lock lives on GitHub, so it is shared across processes.
40
+ - **State persisted in the Issue body** — saved as JSON in an HTML comment at the end of the body
41
+ (`<!-- GHSWARM_STATE_START ... GHSWARM_STATE_END -->`), holding `next_action` / `branch_name` / `spec_path` / `pr_number` and so on.
42
+ - **Progress via checkboxes** — `- [ ] task` items in the Issue body are worked through and updated to `- [x]`. Incomplete tasks are
43
+ **handed to a single CLI run all at once** (because each CLI is a one-shot headless invocation that loses its context every time,
44
+ launching one per task would force it to re-read the codebase). Once verify passes, all items are checked.
45
+ - **Spec-driven** — the spec file's body is injected into each implement/review prompt. The agent follows the spec.
46
+ - **CLI/model fixed per phase** — the CLI and model used in the `implement` / `review` phases are specified explicitly
47
+ in the config (the `command` under `agents.implement` / `agents.review`). There is no dynamic routing by an LLM.
48
+ - **Self-healing** — CLI run → tests → on failure, retry with the log attached (up to N times). If that fails, `git reset --hard`
49
+ rolls back, sets `status: blocked`, and escalates to a human.
50
+ - **Clarification** — if the spec is unclear during implementation, the agent writes `.agent_question.md` and exits. ghswarm comments on the Issue,
51
+ sets `status: blocked`, and once an answer is posted it resumes with `--resume`.
52
+ - **CI/approve gate → auto-merge → post-merge CI gate** — after the PR is created, it polls as `wait_ci`.
53
+ Once all CI succeeds and the PR review is approved (`require_approval`), it squash-merges. On CI failure it blocks.
54
+ When `mergeable=CONFLICTING`, it merges `origin/<base_branch>` in the worktree to auto-resolve, then
55
+ re-runs CI after pushing (can be disabled with `auto_resolve_conflicts`).
56
+ After merging it does not close the Issue but proceeds to `verify_merge`, closing the Issue only when the CI of the merge
57
+ commit on the merge target branch (`base_branch`) is green (regression detection; can be disabled with `post_merge_ci`).
58
+ - **blocked notifications** — when transitioning to `blocked` or awaiting clarification, it pushes to a configured Slack webhook or
59
+ macOS notification (the `notify` section, disabled by default). Consecutive re-transitions for the same reason are suppressed, and
60
+ a re-block after leaving blocked notifies again. Send failures only produce a warning log; the main process continues.
61
+
62
+ ## State machine (Line 2)
63
+
64
+ ```mermaid
65
+ stateDiagram-v2
66
+ state "wait_for_clarification (status: blocked)" as clarify
67
+ state "blocked (human step-in)" as blocked
68
+
69
+ [*] --> implement
70
+ implement --> implement: tasks remain
71
+ implement --> ai_review: tasks done
72
+ implement --> clarify: spec unclear
73
+ ai_review --> create_pr
74
+ create_pr --> wait_ci
75
+ wait_ci --> verify_merge: CI green + approve
76
+ wait_ci --> blocked: CI failed
77
+ verify_merge --> done: post-merge CI green
78
+ verify_merge --> blocked: post-merge CI failed
79
+ clarify --> implement: answer + --resume
80
+ done --> [*]
81
+ ```
82
+
83
+ ## Installation
84
+
85
+ ```bash
86
+ cd ghswarm && pip install -e .
87
+ ```
88
+
89
+ Prerequisites: `gh` (already `gh auth login`ed), and each coding CLI you use being authenticated and able to run headless.
90
+
91
+ ## Setup
92
+
93
+ ```bash
94
+ ghswarm init && $EDITOR ~/.ghswarm.yaml # edit repositories / agents, etc.
95
+ ```
96
+
97
+ ### Config file lookup order
98
+
99
+ Only the first file found is used (no merging).
100
+
101
+ 1. `--config <path>` (explicit)
102
+ 2. `~/.ghswarm.yaml`
103
+ 3. `~/.ghswarm.yml`
104
+
105
+ Config is **centralized in `~/.ghswarm.yaml` at home**. Declare multiple repositories under `repositories:` as a
106
+ mapping keyed by alias, and put shared settings under `defaults:`. Each repo entry requires
107
+ `repo` (owner/repo) and `path` (local clone).
108
+
109
+ `max_parallel_repos` limits the number of repos run concurrently during `loop` (default 3).
110
+
111
+ `daemon_log` / `daemon_pid` set the log base path and PID file for daemon residency (defaults are
112
+ `~/.ghswarm/ghswarm.log` / `~/.ghswarm/ghswarm.pid`). When started with `-d`, the start date
113
+ `-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.
114
+
115
+ ### Event log (for observation)
116
+
117
+ The result of each step (`action` / `detail`) is, by default, also recorded in SQLite at `~/.ghswarm/events.db`
118
+ (changeable via `event_db`, empty string disables it). **The labels and Issue body on GitHub are the source of truth**,
119
+ and the event DB is derived data for observation/auditing (losing it does not affect resumability). Do not lean state on the DB.
120
+
121
+ `dry_run` and `action=skipped` (passes such as lock held / waiting on CI) are not recorded. There is no fixed aggregation command
122
+ (such as `metrics`); the intent is to accumulate raw events and later have an AI skill write SQL for ad-hoc analysis.
123
+
124
+ In v1, `ghswarm history` **only opens a single DB** (when `-r` is unspecified, the `event_db` of the first repo).
125
+ If you split `event_db` per repo, events from other DBs are not visible from `history`.
126
+
127
+ If you place `event_db` inside the repository, add `*.db` / `*.db-wal` / `*.db-shm` to `.gitignore`
128
+ (not needed under the default `~/.ghswarm/`).
129
+
130
+ ## Config reference
131
+
132
+ A list of all keys. For a template, see [`config.example.yaml`](src/ghswarm/config.example.yaml) (`ghswarm init` copies it
133
+ to `~/.ghswarm.yaml`). Within string values, `${VAR}` / `${VAR:-default}` expand to environment variables
134
+ (`${VAR}` errors if unset, `${VAR:-default}` uses default when unset).
135
+
136
+ The shared settings under `defaults:` and each `repositories.<alias>` entry are **deep-merged key by key**
137
+ (nested mappings merge recursively; scalars/lists are replaced by the repo side). The RepoConfig items below can be written
138
+ in `defaults:` as well as in each repo entry.
139
+
140
+ ### Top level
141
+
142
+ | Key | Default | Description |
143
+ | --- | --- | --- |
144
+ | `repositories` | (required) | Mapping of repo configs keyed by alias. Select with `-r <alias>` |
145
+ | `defaults` | `{}` | Fallback shared by all repos (takes the RepoConfig items below) |
146
+ | `max_parallel_repos` | `3` | Upper bound on repos run concurrently during `loop` (1 or more) |
147
+ | `daemon_log` | `~/.ghswarm/ghswarm.log` | Log base path for `loop -d` (start date `-YYYY-MM-DD` is appended) |
148
+ | `daemon_pid` | `~/.ghswarm/ghswarm.pid` | PID file for `loop -d` |
149
+
150
+ ### repositories.&lt;alias&gt; (= RepoConfig; can also go in `defaults`)
151
+
152
+ | Key | Default | Description |
153
+ | --- | --- | --- |
154
+ | `repo` | (required) | GitHub `owner/repo` |
155
+ | `path` | (required) | Absolute path to the local clone (`~` allowed) |
156
+ | `base_branch` | `""` | The PR's merge target. Work branches are also cut from here. Empty means auto-detect via `gh` |
157
+ | `branch_prefix` | `"issue-"` | Prefix for work branch names (`issue-N`) |
158
+ | `spec_dir` | `".specs"` | Directory where spec files are placed |
159
+ | `test_command` | `""` | Test command run at verify. Empty skips test verification |
160
+ | `poll_interval` | `60` | Polling interval for `loop` (seconds) |
161
+ | `question_file` | `".agent_question.md"` | File the agent writes clarifications to |
162
+ | `merge_method` | `"squash"` | `squash` / `merge` / `rebase` |
163
+ | `require_approval` | `true` | Whether auto-merge requires a PR review approval |
164
+ | `address_pr_reviews` | `true` | Whether to have the review agent address PR review comments (human / bot) |
165
+ | `delete_branch_on_merge` | `true` | Whether to delete the work branch after merge |
166
+ | `post_merge_ci` | `true` | Whether to confirm the base branch's CI is green after merge before closing the Issue |
167
+ | `post_merge_ci_grace` | `180` | Grace before treating post-merge CI as "no CI" when there are 0 checks (seconds) |
168
+ | `worktree_dir` | `""` | Location for per-issue git worktrees. Empty means `../<repo-name>-worktrees` |
169
+ | `worktree_setup` | `""` | Command run once, immediately after a worktree is newly created |
170
+ | `auto_resolve_conflicts` | `true` | Whether to merge base to auto-resolve when the PR is CONFLICTING |
171
+ | `conflict_max_retries` | `3` | Retry cap for conflict resolution across loops |
172
+ | `auto_fix_ci` | `true` | Whether to fetch logs on PR CI failure and auto-fix in implement |
173
+ | `ci_fix_max_retries` | `3` | Retry cap for CI fixes across loops |
174
+ | `transient_error_patterns` | default pattern set | Regexes treated as transient errors (case-insensitive). `resource_exhausted` / `rate limit` / `\b(429\|503)\b`, etc. |
175
+ | `transient_max_retries` | `5` | Retry cap for transient errors across loops |
176
+ | `max_retries` | `3` | Self-healing retry cap within a single run |
177
+ | `issue_max_agent_runs` | `10` | Cumulative agent-run cap per Issue. `0` means unlimited |
178
+ | `lock_ttl` | `14400` | Absolute TTL of the busy lock (seconds). On the same host, pid check takes precedence |
179
+ | `event_db` | `~/.ghswarm/events.db` | SQLite path for the structured event log. Empty string disables it |
180
+ | `activity_dir` | `~/.ghswarm/activity` | Location for activity files while the daemon runs. Empty string disables it |
181
+ | `env` | `{}` | Environment variables injected into `gh` calls for this repo (`~` expansion applies) |
182
+ | `agents` | (required) | Phase → CLI invocation definition (below) |
183
+ | `labels` | defaults below | Status label names |
184
+ | `target` | `{}` | Filter conditions for the Issues to pick up |
185
+ | `notify` | all disabled | Push notifications for blocked, etc. |
186
+ | `sandbox` | `driver: none` | Docker execution of setup / verify (opt-in) |
187
+
188
+ ### agents (required)
189
+
190
+ The two phases `implement` / `review` are required. Each phase is a mapping with a `command`.
191
+ `command` is a single string, or a fallback chain (a list, with the primary command first).
192
+ The `{prompt}` placeholder is replaced with the `shlex.quote`d prompt.
193
+
194
+ ```yaml
195
+ agents:
196
+ implement:
197
+ command:
198
+ - "cursor-agent -p {prompt} --model auto"
199
+ - "claude -p {prompt} --model opus --dangerously-skip-permissions"
200
+ review:
201
+ command: "claude -p {prompt} --model sonnet --dangerously-skip-permissions"
202
+ ```
203
+
204
+ ### labels
205
+
206
+ | Key | Default |
207
+ | --- | --- |
208
+ | `idle` | `"status: idle"` |
209
+ | `blocked` | `"status: blocked"` |
210
+ | `completed` | `"status: completed"` |
211
+ | `busy_prefix` | `"status: busy-"` (busy labels are generated dynamically as `status: busy-<agent>`) |
212
+
213
+ ### target (filter for open Issues to pick up)
214
+
215
+ | Key | Default | Description |
216
+ | --- | --- | --- |
217
+ | `labels` | `[]` | Required labels (string / list). Narrows targets by labels **other than** status labels |
218
+ | `assignee` | `""` | Filter by assignee |
219
+ | `milestone` | `""` | Filter by milestone |
220
+
221
+ ### notify (push notifications on blocked / awaiting clarification / completion)
222
+
223
+ | Key | Default | Description |
224
+ | --- | --- | --- |
225
+ | `slack_webhook_url` | `null` | Slack Incoming Webhook URL. Unset disables Slack |
226
+ | `slack_mention` | `null` | Mention string to attach to the Slack body |
227
+ | `macos` | `false` | Whether to emit macOS notifications |
228
+ | `on_completed` | `true` | Whether to also notify on merge completion |
229
+
230
+ ### sandbox (Docker execution of setup / verify, opt-in)
231
+
232
+ | Key | Default | Description |
233
+ | --- | --- | --- |
234
+ | `driver` | `"none"` | `none` / `docker` |
235
+ | `image` | `""` | Required when `driver: docker` |
236
+ | `network` | `"default"` | `default` / `none` |
237
+ | `user` | `"auto"` | `auto` (= `$(id -u):$(id -g)`) / `"1000:1000"` / `""` (omit `--user`) |
238
+ | `env` | `{}` | Environment variables inside the container (`~` expansion applies) |
239
+ | `env_passthrough` | `[]` | Names of environment variables passed through from the host (e.g. `GH_TOKEN`) |
240
+ | `volumes` | `[]` | Additional mounts (`name:path`) |
241
+ | `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 `..` |
242
+
243
+ ## Usage
244
+
245
+ ```bash
246
+ # Line 1: prepare the spec and Issue (in Claude Code)
247
+ /ghswarm-spec ...request...
248
+
249
+ # Line 2: ghswarm (cwd is arbitrary; the configured path is the base for git operations)
250
+ ghswarm status # Issue state across all repos
251
+ ghswarm status -r main # only alias main
252
+ ghswarm history # local event log (latest 50)
253
+ ghswarm history -r main --issue 42 # filter by repo / Issue
254
+ ghswarm run 42 -r main --dry-run # inspect the plan for the next single step
255
+ ghswarm run 42 -r main # run Issue #42 to completion
256
+ ghswarm run 42 -r main --resume # resume from awaiting-clarification
257
+ ghswarm loop # resident parallel polling across all repos
258
+ ghswarm loop -d # background residency (does not occupy the terminal)
259
+ ghswarm loop --stop # stop the resident daemon
260
+ ghswarm loop -r a -r b # only aliases a, b
261
+ ghswarm loop --once # one pass over all repos (for cron)
262
+
263
+ # After starting the daemon, tail the actual log path shown in the banner
264
+ # tail -f ~/.ghswarm/ghswarm-2026-07-18.log
265
+
266
+ # cron example (every 5 minutes): */5 * * * * ghswarm loop --once >> ~/.ghswarm/ghswarm-cron.log 2>&1
267
+ ```
268
+
269
+ **Choosing a residency mode**: `loop -d` (daemon residency) and `loop --once` × cron are mutually exclusive. Use only one.
270
+ Daemon logs accumulate one file per start date. Manage size at your discretion with `logrotate` or similar.
271
+
272
+ ## Notes
273
+
274
+ - Within the same repository, Issues are processed serially (one active at a time). Multiple repositories can run in parallel via `loop`.
275
+ - Each coding CLI's headless auto-approval flag (`--dangerously-skip-permissions`, etc.) is set at your own risk.
276
+ - With `require_approval: true`, a PR is not merged until it gets an approval. In setups with no approver,
277
+ set it to `false`, or gate on an approve from another agent/human.
278
+ - 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**.
279
+ ghswarm finds and takes over origin's spec PR by branch name. If you merge it first, the spec lands on default, so implementation
280
+ itself can continue, but the implementation splits off into a separate new PR.
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ghswarm"
7
+ version = "0.1.0"
8
+ description = "A spec-driven development PM agent that orchestrates multiple coding CLIs using GitHub Issues as state"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Hiroshi Toyama" },
15
+ ]
16
+ dependencies = [
17
+ "PyYAML>=6.0",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/toyama0919/ghswarm"
22
+ Repository = "https://github.com/toyama0919/ghswarm"
23
+ Issues = "https://github.com/toyama0919/ghswarm/issues"
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=8.0",
28
+ "ruff>=0.8",
29
+ ]
30
+
31
+ [project.scripts]
32
+ ghswarm = "ghswarm.cli:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.setuptools.package-data]
38
+ ghswarm = ["config.example.yaml"]
39
+
40
+ [tool.pytest.ini_options]
41
+ pythonpath = ["src"]
42
+
43
+ [tool.ruff]
44
+ line-length = 100
45
+
46
+ [tool.ruff.lint]
47
+ # Pin the rule set explicitly so ruff's default-set changes (e.g. the
48
+ # expanded defaults in 0.16) don't silently break CI. These are the
49
+ # classic pre-0.16 defaults.
50
+ select = ["E4", "E7", "E9", "F"]
51
+ # E741: allow the existing use of `l` as a variable name
52
+ extend-ignore = ["E203", "E741"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+