cpmux
A declarative multiplexer for GitHub Copilot CLI agents: "tmuxinator for copilot sessions."
Write one YAML file with a shared system prompt and a task list. cpmux starts one headless
copilot session per task, each in its own git worktree and branch, and opens a draft PR.
Monitor and steer all sessions, verify exact candidate commits, and recover selected work
without replaying successful items.
Install
Requires macOS or Linux, Python ≥ 3.12, uv, and the
copilot, git, and gh CLIs on your
PATH.
uv tool install cpmux
Or from source, for development:
git clone https://github.com/gugarosa/cpmux
cd cpmux
uv tool install --editable .
Quickstart
From the root of the GitHub repository you want to change, create cpmux.yml (or run
cpmux init for a starter):
system: |
Make the smallest change that fully addresses the task.
Follow the repository's conventions and add or update tests.
items:
- Fix the broken install link in the README
- name: pagination-regression
prompt: Add a regression test for the pagination helper.
Preview the plan, start the sessions in the background, and watch the run:
cpmux up --dry-run # preview the resolved plan
cpmux up --yes # start the run in the background
cpmux attach # watch it (Ctrl-C stops watching, not the run)
By default, cpmux up starts the run in the background and opens one draft PR per item.
Pass --foreground to stay attached and watch inline (Ctrl-C then stops the run).
Each PR's title and description are written by the session itself from the changes it made, following the target repository's pull-request template when one exists. If a session doesn't produce one, cpmux falls back to the item name and prompt.
The cpmux file
A file has a shared system prompt, run-wide defaults, and items. Each item is either a
prompt string or a mapping:
system: |
Make the smallest change that fully fixes the issue, follow the surrounding
conventions, and add or update a test.
defaults:
model: gpt-5.5 # any `copilot --model` id
effort: medium # none | minimal | low | medium | high | xhigh | max
permissions: edit # readonly | edit | full (yolo)
base: main # branch to fork from and open PRs against
remote: origin # git remote to push branches and open PRs on
branch_template: cpmux/{slug} # each item's branch name; {slug} or {id}, e.g. gderosa/{slug}
concurrency: 6 # max sessions running at once (1–64)
deps: symlink # seed a worktree's node_modules: symlink | copy | install | skip
port_base: 3000 # give each item a unique port (3000, 3001, …) via $PORT
port_env: PORT # rename the port variable (default $PORT)
pr:
draft: true
labels: [cpmux]
items:
- Fix the flaky login test # bare string → key is the slug "fix-the-flaky-login-test"
- Paginate the notifications list
- name: dark-mode-contrast # mapping → key is the slug of `name`
prompt: Fix the dark-mode contrast on secondary buttons; it fails WCAG AA.
model: claude-opus-4.8
effort: high
paths: [src/components/buttons]
labels: [a11y]
depends_on: [fix-the-flaky-login-test]
Item mappings accept prompt, name, id, model, effort, permissions, base,
branch, labels, draft, paths, depends_on, base_from, env, include_system,
profile, setup, checks, timeout_seconds, and imported issue source metadata.
The pr block also accepts title_template and body_template — the fallback title and body
used when a session writes no .cpmux-pr.md; both expand {name}, {slug}, and {prompt},
while branch_template expands {slug} and {id}. permissions may be a bare preset or a
mapping that extends one —
{preset: edit, allow: [...], deny: [...], add_dir: [...], allow_url: [...]} — adding
copilot tool and network rules. Only full/yolo let the agent run git push itself; the
other presets keep push denied so cpmux owns delivery. An optional top-level version selects
the schema (currently only 1).
An item's key is its id when set, otherwise a slug of its name or prompt. Pass keys
to enter, send, logs, and kill; cpmux ls and --dry-run print them. Any string field
expands ${VAR} and ${VAR:-default} from the environment once. Escape a reference as
$${VAR} to keep the literal ${VAR} for the agent or a shell command; substituted values
are never expanded a second time. Set include_system: false to omit the shared prompt.
Identifiers must be normalized relative names, without absolute paths, ./.. components,
or aliases such as team//task. Safe namespaced item keys such as frontend/login remain
valid, but an item's key cannot be a parent of another item's key: their worktrees must
not overlap. Run IDs passed to --run follow the same relative-path rules.
Set port_base when items run dev servers: each item gets port_base + index in its
environment (as $PORT, or port_env to rename it), so parallel servers do not collide. An
item's own env takes precedence, and env values reach the session's subprocess.
Setup profiles and verified delivery
Declare the commands a worktree needs and the checks required before cpmux delivers its changes. Named profiles can be reused across items:
profiles:
python:
setup:
- name: dependencies
command: uv sync --locked --extra dev
timeout_seconds: 180
checks:
- name: tests
command: uv run pytest -q
timeout_seconds: 300
defaults:
profile: python
timeout_seconds: 1800
items:
- Fix the pagination regression
Commands may also be strings, such as checks: ["uv run pytest -q"]. Their default
timeout is 300 seconds. For setup and checks, explicit item values override the
selected profile, whose omitted fields inherit run defaults. An explicit empty list
disables inherited commands for that item.
The existing deps: install Node-lockfile convenience also runs as an owned setup
command (300-second timeout); a failed installation now prevents agent launch.
timeout_seconds limits each automated agent turn, including headless follow-ups,
not the entire run. Native interactive enter remains user-controlled.
Setup must succeed before the agent starts. After the agent finishes, cpmux commits a local candidate, runs its configured checks, and records command outcomes and the exact checked commit. Failed checks, changed source, or a moved HEAD prevent cpmux from pushing that candidate or opening a PR. Ignored test/build outputs are allowed. A successful receipt records passing commands with matching HEAD and non-ignored source before and afterward, not a hermetic or tamper-proof build. Tool versions, inherited environment, ignored files and external services are outside its fingerprint; passing checks do not establish complete correctness.
These are trusted POSIX shell commands running with your account and item environment, outside the agent's permission presets. Review the plan before launching it. Worktrees and permission presets are not operating-system sandboxes. Commands must finish their work in the foreground; cpmux stops and fails commands that leave background children. No checks configured means not configured, never an implicit verification pass.
cpmux report shows attempts, candidate commits, check status, and delivery. Add --json
for a versioned, read-only report. Dedicated prompt, environment, raw-command, and
complete-log fields are omitted. Arbitrary error text can contain command output or
secrets; reports are not automatically redacted, so inspect them before sharing.
Source edits after verification make its receipt stale.
Recover, review, and deliver
Reconnecting is different from rerunning:
cpmux attach # only watch the existing run
cpmux retry # retry failed/blocked/stopped/unstarted items
cpmux retry pagination --resume # explicitly continue its observed native session
cpmux retry pagination --fresh # new conversation and setup, retaining Git edits
cpmux retry pagination --detach --yes # recover in the background
An ordinary retry reuses successful agent work after a check or delivery failure. It
rechecks/delivers the candidate without another agent turn. If the agent itself failed,
retry starts a new conversation with the original task. All modes retain worktrees,
commits, PR references, attempts, and reported usage; none resets or rebases your edits.
Use explicit item keys to rerun successful work. A missing previously created worktree
must be restored rather than silently recreated from an old base.
Runs use their saved task/setup/check configuration; editing the original YAML affects
new runs, not an existing retry. The soft admission budget can be revised explicitly
with retry --budget.
Review and repair are explicit operations:
cpmux diff pagination # diff plus a revision token
cpmux feedback pagination "Fix the boundary case" --revision TOKEN
cpmux verify pagination # commit/check locally, never push
cpmux finalize pagination # check and deliver according to the run's --pr/--no-pr setting
Feedback refuses a changed revision under the same lease used by the agent turn. Follow-up
and interactive turns invalidate prior verification and do not automatically push.
Finalization may reuse a still-current successful receipt; an existing PR must still
be open and match the intended repository and branches before it can be updated.
verify always runs the configured checks again. PR updates confirm the delivered
head commit and refresh only cpmux's verification section, preserving human-written
title and description text. PR creation and updates target the configured remote's
repository; cross-repository PR heads are not adopted.
Git's effective push URL determines that repository, including remote.<name>.pushurl
and URL rewriting. A remote with multiple push destinations is rejected.
Ordering and stacked changes
depends_on waits for successful predecessors but still branches independently.
Use base_from when a task actually needs another item's completed code:
items:
- id: api
prompt: Add the pagination API.
- id: client
prompt: Add the client using the pagination API.
base_from: api
- id: documentation
prompt: Document the existing architecture independently.
depends_on: [api]
The child's worktree is created only after its predecessor succeeds, at the predecessor's recorded candidate commit. Its PR targets that predecessor's branch; a no-change parent uses its original base instead. Failed parents block their children without creating misleading worktrees. Ordering and inheritance edges share cycle validation. Only one code predecessor is supported: there is no automatic multi-parent merge, rebase, or retargeting of already-created children when a parent later changes.
Queue and resource visibility
cpmux pause stops admitting queued items while active agents/checks finish.
cpmux unpause resumes admission in a running supervisor, not an idle run.
defaults.premium_budget: 40 sets a soft run admission ceiling, based on reported
premium requests. In-flight sessions and unknown usage can exceed it; it is not a
hard spending cap or a dollar estimate. Once reached, unstarted agent work is blocked.
Use cpmux retry --budget 60 to increase the ceiling. Check/delivery-only recovery and
explicit follow-ups do not require new automated-agent admission.
cpmux report --json reports known usage alongside missing-usage indicators, including
follow-ups whose earlier attempts had known usage. Child RSS, when available, describes
only the identified child process, not its descendants or a whole-machine memory limit.
cpmux wait exits with 0 for complete success, 1 for a terminal failure, 2 for
unfinished work without a live owner, or 124 for --timeout SECONDS. Waiting does not
cancel healthy work. Add --json for the final report or --notify for one opt-in terminal
bell on completion.
Machine-readable reports
report --json and wait --json use report schema version 1. The report reads
persisted state and inspects source/process identity; it does not reconcile or mutate
the run. The wait command itself may reconcile abandoned work before producing it.
| Surface | Fields |
|---|---|
| Run identity and state | schema_version, run_id, managed, owner_error, paused, expected_items, recorded_items |
| Admission and usage | premium_budget, budget_reached, reported_premium_requests, items_with_unknown_usage |
Each items entry |
key, name, status, phase, attention, error, elapsed_seconds, last_activity_at |
| Item source and delivery | branch, base, base_sha, base_from, depends_on, source, candidate_sha, delivery_sha, pr_url, verification |
| Item resource/history data | reported_premium_requests, usage_incomplete, process, attempts |
verification.status is not_configured, missing, running, failed, passed,
or stale. A receipt also identifies its attempt, commit, Git tree, configured-check
fingerprint and completion time. source contains issue metadata, never its body.
Each attempt records its mode, phase, outcome, timestamps, exit/error, candidate,
whether an agent started, reported usage, and setup/check command results. Command
results include display name, phase, status, exit code, duration, log path and error,
not the raw command or complete log.
null usage means unreported, not zero. A known cumulative amount can still have
usage_incomplete: true after a later unreported turn. managed: null with
owner_error means ownership could not be established, not that the run is stopped.
Dashboard
cpmux dash orders failures and stale/unverified work before completed work. The queue
shows the real outcome, attempt and reported usage; the selected item's header adds
activity and source/delivery context. Below 84 columns, Enter opens a full-width detail
view and Escape returns to the queue.
| Keys | Action |
|---|---|
1 / 2 / 3 / 4 |
All / attention / ready for review / failed views |
j / k or arrows in the queue |
Move through tasks |
t / d / c / i |
Transcript / immutable diff / checks and attempt logs / details |
| Enter / Escape | Focus the detail pane / return focus to the queue |
| Page Up / Page Down | Scroll the focused transcript, diff, checks or details |
s |
Send a follow-up; in Diff, send feedback bound to the displayed revision |
v / f / u |
Re-run checks / finalize / update an existing PR |
p / x / r |
Pause or unpause admission / stop the item / refresh the report |
e / o / / |
Enter native Copilot / open the PR / search transcripts |
q |
Close the dashboard after cleaning up its own operations |
Run-state loading and Git inspection run off the UI event loop. Source inspections
are throttled to five seconds between completed reports unless lifecycle changes or
r request an earlier refresh; slow I/O can delay a report without blocking navigation.
d explicitly loads a new immutable diff. Scrolling is retained across refreshes, and
transcripts are read incrementally rather than reread in full for every update.
Closing a monitor leaves a healthy external background run alone. Follow-ups and
verification/finalization started inside this dashboard are owned by it: q cancels
and waits for their cleanup, including outstanding Git work, before exiting. Failures
are shown as failures, and missing verification or usage is never displayed as a pass
or fabricated zero. Conflicting write actions are disabled while the supervisor owns
the run; engine leases still recheck ownership when an operation actually starts.
Commands
Run-scoped commands accept --run <id> and default to the latest run.
| Group | Command | What it does |
|---|---|---|
| Create | cpmux init [FILE] |
Write a starter plan (defaults to cpmux.yml). Flag: --force/-f. |
cpmux plan [FILE] |
Compose a plan in your editor, or from text, speech, or audio. Flags: --text, --voice, --audio (mutually exclusive), --transcribe-model, --model, --force/-f, --up, --pr/--no-pr, --detach/--foreground/-d, --yes/-y. |
|
cpmux issues [REFS...] |
Import issue numbers or URLs into an editable plan without running an agent. Flags: --query, --repo, --limit, --template, --profile, --output, --force. |
|
| Launch | cpmux up [FILE] |
Spawn one session per item (defaults to cpmux.yml). Flags: --dry-run, --detach/--foreground/-d/-f (background by default), --concurrency/-j, --pr/--no-pr, --deps, --strip-github-token/--no-strip-github-token, --yes/-y. |
cpmux retry [KEYS...] |
Selectively recover work. Flags: --resume, --fresh, --budget, --detach/-d, --yes/-y. Foreground by default. |
|
| Monitor | cpmux ls |
Snapshot each item's status, elapsed time, and activity. |
cpmux report |
Read-only attempt, verification, and delivery summary. Flag: --json. |
|
cpmux attach |
Live status monitor; reconnects to a background run (Ctrl-C to detach). | |
cpmux dash |
Attention-first TUI with transcript, diff, checks and details. | |
cpmux wait |
Wait for completion with useful exit codes. Flags: --timeout, --json, --notify. |
|
cpmux logs KEY |
Print a transcript; --follow/-f to stream, --raw for the JSONL. |
|
cpmux search QUERY |
Search across transcripts; --all for every run, --regex, --fts to rank via Copilot's index. |
|
| Steer | cpmux enter KEY |
Drop into an interactive copilot session, resumed in place. |
cpmux send KEY "…" |
Append a follow-up turn and print the reply. | |
cpmux diff KEY |
Show the current change and its revision token. Flag: --json. |
|
cpmux feedback KEY "…" --revision TOKEN |
Send feedback only if the reviewed source is still current. Optional context: --file, --line. |
|
cpmux verify KEY |
Commit/check a candidate locally without pushing it. | |
cpmux finalize KEY |
Explicitly deliver or update the matching open PR, respecting --no-pr. |
|
cpmux pause / cpmux unpause |
Control admission, leaving active work untouched. | |
cpmux kill KEY |
Stop one running session. Flag: --yes/-y. |
|
| Teardown | cpmux down |
Stop a run's background daemon and any live sessions. Flag: --yes/-y. |
cpmux rm |
Remove the run's git worktrees. Flags: --yes/-y, --force/-f (delete uncommitted work), --purge (also delete run history). |
ls, attach, wait, retry, and dash reconcile crashed runs: they may stop orphaned session
processes and persist failed records. Detaching a monitor does not stop a healthy
managed run.
Mutating operations use run/session leases. A running supervisor excludes follow-up and
interactive writers; independent sessions in an idle run may be operated separately.
Conflicting writers fail clearly instead of racing the same native session or record.
Cancellation targets PID-and-creation-time identities, not a reused PID from old history.
Unverifiable live process identities remain unresolved and require manual inspection;
cpmux neither signals an unknown process nor clears its identity to admit another writer.
Cancellation waits for in-flight Git finalization instead of releasing its lease while
a Git operation still runs. down may report that cancellation is queued rather than
forcibly abandoning those operations and their possible remote effects.
Subprocesses retain the underlying lease descriptors if their controller crashes;
reconciliation cannot admit another writer until those jobs finish or are stopped.
Upgrading to 0.2
Finish or stop pre-0.2 runs with the version that started them before upgrading, then start a new plan. PID-only owner metadata is deliberately rejected, not converted. The superseded PID-only Python helpers are removed; ownership uses one creation-time identity API, with no old/new execution path or compatibility wrapper. The engine's interactive operation is async, like follow-up and review operations; there is no separate synchronous compatibility implementation.
GitHub issue intake
cpmux issues 42 57 --output issues.yml
cpmux issues --repo owner/repository --query "is:open label:ready" --limit 10
cpmux issues 42 --template team-plan.yml --profile python
Issue intake is read-only: it never comments, assigns, closes, or runs an issue.
Numbers and canonical issue URLs must belong to one repository. The configured gh
host and authentication are respected. A generated plan keeps stable issue IDs, URLs,
and source title/labels/update-time snapshots. Review the issue text and the editable
plan before executing it; imported text is not trusted instruction or shell configuration.
Literal ${...} content is escaped rather than expanded into local environment values.
A template contributes its system prompt, defaults and profiles, not its old items.
Imports do not silently mix previously queued tasks into the new batch.
Issue plans use cpmux/{id} branches unless the template explicitly supplies a branch
pattern, so repeated issue titles remain distinct. Branch collisions in any plan are
rejected before worktree creation.
Queries are used as written: include is:open or is:closed to restrict state. An explicit
selection larger than --limit is rejected rather than truncated. Enterprise hosts can
come from the current repository, GH_HOST, or --repo HOST/OWNER/REPOSITORY.
Composing a plan
Compose a cpmux file in your editor by default, or from text, speech, or audio:
cpmux plan issues.yml # compose in $EDITOR → cpmux file
cpmux plan issues.yml --text "fix the flaky login test and paginate the notifications"
cpmux plan issues.yml --voice # record from the mic (Enter to stop) instead
cpmux plan issues.yml --audio memo.wav # transcribe an existing recording instead
cpmux plan issues.yml --up # generate and launch it
cpmux plan opens your $EDITOR to describe the work (or takes --text), then asks copilot
to produce a validated cpmux file. Add --up to launch it. With --voice or --audio,
faster-whisper transcribes speech on-device.
Audio stays local. --voice shows a live transcript as you speak (a fast model streams
partials while recording; your chosen model produces the accurate final text on stop).
The cpmux[voice] extra installs sounddevice and faster-whisper. --text and the editor
need neither:
uv tool install "cpmux[voice]"
brew install portaudio # macOS only: sounddevice needs PortAudio
The default transcription model is large-v3-turbo (near-large-v3 accuracy, much faster
decoding). It downloads on first use (~1.6 GB) and is cached. Pick a lighter one with
--transcribe-model (for example, small, distil-large-v3, or base); larger models are
more accurate but slower on CPU.
How it works
- One item, one session. Each task becomes a headless
copilot -prun with a pre-assigned--session-id. - Separate worktrees. Each session runs in its own
git worktreeon acpmux/<slug>branch offorigin/<base>. - cpmux owns delivery. Sessions run with
git pushdenied. cpmux commits each worktree and opens one draft PR per item. With--no-pr, it commits locally and stops. - JSONL monitoring. cpmux reads copilot's
--output-format jsonevent stream and writes it to disk. Runs continue after detach and can be reattached. Crashed sessions resolve to a terminal state.
issues.yaml ──cpmux up──► session fix-login-test → worktree ─ branch ─ draft PR
system: … session paginate-list → worktree ─ branch ─ draft PR
items: … ───────────►session dark-mode-contrast→ worktree ─ branch ─ draft PR
session … (parallel · isolated)
│
monitor and steer: cpmux attach · dash · ls · logs · search
What a run leaves on disk
cpmux writes under a gitignored .cpmux/:
.cpmux/
runs/<run_id>/
manifest.json resolved run config
sessions/<key>/
prompt.md the exact prompt sent (system + item)
transcript.jsonl raw tee of copilot --output-format json
session.json per-session record (status, branch, PR url, …)
copilot-logs/ copilot's own --log-dir
attempts/<number>/ setup/check logs and source-bound verification receipt
worktrees/<run_id>/<key>/ one git worktree per item
The local Git exclude file protects .cpmux/ when the repository does not already ignore
it; cpmux does not modify the tracked .gitignore for this.
Examples
See examples/minimal.yaml and a twelve-issue frontend run
in examples/frontend.yaml. examples/verified-python.yaml
combines an explicit Python profile, soft usage limit and a two-item code stack.
Development
uv sync --extra dev
uv run pytest
Conventions, architecture invariants, and implementation boundaries live in CONVENTIONS.md.
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 cpmux-0.2.0.tar.gz.
File metadata
- Download URL: cpmux-0.2.0.tar.gz
- Upload date:
- Size: 200.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92e172ea8eee987c9362850932049a5e71ce4f25510965c845d2fbdd6c3c13b1
|
|
| MD5 |
79014e86e638e2f75089358595b54cdc
|
|
| BLAKE2b-256 |
4288c1a268641d4547ed0ea2c0b8bbab16738a3ba4cf1002f32414f61ea9d737
|
Provenance
The following attestation bundles were made for cpmux-0.2.0.tar.gz:
Publisher:
release.yml on gugarosa/cpmux
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cpmux-0.2.0.tar.gz -
Subject digest:
92e172ea8eee987c9362850932049a5e71ce4f25510965c845d2fbdd6c3c13b1 - Sigstore transparency entry: 2743302015
- Sigstore integration time:
-
Permalink:
gugarosa/cpmux@e1aaba60581be134a0bcab0d7d301f372fcc0172 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/gugarosa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e1aaba60581be134a0bcab0d7d301f372fcc0172 -
Trigger Event:
push
-
Statement type:
File details
Details for the file cpmux-0.2.0-py3-none-any.whl.
File metadata
- Download URL: cpmux-0.2.0-py3-none-any.whl
- Upload date:
- Size: 109.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f949300fc4e21256f7678c4c40a1b0f569b39408c0b4a35f539b3790d2394577
|
|
| MD5 |
8b46f56e0c805e72f35731b421546ce0
|
|
| BLAKE2b-256 |
77fdbaee3effbbc3eb312b6617221b85da54fab1fa37f69a7e0ffc5b3254da43
|
Provenance
The following attestation bundles were made for cpmux-0.2.0-py3-none-any.whl:
Publisher:
release.yml on gugarosa/cpmux
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cpmux-0.2.0-py3-none-any.whl -
Subject digest:
f949300fc4e21256f7678c4c40a1b0f569b39408c0b4a35f539b3790d2394577 - Sigstore transparency entry: 2743302081
- Sigstore integration time:
-
Permalink:
gugarosa/cpmux@e1aaba60581be134a0bcab0d7d301f372fcc0172 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/gugarosa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e1aaba60581be134a0bcab0d7d301f372fcc0172 -
Trigger Event:
push
-
Statement type: