Run several Claude Code sessions in parallel on the same project, isolated by git worktree, coordinated by a shared SQLite state store — without adopting a multi-agent framework or giving up control over merges.
Built for people who want AI-assisted parallel development but aren't comfortable handing an autonomous agent swarm full control. This tool handles worktree isolation, session dispatch, dependency ordering, and merge review. You still decide what work exists and you still approve every merge. Nothing auto-merges, nothing auto-resolves conflicts.
The iTerm2/AppleScript layer is gone — everything runs on
tmuxnow, and the WSL2 code paths are written and code-reviewed. But nobody has ever run it on Windows. Every macOS path has been verified live; the entire WSL2 column is unrun.If you have a Windows machine and twenty minutes, you'd be the first person to find out. There are five specific things to check in Help wanted: WSL2 verification — a report that it broke is just as useful as one that it worked. Open an issue either way.
- Isolation — each session runs in its own git worktree and
session/<label>branch, so two sessions editing the same project can never clobber each other's files - Shared state — a local SQLite store (
.claude/session-state.db) tracks every session's label, status, worktree path, pid, and retry count, so all the scripts agree on what's actually happening - Task drafting — the orchestrator session runs with a system prompt (
orchestration/prompts/task-drafting-mode.md) that turns plain-language descriptions into structured, consistent task prompts before any worker is spawned - Dependency ordering — sessions can declare that they need an artifact (a file, branch, or commit) from another session. The dependency graph is cycle-checked, and a dependent is notified once the artifact actually exists
- Optional Notion pick-up — if you point it at a Notion Projects/Tasks workspace, the orchestrator session lists that project's open tasks at launch and flips a task to
In progresswhen you pick it - Manual, confirmed merges —
merge-review.shshows you a diff and commit log per session; merging requires an explicit flag, and a yes/no confirmation unless you pass--yes
- macOS, or Windows via WSL2 — session spawn, attach, and teardown all go through
tmux, so any platform withtmuxandbashshould work. See the platform-support caveat under Known limitations before relying on the WSL2 path — it is implemented but has never been run on Windows - Any terminal emulator. There is no longer an iTerm2 or AppleScript dependency
- Homebrew bash 5+ (
brew install bash) on macOS — macOS ships bash 3.2, which silently corrupts the associative arrays this tool relies on. Most Linux/WSL2 distributions already ship bash 5 tmux— each worker session runs inside a grouped tmux sessionjq— reads your Claude Code MCP configsqlite3- Optional, for desktop notifications:
osascripton macOS (bundled) orpowershell.exereachable from WSL2. With neither, notifications are a silent no-op - The
claudeCLI, authenticated - Target project must be a git repository (worktrees require it)
git clone https://github.com/WhoReallyKnowsAnything/ai-orchestrator.git
cd ai-orchestrator
cp .env.example .envInstall the dependencies for your platform:
# macOS -- bash 5 matters here, macOS ships 3.2
brew install bash tmux jq
# Debian / Ubuntu, including WSL2
sudo apt install tmux jq sqlite3
# Fedora / RHEL, including WSL2
sudo dnf install tmux jq sqliteMost Linux and WSL2 distributions already ship bash 5 and sqlite3; macOS ships sqlite3 but only bash 3.2, which is why the Homebrew bash is listed there and not elsewhere.
Then edit .env and set at least PROJECTS_DIR to the directory holding your projects. Every value in .env is optional — anything left blank degrades gracefully rather than failing. See the comments in .env.example for what each one does.
.env is git-ignored. There is no build or install step beyond the above: the SQLite state store is created for you the first time you launch a session against a git repository.
If you want to run the test suite on a fresh clone before ever launching a session, create the state store first with
bash orchestration/init/init-state-store.sh <path-to-a-git-repo>— otherwisetest/test-dependency-graph.shfails withno such table: dependencies. Launching normally does this for you; only the tests hit an uninitialised clone.
bash test/test-no-hardcoded-personal-values.sh # no maintainer paths, IDs, or IPs shipped
bash test/test-no-osascript-iterm.sh # no AppleScript/iTerm2 coupling in orchestration/
bash test/test-readme-quickstart-links.sh # README commands and components match reality
bash test/test-dependency-graph.sh # session dependency graph (needs the state store, see above)Each is plain bash with no framework, and each exits non-zero on failure.
./orchestration/launch-claude.shInteractive. Pick root-dir mode or project mode, an existing project from PROJECTS_DIR or a new one, then a new or resumed conversation. The first session opened in orchestration mode becomes the orchestrator and gets the task-drafting system prompt appended.
orchestration/lib/dispatch-task.sh is the single place session-spawning lives. The orchestrator session calls it for you, but it is a normal script you can drive by hand:
# turn a task title into a collision-free session label
./orchestration/lib/dispatch-task.sh --slugify "Add email validation to the signup form"
# spawn a worktree-isolated session under that label
./orchestration/lib/dispatch-task.sh add-email-validation-to-the-signup-form
# tear one down (removes worktree and branch if merged; renames the branch if not)
./orchestration/lib/dispatch-task.sh --kill add-email-validation-to-the-signup-formSend the task text to a running session with Claude Code's own cross-session messaging, not as a script argument.
Dispatching does not open a window for you. The script creates the worktree and the tmux window, then prints the command to attach:
Worker 'add-email-validation-to-the-signup-form' dispatched. Attach with:
/opt/homebrew/bin/tmux new-session -A -t <base> -s <base>-<label> \; select-window -t <label>
The tmux path is resolved absolutely (SSH forced commands run with a bare PATH), and the grouped-session form gives each attached client its own window focus while sharing the underlying windows — so you and, say, a phone SSH session can look at different workers at the same time.
Run that command yourself, in whatever terminal you like, whenever you want to watch the session. The worker starts and runs whether or not you ever attach. There is no cross-platform way to open a terminal window, so printing the command is the portable equivalent — see Limits.
# B needs an artifact from A
./orchestration/lib/dispatch-task.sh --add-dependency <dependent> <upstream> <artifact>
# has the artifact landed yet? exit 0 = yes, 1 = not yet. prints nothing
./orchestration/lib/dispatch-task.sh --artifact-exists <upstream> <artifact>
# who is still waiting on this session?
./orchestration/lib/dispatch-task.sh --pending-dependents <label>
# record that a dependent has been told, so it is not notified twice
./orchestration/lib/dispatch-task.sh --mark-notified <dependent> <upstream><artifact> is a plain locator — a file path, branch name, or commit SHA. Cycles are refused when the edge is added.
./orchestration/merge-review.sh # all sessions, with status
sqlite3 .claude/session-state.db \
"SELECT agent_id, status, retry_count FROM sessions;"Status values: running, blocked, done, merged, stalled, dead.
./orchestration/merge-review.sh <label> # diff + commit log, no changes
./orchestration/merge-review.sh <label> --merge # conflict check, then confirm
./orchestration/merge-review.sh <label> --merge --yes # skip the prompt--merge runs a dry-run conflict check and shows the diff before asking. After a successful merge it optionally sends WRAP_UP_CMD to the worker's tmux window (see Optional integrations), then tears the window down and removes the worktree.
Already have a plain Claude Code session running and want to orchestrate from it?
./orchestration/promote-session.sh [target_dir] # default: cwdInitializes the state store and registers the current session as the dashboard. It cannot retrofit --model or --append-system-prompt-file onto an already-running claude process, so the task-drafting prompt is not applied — relaunch via launch-claude.sh if you need it.
The orchestrator runs standalone. Everything below is opt-in, configured in .env, and absent by default — nothing here is required, and leaving any of it blank is a supported configuration, not a degraded one.
These exist because the tool was built alongside a particular Claude Code setup. They are wired as generic hooks rather than hard dependencies, so you can point them at your own equivalents or ignore them entirely.
WRAP_UP_CMD is an optional command string sent to a worker's tmux window just before the session is torn down, giving that session a chance to save state, write notes, or sync whatever you care about before it disappears.
Blank by default, in which case no wrap-up is sent and merge-review.sh goes straight to teardown.
Set it to whatever slash command your setup uses. If you run a session-management skill suite, this is where its wrap-up entry point goes — for example a /wrap-up-style command that writes session learnings out before the window closes. The tool does not care what the command is or what it does; it types the string into the window and waits.
GSD is a planning-and-execution skill suite for Claude Code — roadmaps, phase plans, and structured execution under .planning/. This repo was built with it, and two shipped prompt files reference it:
orchestration/prompts/worker-dependency-awareness.mdtells a worker to route phase- or wave-scoped tasks through a GSD skill and mentions/gsd-nextorchestration/prompts/task-drafting-mode.mdreferences/notion-pullwhen describing the Notion task flow
Neither is a dependency. If you don't have GSD installed, a worker simply never matches those instructions and implements the task directly. Nothing errors, nothing hangs. Edit or delete those passages if you'd rather they weren't there — the prompts are plain markdown appended to a session's system prompt.
Set NOTION_PROJECTS_DATA_SOURCE_ID and NOTION_TASKS_DATA_SOURCE_ID to make the orchestrator session list your project's open Notion tasks at launch and flip one to In progress when you pick it.
Both blank is the default and skips the entire flow — no wait, no error, no prompt. The Notion token itself is not read from .env; it comes from your existing Claude Code MCP config at ~/.claude.json.
OBSIDIAN_WIKI_DIR points the New-Project scaffold at a vault folder where it drops a stub note for each new project. Blank, or a directory that doesn't exist, and the step is skipped.
TAILSCALE_IP is the Tailscale IP of this machine, used only by the one-shot orchestration/lib/setup-phone-keys.sh script to write an SSH config HostName entry for each project. Blank by default, and only needed if you're running that script — see docs/phone-access.md for the full phone-access setup this feeds into.
launch-claude.sh ──spawns──> dispatch-task.sh ──> tmux window
│ │ (worktree-isolated
│ │ Claude Code session)
│ │
└──────writes────> session-state.db <────writes────┘
(SQLite)
│
└──reads── merge-review.sh ──> diff, confirm, merge
dispatch-task.sh is the one place worktree- and tmux-window-creation logic lives, shared by the interactive launcher and by the orchestrator session. It prints a tmux attach command rather than opening a terminal window for you — there is no cross-platform primitive for that, and printing works identically everywhere. lib/tmux-session.sh is the only module that talks to tmux directly — every script that creates, attaches to, or tears down a session window sources it rather than shelling out to tmux on its own. lib/session-state.sh is the only module that talks to SQLite. lib/config.sh loads .env and is deliberately fail-soft.
- No third-party multi-agent framework — everything is scripted against
git worktree, theclaudeCLI,tmux, and the platform's own notifier - No long-running daemon
- Automation fails safe: on any uncertainty, log loudly and leave things alone rather than guess
- Conflicts, merges, and anything hard to reverse always surface to you — never auto-resolved
This section is deliberately specific, because "cross-platform" usually means "the author only ever ran it on one machine." That is exactly the situation here, and pretending otherwise would waste your time.
The iTerm2/AppleScript layer has been removed entirely. Session spawn, attach, wrap-up, and teardown all go through tmux, and desktop notifications go through a shim that picks osascript on macOS, powershell.exe on WSL2, and silently does nothing if neither is available. There is no macOS-only code path left.
But the WSL2 path has never been run on Windows. It was written against documented behaviour and verified by code review, not by execution. Here is the honest state:
| Area | macOS | WSL2 / Windows |
|---|---|---|
Automated regression suite (test/*.sh) |
Passing | Not run |
| Desktop notification renders | Verified live | Not run |
| Notification silent no-op with no notifier | Verified live | Not run |
tmux window create / send-keys / teardown leaves nothing behind |
Verified live | Not run |
init-state-store.sh schema create + migration |
Verified live | Not run |
Full launch-claude.sh launch flow |
Verified live | Not run |
$PPID tmux-server watchdog |
Verified live | Not run |
merge-review.sh <label> --merge end-to-end |
Verified live | Not run |
Every row has been exercised on macOS by the maintainer. Nothing in the WSL2 column has been run by anyone — that is the whole of the gap, and it is why the section below exists.
If you run this on WSL2, the maintainer would genuinely like to hear what happened — working or not. The useful things to check:
./orchestration/launch-claude.shcompletes and leaves you attached to a tmux session- Dispatching a worker produces a session that runs its task to completion
- A completion notification appears via
powershell.exe, and degrades to silence rather than an error if it is unreachable ./orchestration/merge-review.sh <label> --mergemerges, andtmux lsafterwards shows no leftover window or session- The
$PPIDwatchdog tears the tmux server down when the invoking shell exits — and, more importantly, does not fire early while you are still using it
Point 5 is the riskiest change in the codebase: its payload is tmux kill-server, which kills every tmux session on the machine, not just this tool's. Test it somewhere you can afford to lose your tmux state.
What this tool deliberately does not do:
- Single machine. Everything reads and writes a local SQLite file and local git worktrees; there is no cross-machine coordination
- Single user. There is no concept of multiple people sharing one orchestrator's state
- Manual merge only. Nothing auto-merges or auto-resolves a conflict. A prior orchestration tool the maintainer used broke concurrent sessions by acting on their behalf, and the fix cost more time than it saved — every merge here requires an explicit flag and a confirmation, on purpose
- No native Windows support without WSL. A native PowerShell/Windows Terminal implementation would mean a second, parallel implementation of the entire spawn/attach/merge layer plus a cross-platform abstraction over both — roughly doubling the maintained surface forever. WSL2 runs the existing bash/tmux code almost unchanged instead
- A 4-session ceiling, soft, not enforced. Nothing stops you from running more; the tool just isn't designed or tested past about four concurrent workers
- Notion is optional. Task pick-up works with plain drafting mode if
NOTION_PROJECTS_DATA_SOURCE_ID/NOTION_TASKS_DATA_SOURCE_IDare left blank
- A fresh clone must initialize its state store before the dependency tests pass.
.claude/session-state.dbis git-ignored, and whilelaunch-claude.shandpromote-session.shboth create it for you on first launch, running the test suite on a clone you have never launched from does not.test/test-dependency-graph.shfails 6 of 16 subtests withno such table: dependenciesuntil you runbash orchestration/init/init-state-store.sh <dir>— after which it is 16/16 - Dispatch prints a
tmux attachcommand instead of opening a window for you. There is no cross-platform way to open a terminal window, so the attach command is printed and you attach yourself. This is a deliberate trade for portability - The tmux-server watchdog keys on the invoking shell's PID. If that shell exits — including a plain
exit, not just closing the window — the tmux server is torn down. PID reuse can also mask a dead shell and leave the server running. Both are documented in aponytail:comment at the call site, with an upgrade path orchestration/lib/setup-phone-keys.shis a one-shot personal-setup script, not part of the orchestrator. It writes to~/.ssh/. Read it before running it; seedocs/phone-access.md- Notion pick-up is opt-in and needs two data-source IDs in
.env. Left blank, it is skipped silently with no wait and no error
MIT — see LICENSE.