A local macOS scheduler that polls GitHub and fires Claude Code routines as soon as phase PRs merge, so a phased implementation plan can advance autonomously while you're away.
┌──────────────┐ every 30m ┌──────────────┐
│ launchd │ ───────────────▶│ cpm script │
└──────────────┘ └──────┬───────┘
│ poll merged PRs
▼
┌──────────────┐
│ GitHub │
└──────┬───────┘
│ phase N merged
▼
┌──────────────┐
│ Claude │
│ routine │──▶ opens phase N+1 PR
└──────────────┘
You break a project into phases, where each phase is one PR. You create a Claude Code routine that knows how to read the plan and open the next PR. cpm runs on your Mac every 30 minutes. When it sees the latest phase PR has just merged, it fires the routine to start the next phase. Merge, repeat. Walk away.
Four concepts you need to know:
- Phased plan: a markdown file (typically
docs/plans/<name>.md) in your project repo listing the work as a sequence of PRs. Each phase is independently mergeable, small enough to finish in one Claude session. - Claude routine: a saved Claude Code prompt that, when invoked, reads the plan and works on the next phase. One routine per project.
- Trigger: the remote handle for a routine. Created in Claude Code; identified by a string like
trig_abc123.... - Claude Project Manager (
cpm): this tool. Watches your repos on a 30-minute loop and fires triggers when the next phase is ready to start.
- macOS (uses launchd for scheduling)
- Claude Code CLI (
claude), authenticated - GitHub CLI (
gh), authenticated to your repos jq(brew install jq)
brew install sixoverground/tap/claude-project-manager
cpm init # check deps, generate plist + empty projects.json, offer to start the scheduler
cpm new # copy the new-project setup prompt to your clipboardThen paste that prompt into Claude Code in your project repo. Claude does the rest and tells you the exact cpm add command to run when it's done. See Set up your first project for the full walkthrough.
cpm init is idempotent and safe to re-run. It won't overwrite your projects.json and won't regenerate the plist unless you pass --force.
After installation:
- Bundled assets (templates, prompts) live under
$(brew --prefix)/share/claude-project-manager/(typically/opt/homebrew/share/claude-project-manager/).brew upgrade claude-project-managerrewrites these. - Your data lives under
~/.cpm/:projects.json, the rendered launchd plist, and.cpm-state.json. Brew never touches this directory. - Logs live under
~/Library/Logs/claude-project-manager/. - The launchd plist is installed to
~/Library/LaunchAgents/claude-project-manager.plistbycpm start.
You can override CPM_SHARE and CPM_DATA via environment variables if you want non-default locations (mostly useful when developing cpm itself).
If you'd rather hack on cpm directly instead of installing via brew:
git clone git@github.com:sixoverground/claude-project-manager.git
cd claude-project-manager
chmod +x cpm
ln -s "$(pwd)/cpm" "$(brew --prefix)/bin/cpm" # optional: put cpm on PATH
cpm init # auto-detects the in-tree templates/ and prompts/ as CPM_SHAREIn dev mode, CPM_SHARE is set to the cloned repo automatically. CPM_DATA still defaults to ~/.cpm/ so you're working against the same project registry you'd use under a brew install.
A phased plan lives at docs/plans/<plan-name>.md in your project repo. It describes the work as a numbered sequence of PRs, each scoped tightly enough to finish in a single Claude Code session. You don't write this yourself; Claude Code drafts it during cpm new (see the walkthrough below).
In Claude Code, a routine is a saved prompt that runs autonomously in the cloud when invoked. For a cpm-managed project, you create one routine per project. Its job is to read docs/plans/<name>.md, find the next Pending phase, do the work, and open a PR. cpm fires the routine each time the previous phase's PR merges.
The Trigger ID (trig_...) is how cpm calls a routine. cpm dispatches via:
claude -p --allowed-tools "RemoteTrigger" --dangerously-skip-permissions \
--no-session-persistence \
"Run the remote trigger with ID trig_... ..."You don't need to remember that. cpm handles it. You just need to record the trigger_id in your registry, which cpm add does for you.
A zsh script + launchd plist. Every 30 minutes it walks projects.json, checks each project's repos via gh, and dispatches the next phase when the previous one merged and no session is currently active. See How it works below or CLAUDE.md for the full step-by-step.
End-to-end walkthrough. Assumes you've already run cpm init and the prerequisites pass.
-
Run
cpm new.cpm new
This copies the new-project setup prompt to your clipboard and tells you what to do next. No arguments needed.
-
Open Claude Code in your project repo and paste the prompt.
cd ~/Code/my-app claude
Then paste from your clipboard. Claude walks you through the whole setup:
- Asks for the project name, repo(s), and what you want to build
- Designs a phased plan and writes it to
docs/plans/<name>.md(committed to main) - Creates a routine via
/schedulewith the right execution prompt - Surfaces the routine's trigger ID
- Tells you to toggle off Repeats at claude.ai/code/routines (the CLI can't disable a routine's schedule yet)
- Ends by printing an exact
cpm addcommand for you to copy
-
Run the
cpm addcommand Claude gave you.cpm add --name my-app --repo yourorg/my-app --trigger trig_01ABCDE...
cpm appends the project to
projects.jsonand asks if you want to kickoff phase 0 now. Say Y to fire the routine immediately. If you'd rather skip that, say n and runcpm trigger my-appwhenever you're ready. -
Watch progress.
cpm status # one-line summary per project cpm logs # tail the run log
Once phase 0's PR is opened, review and merge it. The next time
cpmruns (within 30 minutes), it sees the merge and dispatches phase 1 automatically. Repeat until done.
If you closed your terminal mid-setup, you can recover by running cpm add with no arguments. It will prompt for the project name, repo(s), and trigger ID; the trigger ID is visible via /schedule list in any Claude Code session, or as part of the URL at claude.ai/code/routines.
| Command | Description |
|---|---|
cpm init |
One-time setup. Checks deps, generates plist + empty projects.json, optionally starts the scheduler. |
cpm doctor |
Verify deps, auth, projects.json, plist, scheduler state. |
cpm new |
Copy the new-project setup prompt to the clipboard. |
cpm add ... |
Register a project (flag-based, or interactive when called with no flags). |
cpm remove <name> |
Delete a project from the registry. |
cpm pause <name> |
Skip this project during runs. |
cpm resume <name> |
Un-pause. |
cpm run |
Execute cpm once (check all projects, dispatch as needed). |
cpm start |
Enable the launchd scheduler (every 30 min). |
cpm stop |
Disable the scheduler. |
cpm status |
Show project states and scheduler status. |
cpm logs |
Tail recent cpm logs. |
cpm trigger <name> |
Manually dispatch a project's routine. |
Each project entry defines the repos to monitor and the routine to dispatch.
{
"projects": [
{
"name": "my-fullstack-app",
"repos": [
{ "repo": "yourorg/my-app-web" },
{ "repo": "yourorg/my-app-ios" }
],
"trigger_id": "trig_abc123",
"paused": false
}
]
}| Field | Required | Description |
|---|---|---|
name |
Yes | Display name. Used in CLI commands. |
repos |
Yes | Array of { "repo": "owner/name" } objects. |
trigger_id |
Yes | The routine's trigger ID (trig_...). |
branch_prefix |
No | Branch prefix to monitor. Default: claude/. |
target_branch |
No | Base branch the phase PRs target (passed as base: to gh pr list). When unset, cpm matches PRs against any base, which preserves the original behavior. Set this when your project merges into something other than the default branch (e.g. develop, release/2026). |
yolo |
No | true to auto-merge phase PRs once the YOLO gates pass. Default: false. Toggle at runtime with cpm yolo <name> on|off. |
paused |
No | true to skip this project. Default: false. |
Each repo entry can override branch_prefix or target_branch if repos within a project use different conventions.
A single routine can operate across multiple repos. cpm checks all repos before dispatching:
- If any repo has an open phase PR, the project is skipped.
- The most recent merge across all repos determines dispatch timing.
- If any repo has branch activity within 2 hours, the project is skipped.
cpm checks each project on a 30-minute loop:
- Open PR? Any repo has an open phase PR (matching
branch_prefix)? If so, SKIP. - Recent merge? Find the most recent merged phase PR across all repos. If it was within the last 4 hours, this project is a candidate.
- Active session? If any repo has branch activity within the last 2 hours, SKIP (a session is likely running).
- Dispatch dedup. Each merged PR gets at most 3 dispatches, spaced 2 hours apart. This prevents runaway dispatches when the routine can't open a new PR quickly enough.
- Dispatch. Fire the routine via
claude -p+RemoteTrigger.
Decision matrix:
| Open PR | Merged (< 4h) | Active branch (< 2h) | Action |
|---|---|---|---|
| Yes | any | any | SKIP |
| No | Yes | No | DISPATCH |
| No | Yes | Yes | SKIP |
| No | No | any | SKIP |
Exception: if a PR was merged more than 4 hours ago with no open PR and no recent activity, cpm dispatches anyway. The previous run may have failed.
State is kept in .cpm-state.json (gitignored) to track dispatch counts per PR. See CLAUDE.md for the full operational narrative.
YOLO mode ("yolo": true on a project) tells cpm to auto-merge a phase PR as soon as five gates pass. Until all five pass, cpm SKIPs as usual.
- Not draft. The PR is not a draft.
- No blocking labels. None of
do-not-merge,wip,blockedare applied. - CI green. Every check on the PR is either
SUCCESSorSKIPPED. Anything else (pending, failing,NEUTRAL,ACTION_REQUIRED, etc.) blocks. A PR with zero configured checks is also refused (failsafe against misconfigured CI). - No
CHANGES_REQUESTEDreviews outstanding. Each reviewer's most recent review is what counts: a CHANGES_REQUESTED that the same reviewer later replaced with an APPROVE no longer blocks. Anyone (human, Copilot, other bots) can block by requesting changes. - Copilot acknowledged. Copilot has reviewed the PR at least once, AND a commit on the PR carries the trailer
Copilot-Addressed: yeswith a timestamp newer than Copilot's latest review.
When all five pass, cpm runs gh pr merge <pr> --squash --delete-branch and records the attempt. The next cpm cycle detects the merged PR via the normal path and dispatches the next phase.
YOLO attempts are deduped (5 max per PR, 1h cooldown between attempts). If a PR exhausts the cap without merging, cpm logs YOLO STUCK and surfaces it in cpm status for human review. Disable at any time with cpm yolo <name> off.
cpm start says "plist not found". Run cpm init first to generate the plist.
cpm doctor says gh auth: not authenticated. Run gh auth login and follow the prompts.
"I created a routine but where's the Trigger ID?" Triggers are listed under your routine in Claude Code's UI. Depending on your Claude Code version, claude trigger list may also work from the CLI. The ID starts with trig_.
Routine fires but no PR appears. Most often a permission prompt blocking the routine. cpm dispatches with --dangerously-skip-permissions, which should bypass interactive prompts, but a sandboxed environment can still block. Check cpm logs for the dispatch output and Claude Code's own logs for the routine's session.
Same PR keeps re-dispatching. cpm caps dispatches per merged PR at 3 attempts, spaced 2 hours apart. If you hit the cap, cpm status shows "Max retries hit for #N." Usually means the routine isn't producing a new branch, so investigate the routine itself.
Routine runs on the wrong branch. Check branch_prefix in projects.json. The default is claude/. The routine must produce branches that match.
How do I see what cpm decided last run? cpm logs tails the most recent daily log. Look for [<name>] SKIP: ... or [<name>] DISPATCH: ... lines.
Logs live at ~/Library/Logs/claude-project-manager/:
cpm-YYYY-MM-DD.logis the daily run log, rotated after 14 days.launchd-stdout.log/launchd-stderr.logcapture launchd output, truncated when over 1 MB.
This repo dogfoods itself. Significant changes are organized as phased plans and shipped one phase per PR. See CONTRIBUTING.md.
MIT. See LICENSE.