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.
- cpmux-0.1.0/.github/workflows/ci.yml +42 -0
- cpmux-0.1.0/.github/workflows/release.yml +37 -0
- cpmux-0.1.0/.gitignore +18 -0
- cpmux-0.1.0/.pre-commit-config.yaml +25 -0
- cpmux-0.1.0/CHANGELOG.md +74 -0
- cpmux-0.1.0/CONVENTIONS.md +162 -0
- cpmux-0.1.0/LICENSE +21 -0
- cpmux-0.1.0/PKG-INFO +248 -0
- cpmux-0.1.0/README.md +216 -0
- cpmux-0.1.0/cpmux/__init__.py +4 -0
- cpmux-0.1.0/cpmux/__main__.py +7 -0
- cpmux-0.1.0/cpmux/config.py +595 -0
- cpmux-0.1.0/cpmux/engine/__init__.py +2 -0
- cpmux-0.1.0/cpmux/engine/copilot_store.py +94 -0
- cpmux-0.1.0/cpmux/engine/daemon.py +234 -0
- cpmux-0.1.0/cpmux/engine/interact.py +59 -0
- cpmux-0.1.0/cpmux/engine/session.py +115 -0
- cpmux-0.1.0/cpmux/engine/store.py +293 -0
- cpmux-0.1.0/cpmux/engine/supervisor.py +410 -0
- cpmux-0.1.0/cpmux/events.py +171 -0
- cpmux-0.1.0/cpmux/logging.py +38 -0
- cpmux-0.1.0/cpmux/py.typed +0 -0
- cpmux-0.1.0/cpmux/theme.py +198 -0
- cpmux-0.1.0/cpmux/ui/__init__.py +2 -0
- cpmux-0.1.0/cpmux/ui/cli.py +904 -0
- cpmux-0.1.0/cpmux/ui/dashboard.py +378 -0
- cpmux-0.1.0/cpmux/ui/render.py +72 -0
- cpmux-0.1.0/cpmux/ui/search.py +89 -0
- cpmux-0.1.0/cpmux/vcs/__init__.py +2 -0
- cpmux-0.1.0/cpmux/vcs/git.py +262 -0
- cpmux-0.1.0/cpmux/vcs/pr.py +240 -0
- cpmux-0.1.0/cpmux/voice/__init__.py +2 -0
- cpmux-0.1.0/cpmux/voice/recorder.py +246 -0
- cpmux-0.1.0/cpmux/voice/synthesizer.py +100 -0
- cpmux-0.1.0/cpmux/voice/transcriber.py +94 -0
- cpmux-0.1.0/examples/frontend.yaml +49 -0
- cpmux-0.1.0/examples/minimal.yaml +10 -0
- cpmux-0.1.0/pyproject.toml +52 -0
- cpmux-0.1.0/setup.cfg +8 -0
- cpmux-0.1.0/tests/conftest.py +16 -0
- cpmux-0.1.0/tests/engine/test_copilot_store.py +72 -0
- cpmux-0.1.0/tests/engine/test_daemon.py +143 -0
- cpmux-0.1.0/tests/engine/test_interact.py +26 -0
- cpmux-0.1.0/tests/engine/test_session.py +96 -0
- cpmux-0.1.0/tests/engine/test_store.py +135 -0
- cpmux-0.1.0/tests/engine/test_supervisor.py +114 -0
- cpmux-0.1.0/tests/test_config.py +354 -0
- cpmux-0.1.0/tests/test_events.py +112 -0
- cpmux-0.1.0/tests/test_logging.py +33 -0
- cpmux-0.1.0/tests/test_theme.py +57 -0
- cpmux-0.1.0/tests/ui/test_cli.py +329 -0
- cpmux-0.1.0/tests/ui/test_dashboard.py +183 -0
- cpmux-0.1.0/tests/ui/test_render.py +84 -0
- cpmux-0.1.0/tests/ui/test_search.py +56 -0
- cpmux-0.1.0/tests/vcs/test_git.py +123 -0
- cpmux-0.1.0/tests/vcs/test_pr.py +99 -0
- cpmux-0.1.0/tests/voice/test_recorder.py +15 -0
- cpmux-0.1.0/tests/voice/test_synthesizer.py +23 -0
- 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
|
cpmux-0.1.0/CHANGELOG.md
ADDED
|
@@ -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).
|