cpmux 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 (59) hide show
  1. cpmux-0.1.0/.github/workflows/ci.yml +42 -0
  2. cpmux-0.1.0/.github/workflows/release.yml +37 -0
  3. cpmux-0.1.0/.gitignore +18 -0
  4. cpmux-0.1.0/.pre-commit-config.yaml +25 -0
  5. cpmux-0.1.0/CHANGELOG.md +74 -0
  6. cpmux-0.1.0/CONVENTIONS.md +162 -0
  7. cpmux-0.1.0/LICENSE +21 -0
  8. cpmux-0.1.0/PKG-INFO +248 -0
  9. cpmux-0.1.0/README.md +216 -0
  10. cpmux-0.1.0/cpmux/__init__.py +4 -0
  11. cpmux-0.1.0/cpmux/__main__.py +7 -0
  12. cpmux-0.1.0/cpmux/config.py +595 -0
  13. cpmux-0.1.0/cpmux/engine/__init__.py +2 -0
  14. cpmux-0.1.0/cpmux/engine/copilot_store.py +94 -0
  15. cpmux-0.1.0/cpmux/engine/daemon.py +234 -0
  16. cpmux-0.1.0/cpmux/engine/interact.py +59 -0
  17. cpmux-0.1.0/cpmux/engine/session.py +115 -0
  18. cpmux-0.1.0/cpmux/engine/store.py +293 -0
  19. cpmux-0.1.0/cpmux/engine/supervisor.py +410 -0
  20. cpmux-0.1.0/cpmux/events.py +171 -0
  21. cpmux-0.1.0/cpmux/logging.py +38 -0
  22. cpmux-0.1.0/cpmux/py.typed +0 -0
  23. cpmux-0.1.0/cpmux/theme.py +198 -0
  24. cpmux-0.1.0/cpmux/ui/__init__.py +2 -0
  25. cpmux-0.1.0/cpmux/ui/cli.py +904 -0
  26. cpmux-0.1.0/cpmux/ui/dashboard.py +378 -0
  27. cpmux-0.1.0/cpmux/ui/render.py +72 -0
  28. cpmux-0.1.0/cpmux/ui/search.py +89 -0
  29. cpmux-0.1.0/cpmux/vcs/__init__.py +2 -0
  30. cpmux-0.1.0/cpmux/vcs/git.py +262 -0
  31. cpmux-0.1.0/cpmux/vcs/pr.py +240 -0
  32. cpmux-0.1.0/cpmux/voice/__init__.py +2 -0
  33. cpmux-0.1.0/cpmux/voice/recorder.py +246 -0
  34. cpmux-0.1.0/cpmux/voice/synthesizer.py +100 -0
  35. cpmux-0.1.0/cpmux/voice/transcriber.py +94 -0
  36. cpmux-0.1.0/examples/frontend.yaml +49 -0
  37. cpmux-0.1.0/examples/minimal.yaml +10 -0
  38. cpmux-0.1.0/pyproject.toml +52 -0
  39. cpmux-0.1.0/setup.cfg +8 -0
  40. cpmux-0.1.0/tests/conftest.py +16 -0
  41. cpmux-0.1.0/tests/engine/test_copilot_store.py +72 -0
  42. cpmux-0.1.0/tests/engine/test_daemon.py +143 -0
  43. cpmux-0.1.0/tests/engine/test_interact.py +26 -0
  44. cpmux-0.1.0/tests/engine/test_session.py +96 -0
  45. cpmux-0.1.0/tests/engine/test_store.py +135 -0
  46. cpmux-0.1.0/tests/engine/test_supervisor.py +114 -0
  47. cpmux-0.1.0/tests/test_config.py +354 -0
  48. cpmux-0.1.0/tests/test_events.py +112 -0
  49. cpmux-0.1.0/tests/test_logging.py +33 -0
  50. cpmux-0.1.0/tests/test_theme.py +57 -0
  51. cpmux-0.1.0/tests/ui/test_cli.py +329 -0
  52. cpmux-0.1.0/tests/ui/test_dashboard.py +183 -0
  53. cpmux-0.1.0/tests/ui/test_render.py +84 -0
  54. cpmux-0.1.0/tests/ui/test_search.py +56 -0
  55. cpmux-0.1.0/tests/vcs/test_git.py +123 -0
  56. cpmux-0.1.0/tests/vcs/test_pr.py +99 -0
  57. cpmux-0.1.0/tests/voice/test_recorder.py +15 -0
  58. cpmux-0.1.0/tests/voice/test_synthesizer.py +23 -0
  59. cpmux-0.1.0/tests/voice/test_transcriber.py +85 -0
@@ -0,0 +1,42 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - uses: actions/setup-python@v6
14
+ with:
15
+ python-version: "3.12"
16
+ - run: pip install pre-commit
17
+ - run: pre-commit run --all-files --show-diff-on-failure
18
+
19
+ test:
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ python-version: ["3.12", "3.13"]
25
+ steps:
26
+ - uses: actions/checkout@v5
27
+ - uses: actions/setup-python@v6
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+ - run: pip install -e ".[dev]"
31
+ - run: pytest -q
32
+
33
+ package:
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v5
37
+ - uses: actions/setup-python@v6
38
+ with:
39
+ python-version: "3.12"
40
+ - run: pip install build twine
41
+ - run: python -m build
42
+ - run: twine check dist/*
@@ -0,0 +1,37 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v5
15
+ - uses: actions/setup-python@v6
16
+ with:
17
+ python-version: "3.12"
18
+ - run: pip install build twine
19
+ - run: python -m build
20
+ - run: twine check dist/*
21
+ - uses: actions/upload-artifact@v4
22
+ with:
23
+ name: dist
24
+ path: dist/
25
+
26
+ publish:
27
+ needs: build
28
+ runs-on: ubuntu-latest
29
+ environment: pypi
30
+ permissions:
31
+ id-token: write
32
+ steps:
33
+ - uses: actions/download-artifact@v4
34
+ with:
35
+ name: dist
36
+ path: dist/
37
+ - uses: pypa/gh-action-pypi-publish@release/v1
cpmux-0.1.0/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ # cpmux runtime state (worktrees, transcripts, run manifests)
2
+ .cpmux/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.egg-info/
8
+ build/
9
+ dist/
10
+ .venv/
11
+ venv/
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+
15
+ # OS / editors
16
+ .DS_Store
17
+ .idea/
18
+ .vscode/
@@ -0,0 +1,25 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: check-added-large-files
6
+ args: ['--maxkb=2500']
7
+ - id: check-case-conflict
8
+ - id: check-docstring-first
9
+ - id: check-merge-conflict
10
+ - id: check-yaml
11
+ - id: detect-private-key
12
+ - id: trailing-whitespace
13
+ - id: requirements-txt-fixer
14
+ - repo: https://github.com/PyCQA/isort
15
+ rev: 5.13.2
16
+ hooks:
17
+ - id: isort
18
+ - repo: https://github.com/psf/black
19
+ rev: 24.10.0
20
+ hooks:
21
+ - id: black
22
+ - repo: https://github.com/PyCQA/flake8
23
+ rev: 7.1.1
24
+ hooks:
25
+ - id: flake8
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ All notable changes to cpmux are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - Pull requests opened by `cpmux up` now carry a title and description authored by the
12
+ session from the changes it actually made, following the target repository's
13
+ pull-request template when one exists. If a session produces none, cpmux falls back to
14
+ the item name and a short summary of the prompt.
15
+ - `cpmux plan --voice` now shows a live transcript while you speak: a fast model streams
16
+ partial text during recording, and the configured model produces the accurate final
17
+ transcription when you stop.
18
+
19
+ ### Changed
20
+
21
+ - `cpmux up` now runs in the background by default; pass `--foreground`/`-f` to stay
22
+ attached and watch inline.
23
+ - Voice dictation now defaults to the `large-v3-turbo` model (was `base`) and enables
24
+ VAD filtering, substantially improving transcription accuracy (first use downloads
25
+ ~1.6 GB, cached afterward; override with `--transcribe-model`).
26
+ - Voice plan synthesis now instructs the model to preserve every dictated detail instead
27
+ of producing a concise summary, so plans no longer drop tasks or constraints.
28
+
29
+ ### Fixed
30
+
31
+ - Live views (`up --foreground`, `attach`) no longer corrupt the terminal when arrow
32
+ keys or other input are pressed: keystroke echo is suppressed while a live view renders.
33
+
34
+ ## [0.1.0]
35
+
36
+ ### Changed
37
+
38
+ - Renamed the project from `cmux` to `cpmux` (the `cmux` name was taken on PyPI):
39
+ the command, package, `.cpmux/` state directory, `CPMUX_*` environment
40
+ variables, and the default `cpmux/{slug}` branch prefix all change accordingly.
41
+
42
+ ### Added
43
+
44
+ - `cpmux rm --purge` to delete a run's on-disk history so it leaves `cpmux ls`.
45
+ - Preflight validation that each item's `paths` exist in its worktree, failing
46
+ early with a clear error instead of a late `copilot` failure.
47
+ - `branch_template` documented in the voice-plan schema so a spoken branch scope
48
+ maps to the branch, not `base`.
49
+
50
+ ### Fixed
51
+
52
+ - A `--no-pr` item whose agent committed its own work now reports `done`
53
+ (previously `no changes`).
54
+ - The dashboard follow-up now forwards each item's `env` overrides, matching
55
+ `cpmux send`.
56
+
57
+ ## [0.0.1]
58
+
59
+ Initial release.
60
+
61
+ ### Added
62
+
63
+ - Declarative, guided multiplexer for GitHub Copilot CLI agents: one YAML plan
64
+ (a shared system prompt plus a list of items) spawns one isolated headless
65
+ `copilot` session per item, each in its own git worktree and branch.
66
+ - Commands: `init`, `up`, `plan`, `ls`, `attach`, `dash`, `logs`, `search`,
67
+ `enter`, `send`, `kill`, `down`, `rm`.
68
+ - Interactive `dash` TUI, live `up` and `attach` monitoring, and `plan` for
69
+ composing a plan from an editor, text, speech (`--voice`), or an audio file.
70
+ - On-device speech-to-text via faster-whisper behind the `voice` extra.
71
+
72
+ [Unreleased]: https://github.com/gugarosa/cpmux/compare/v0.1.0...HEAD
73
+ [0.1.0]: https://github.com/gugarosa/cpmux/compare/v0.0.1...v0.1.0
74
+ [0.0.1]: https://github.com/gugarosa/cpmux/releases/tag/v0.0.1
@@ -0,0 +1,162 @@
1
+ # cpmux — Conventions
2
+
3
+ Rules and invariants for adding to or changing cpmux. cpmux adopts the **phitrain conventions**
4
+ (microsoft/aifsdk `.github/rules/` R1–R18 and `phitrain/CONVENTIONS.md`) as its style
5
+ rules. **Code defines behavior; this file lists the applicable rules.**
6
+
7
+ For user-facing setup and usage see `README.md`.
8
+
9
+ ## Architecture invariants
10
+
11
+ These invariants keep cpmux composable; do not violate them.
12
+
13
+ - **One worktree per item.** Every item runs in its own `git worktree` on a unique
14
+ `cpmux/<slug>` branch off `origin/<base>`. Items never share a working tree.
15
+ - **Agents are edit-only; the orchestrator ships.** Sessions run with `git push`
16
+ denied (`--deny-tool='shell(git push)'`). The orchestrator, never the agent,
17
+ commits the diff, pushes the branch, and opens exactly one draft PR per item.
18
+ - **Monitor via JSONL, never PTY.** Session state is derived from
19
+ `copilot --output-format json` (a JSONL event stream), tee'd to disk. Do not
20
+ screen-scrape a terminal.
21
+ - **Pre-assigned session ids.** The orchestrator assigns each session's
22
+ `--session-id` UUID up front, so a session is always addressable for status,
23
+ resume, and recovery.
24
+ - **cpmux owns only `.cpmux/`.** copilot keeps its own transcripts and resumable
25
+ session store under `~/.copilot`; reuse it read-only rather than duplicating it.
26
+ - **A run has one owner.** Its pid is recorded in `owner.json`: the foreground `up`
27
+ process, or the detached daemon. A live owner means the run is managed. A stale
28
+ owner (present but dead) marks a crash, so non-terminal sessions reconcile to a terminal
29
+ state instead of remaining "running" indefinitely.
30
+ - **Config precedence is `item > defaults > built-in`.** Resolution happens once in
31
+ `Plan.resolve()`; downstream code consumes `ResolvedItem`, never re-merges.
32
+
33
+ ## Package structure
34
+
35
+ Modules are grouped by domain. Shared foundation modules stay at the package root.
36
+
37
+ ```
38
+ cpmux/
39
+ config.py events.py logging.py foundation: config model, JSONL/status, logging
40
+ engine/ supervisor session daemon store interact run lifecycle + state
41
+ vcs/ git pr git worktrees + PR automation
42
+ voice/ recorder transcriber synthesizer speech → transcript → cpmux plan
43
+ ui/ cli dashboard search render Typer commands, TUI, transcript rendering
44
+ ```
45
+
46
+ - **Layering is one-directional:** `ui` → {`engine`, `voice`} → `vcs` → foundation. A layer
47
+ may import only the layers below it; foundation imports no subpackage. This keeps `engine`
48
+ headless without the TUI. Shared code moves to the lowest layer that needs it
49
+ (status-to-colour lives in `ui/render.py`
50
+ because only the UI reads it; the JSONL `event_data` unwrap lives in `events.py`
51
+ because the engine needs it too).
52
+ - **A subpackage must be a cohesive domain** with a few focused modules,
53
+ not a thin split of one concern. Heavy or optional third-party deps (`sounddevice`,
54
+ `faster-whisper` behind the `voice` extra) are imported lazily in the function that
55
+ needs them to keep the core install and `--help` light.
56
+ - **Absolute imports only**, and `__init__.py` stays empty apart from the header —
57
+ import from the module, not the package.
58
+ - **Tests mirror source 1:1**, so `engine/store.py` is tested by
59
+ `tests/engine/test_store.py`. Shared fixtures live in `tests/conftest.py`.
60
+
61
+ ## `.cpmux/` layout
62
+
63
+ Repo-local and gitignored. cpmux stores orchestration bookkeeping here; nothing here is committed.
64
+
65
+ ```
66
+ .cpmux/
67
+ runs/<run_id>/
68
+ manifest.json resolved run config
69
+ sessions/<key>/
70
+ prompt.md the exact prompt sent to copilot
71
+ transcript.jsonl raw tee of copilot --output-format json
72
+ session.json per-session record (status, branch, PR...)
73
+ copilot-logs/ copilot's own --log-dir
74
+ worktrees/<run_id>/<key>/ one git worktree per item
75
+ ```
76
+
77
+ ## Code style
78
+
79
+ Adopted from phitrain (rule ids in parentheses).
80
+
81
+ - Python 3.12+ syntax. Use `X | None`, never `Optional[X]`. Use builtin generics
82
+ (`dict[str, Any]`, `list[str]`); import only `Any`, `Literal`, `Annotated`, … from
83
+ `typing`. ABCs (`Callable`, `Iterable`, …) come from `collections.abc`. (R2)
84
+ - Every `.py` file starts with the two-line copyright/license header.
85
+ - Imports are top-level and absolute (`from cpmux.x import y`). Order: stdlib →
86
+ third-party → local, blank-separated.
87
+ - Public functions, classes, and their `__init__` carry Google-style docstrings
88
+ (single-sentence summary; one-line `Args:`/`Returns:`/`Raises:` entries). A regular
89
+ class keeps a one-line class summary and documents its constructor `Args:` on
90
+ `__init__`. Private helpers (`_name`) and framework-dispatched overrides (Textual
91
+ `compose`/`on_<event>`/lifecycle hooks) carry none. No semicolons or
92
+ `defaults to <X>` tails in entries. (R3, R13)
93
+ - A docstring keeps one blank line before its closing `"""`, and one blank line
94
+ after the closing `"""` before the first statement or field.
95
+ - Data classes (Pydantic models and `@dataclass`, which have no explicit `__init__`)
96
+ document every field in an `Attributes:` section, one line per field
97
+ (`name: what it holds.`).
98
+ - Logging uses `get_logger(__name__)` from `cpmux.logging`; **never `print()` in
99
+ library code** (the CLI presentation layer uses Rich and `typer.echo`). Diagnostic
100
+ `logger.warning`/`logger.error` use a backticked offender and trailing period:
101
+ `` f"`name=value` <verb-phrase>." ``; `logger.info`/`logger.debug` stay plain. (R14)
102
+ - Raised error messages use `` f"`<name>` <verb-phrase>[, but got <value>]." `` with a
103
+ trailing period and `is None`/`is True` prose. (R1)
104
+ - Validation uses `if/raise` with a specific exception, never `assert`. Bare `except:`
105
+ is forbidden.
106
+ - Comments explain **why**, not **what**: default to none, one-liner preference,
107
+ 3-line hard cap, no banner/section separators, no trailing period. (R8)
108
+ - Insert a single blank line at each phase transition in function bodies ≥ 12 LOC. (R11)
109
+ - Inline first; extract a helper/constant/parameter only on a second call-site. (R16)
110
+ - Double quotes for strings. Readable prose stays within 120 characters. (R9)
111
+
112
+ **Deliberate divergence: config uses Pydantic v2, not `@dataclass`.** phitrain models
113
+ config with `@dataclass` + `__post_init__` because it uses OmegaConf. cpmux's declarative
114
+ YAML needs string→item coercion, discriminated unions,
115
+ `${ENV}` interpolation, and precise validation errors, all idiomatic in Pydantic v2.
116
+ The config models in `config.py` and on-disk records in `engine/store.py` are therefore
117
+ Pydantic `BaseModel`s. Everything else follows phitrain.
118
+
119
+ ## CLI conventions (`ui/cli.py`)
120
+
121
+ - One `command()` function per verb, aggregated on the Typer `app`.
122
+ - Multi-word options use `--kebab-case` (e.g. `--dry-run`, `--transcribe-model`,
123
+ `--no-pr`); single-letter shortcuts are unique within a command.
124
+ - Validate inputs with `if/raise <SpecificError>`; surface operational failures with
125
+ `logger.error(...)` followed by `raise typer.Exit(1)`, with no hand-rolled `"Error:"`
126
+ prefix, no `typer.echo(..., err=True)`.
127
+ - Presentation (status tables, transcripts) uses Rich; raw machine output uses
128
+ `typer.echo`. Each `command()` carries a single-sentence docstring for `--help`.
129
+ - Short-lived subprocesses use `subprocess.run(..., capture_output=True, text=True,
130
+ check=False)`; streaming/long-running children use `asyncio` subprocesses.
131
+
132
+ ## Tests
133
+
134
+ - Tests mirror the source layout: `tests/<subpackage>/test_<module>.py`, with foundation
135
+ modules tested at the `tests/` root and shared fixtures in `tests/conftest.py`.
136
+ - Test functions are named `test_<function_or_class_name>_<behavior>`: lead with the exact
137
+ function, method, or class under test (snake_cased, any leading underscore dropped), then
138
+ the behavior — e.g. `test_resolve_base_falls_back_to_head`,
139
+ `test_run_paths_resolve_under_run_dir`. They are plain functions with no docstrings or type
140
+ hints.
141
+ - Asserts are bare `assert <expr>`, with no failure-message strings; the test name carries
142
+ the intent. (R15)
143
+
144
+ ## Tooling
145
+
146
+ black + isort (`profile = black`) + flake8, all at line-length 120, wired through
147
+ `.pre-commit-config.yaml`.
148
+
149
+ ```bash
150
+ isort cpmux tests && black cpmux tests && flake8 cpmux tests
151
+ pytest
152
+ ```
153
+
154
+ ## Status and roadmap
155
+
156
+ - **Current:** foreground and detached (`--detach`) runs; `ls`/`attach` live monitor; an
157
+ interactive Textual `dash` (session list + live transcript + `/` search + `e` to drop into a
158
+ native `copilot --resume`); `enter`/`send` interaction; cross-session `search` (with `--fts`
159
+ over copilot's own index); `logs --follow`; `down`/`kill`/`rm`; per-item dev-server ports
160
+ (`port_base`); `depends_on` ordering; voice/text/audio plan composition; crash reconciliation
161
+ via the run owner.
162
+ - **Remaining:** ACP transport for live permission prompts; optional remote `/delegate` mode.
cpmux-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gustavo de Rosa
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.
cpmux-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: cpmux
3
+ Version: 0.1.0
4
+ Summary: Multiplexer for GitHub Copilot CLI agents
5
+ Project-URL: Homepage, https://github.com/gugarosa/cpmux
6
+ Project-URL: Repository, https://github.com/gugarosa/cpmux
7
+ Author: Gustavo de Rosa
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,cli,copilot,multiplexer,orchestration,tmux,worktree
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Build Tools
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: click>=8.0
21
+ Requires-Dist: pydantic>=2.5
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: rich>=13.0
24
+ Requires-Dist: textual>=0.80
25
+ Requires-Dist: typer>=0.12
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Provides-Extra: voice
29
+ Requires-Dist: faster-whisper>=1.0; extra == 'voice'
30
+ Requires-Dist: sounddevice>=0.4; extra == 'voice'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # cpmux
34
+
35
+ **A declarative multiplexer for GitHub Copilot CLI agents: "tmuxinator for `copilot` sessions."**
36
+
37
+ Write one YAML file with a shared system prompt and a task list. cpmux starts one headless
38
+ `copilot` session per task, each in its own git worktree and branch, and opens a draft PR.
39
+ Monitor and steer all sessions.
40
+
41
+ ## Install
42
+
43
+ Requires macOS or Linux, Python ≥ 3.12, and the [`copilot`](https://docs.github.com/copilot/how-tos/copilot-cli),
44
+ `git`, and `gh` CLIs on your `PATH`.
45
+
46
+ ```bash
47
+ pip install git+https://github.com/gugarosa/cpmux
48
+ ```
49
+
50
+ Or from source, for development:
51
+
52
+ ```bash
53
+ git clone https://github.com/gugarosa/cpmux
54
+ cd cpmux
55
+ pip install -e .
56
+ ```
57
+
58
+ ## Quickstart
59
+
60
+ From the root of the GitHub repository you want to change, create `cpmux.yml` (or run
61
+ `cpmux init` for a starter):
62
+
63
+ ```yaml
64
+ system: |
65
+ Make the smallest change that fully addresses the task.
66
+ Follow the repository's conventions and add or update tests.
67
+
68
+ items:
69
+ - Fix the broken install link in the README
70
+ - name: pagination-regression
71
+ prompt: Add a regression test for the pagination helper.
72
+ ```
73
+
74
+ Preview the plan, start the sessions in the background, and watch the run:
75
+
76
+ ```bash
77
+ cpmux up --dry-run # preview the resolved plan
78
+ cpmux up --yes # start the run in the background
79
+ cpmux attach # watch it (Ctrl-C stops watching, not the run)
80
+ ```
81
+
82
+ By default, `cpmux up` starts the run in the background and opens one draft PR per item.
83
+ Pass `--foreground` to stay attached and watch inline (Ctrl-C then stops the run).
84
+
85
+ Each PR's title and description are written by the session itself from the changes it made,
86
+ following the target repository's pull-request template when one exists. If a session
87
+ doesn't produce one, cpmux falls back to the item name and prompt.
88
+
89
+ ## The cpmux file
90
+
91
+ A file has a shared `system` prompt, run-wide `defaults`, and `items`. Each item is either a
92
+ prompt string or a mapping:
93
+
94
+ ```yaml
95
+ system: |
96
+ Make the smallest change that fully fixes the issue, follow the surrounding
97
+ conventions, and add or update a test.
98
+
99
+ defaults:
100
+ model: gpt-5.5 # any `copilot --model` id
101
+ effort: medium # none | minimal | low | medium | high | xhigh | max
102
+ permissions: edit # readonly | edit | full (yolo)
103
+ base: main # branch to fork from and open PRs against
104
+ remote: origin # git remote to push branches and open PRs on
105
+ branch_template: cpmux/{slug} # each item's branch name; {slug} or {id}, e.g. gderosa/{slug}
106
+ concurrency: 6 # max sessions running at once (1–64)
107
+ deps: symlink # seed a worktree's node_modules: symlink | copy | install | skip
108
+ port_base: 3000 # give each item a unique port (3000, 3001, …) via $PORT
109
+ port_env: PORT # rename the port variable (default $PORT)
110
+ pr:
111
+ draft: true
112
+ labels: [cpmux]
113
+
114
+ items:
115
+ - Fix the flaky login test # bare string → key is the slug "fix-the-flaky-login-test"
116
+ - Paginate the notifications list
117
+
118
+ - name: dark-mode-contrast # mapping → key is the slug of `name`
119
+ prompt: Fix the dark-mode contrast on secondary buttons; it fails WCAG AA.
120
+ model: claude-opus-4.8
121
+ effort: high
122
+ paths: [src/components/buttons]
123
+ labels: [a11y]
124
+ depends_on: [fix-the-flaky-login-test]
125
+ ```
126
+
127
+ Item mappings accept `prompt`, `name`, `id`, `model`, `effort`, `permissions`, `base`,
128
+ `branch`, `labels`, `draft`, `paths`, `depends_on`, `env`, and `include_system`.
129
+
130
+ The `pr` block also accepts `title_template` and `body_template` — the fallback title and body
131
+ used when a session writes no `.cpmux-pr.md`; both expand `{name}`, `{slug}`, and `{prompt}`,
132
+ while `branch_template` expands `{slug}` and `{id}`. `permissions` may be a bare preset or a
133
+ mapping that extends one —
134
+ `{preset: edit, allow: [...], deny: [...], add_dir: [...], allow_url: [...]}` — adding
135
+ `copilot` tool and network rules. Only `full`/`yolo` let the agent run `git push` itself; the
136
+ other presets keep push denied so cpmux owns delivery. An optional top-level `version` selects
137
+ the schema (currently only `1`).
138
+
139
+ An item's **key** is its `id` when set, otherwise a slug of its `name` or `prompt`. Pass keys
140
+ to `enter`, `send`, `logs`, and `kill`; `cpmux ls` and `--dry-run` print them. Any string field
141
+ expands `${VAR}` and `${VAR:-default}` from the environment. Set `include_system: false` to
142
+ omit the shared prompt for an item.
143
+
144
+ Set `port_base` when items run dev servers: each item gets `port_base + index` in its
145
+ environment (as `$PORT`, or `port_env` to rename it), so parallel servers do not collide. An
146
+ item's own `env` takes precedence, and `env` values reach the session's subprocess.
147
+
148
+ ## Commands
149
+
150
+ Run-scoped commands accept `--run <id>` and default to the latest run.
151
+
152
+ | Group | Command | What it does |
153
+ |---|---|---|
154
+ | **Create** | `cpmux init [FILE]` | Write a starter plan (defaults to `cpmux.yml`). Flag: `--force/-f`. |
155
+ | | `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`. |
156
+ | **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`. |
157
+ | **Monitor** | `cpmux ls` | Snapshot each item's status, elapsed time, and activity. |
158
+ | | `cpmux attach` | Live, read-only monitor; reconnects to a background run (Ctrl-C to detach). |
159
+ | | `cpmux dash` | Interactive TUI: session list, live transcript, search. |
160
+ | | `cpmux logs KEY` | Print a transcript; `--follow/-f` to stream, `--raw` for the JSONL. |
161
+ | | `cpmux search QUERY` | Search across transcripts; `--all` for every run, `--regex`, `--fts` to rank via Copilot's index. |
162
+ | **Steer** | `cpmux enter KEY` | Drop into an interactive copilot session, resumed in place. |
163
+ | | `cpmux send KEY "…"` | Append a follow-up turn and print the reply. |
164
+ | | `cpmux kill KEY` | Stop one running session. Flag: `--yes/-y`. |
165
+ | **Teardown** | `cpmux down` | Stop a run's background daemon and any live sessions. Flag: `--yes/-y`. |
166
+ | | `cpmux rm` | Remove the run's git worktrees. Flags: `--yes/-y`, `--force/-f` (delete uncommitted work), `--purge` (also delete run history). |
167
+
168
+ ## Composing a plan
169
+
170
+ Compose a cpmux file in your editor by default, or from text, speech, or audio:
171
+
172
+ ```bash
173
+ cpmux plan issues.yml # compose in $EDITOR → cpmux file
174
+ cpmux plan issues.yml --text "fix the flaky login test and paginate the notifications"
175
+ cpmux plan issues.yml --voice # record from the mic (Enter to stop) instead
176
+ cpmux plan issues.yml --audio memo.wav # transcribe an existing recording instead
177
+ cpmux plan issues.yml --up # generate and launch it
178
+ ```
179
+
180
+ `cpmux plan` opens your `$EDITOR` to describe the work (or takes `--text`), then asks `copilot`
181
+ to produce a validated cpmux file. Add `--up` to launch it. With `--voice` or `--audio`,
182
+ [faster-whisper](https://github.com/SYSTRAN/faster-whisper) transcribes speech on-device.
183
+ Audio stays local. `--voice` shows a live transcript as you speak (a fast model streams
184
+ partials while recording; your chosen model produces the accurate final text on stop).
185
+
186
+ The `cpmux[voice]` extra installs `sounddevice` and `faster-whisper`. `--text` and the editor
187
+ need neither:
188
+
189
+ ```bash
190
+ pip install "cpmux[voice] @ git+https://github.com/gugarosa/cpmux"
191
+ brew install portaudio # macOS only: sounddevice needs PortAudio
192
+ ```
193
+
194
+ The default transcription model is `large-v3-turbo` (near-`large-v3` accuracy, much faster
195
+ decoding). It downloads on first use (~1.6 GB) and is cached. Pick a lighter one with
196
+ `--transcribe-model` (for example, `small`, `distil-large-v3`, or `base`); larger models are
197
+ more accurate but slower on CPU.
198
+
199
+ ## How it works
200
+
201
+ - **One item, one session.** Each task becomes a headless `copilot -p` run with a
202
+ pre-assigned `--session-id`.
203
+ - **Separate worktrees.** Each session runs in its own `git worktree` on a `cpmux/<slug>`
204
+ branch off `origin/<base>`.
205
+ - **cpmux owns delivery.** Sessions run with `git push` denied. cpmux commits each worktree and
206
+ opens one draft PR per item. With `--no-pr`, it commits locally and stops.
207
+ - **JSONL monitoring.** cpmux reads copilot's `--output-format json` event stream and writes it
208
+ to disk. Runs continue after detach and can be reattached. Crashed sessions resolve to a
209
+ terminal state.
210
+
211
+ ```
212
+ issues.yaml ──cpmux up──► session fix-login-test → worktree ─ branch ─ draft PR
213
+ system: … session paginate-list → worktree ─ branch ─ draft PR
214
+ items: … ───────────►session dark-mode-contrast→ worktree ─ branch ─ draft PR
215
+ session … (parallel · isolated)
216
+
217
+ monitor and steer: cpmux attach · dash · ls · logs · search
218
+ ```
219
+
220
+ ## What a run leaves on disk
221
+
222
+ cpmux writes under a gitignored `.cpmux/`:
223
+
224
+ ```
225
+ .cpmux/
226
+ runs/<run_id>/
227
+ manifest.json resolved run config
228
+ sessions/<key>/
229
+ prompt.md the exact prompt sent (system + item)
230
+ transcript.jsonl raw tee of copilot --output-format json
231
+ session.json per-session record (status, branch, PR url, …)
232
+ copilot-logs/ copilot's own --log-dir
233
+ worktrees/<run_id>/<key>/ one git worktree per item
234
+ ```
235
+
236
+ ## Examples
237
+
238
+ See [`examples/minimal.yaml`](examples/minimal.yaml) and a twelve-issue frontend run
239
+ in [`examples/frontend.yaml`](examples/frontend.yaml).
240
+
241
+ ## Development
242
+
243
+ ```bash
244
+ pip install -e .
245
+ pytest
246
+ ```
247
+
248
+ Conventions, architecture invariants, and roadmap live in [`CONVENTIONS.md`](CONVENTIONS.md).