paircode 0.7.1__py3-none-any.whl

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.
paircode/journey.py ADDED
@@ -0,0 +1,63 @@
1
+ """Update JOURNEY.md as focuses open/close."""
2
+ from __future__ import annotations
3
+
4
+ import datetime as _dt
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from paircode.state import PaircodeState
9
+
10
+
11
+ _HISTORY_HEADER_RE = re.compile(r"^## History\s*$", re.MULTILINE)
12
+ _HISTORY_TABLE_RE = re.compile(
13
+ r"\| # \| Focus \| Opened \| Closed \| Iterations \| Result \|\n\|[-|\s]+\|\n"
14
+ )
15
+ _ACTIVE_FOCUS_RE = re.compile(
16
+ r"## Active focus\s*\n\n(.+?)\n\n##", re.DOTALL
17
+ )
18
+
19
+
20
+ def _now() -> str:
21
+ return _dt.datetime.now().strftime("%Y-%m-%d %H:%M")
22
+
23
+
24
+ def note_focus_opened(state: PaircodeState, focus_name: str) -> None:
25
+ """Update JOURNEY.md: set active focus, append history row."""
26
+ j = state.journey_path
27
+ if not j.exists():
28
+ return
29
+ text = j.read_text(encoding="utf-8")
30
+
31
+ # Replace the "Active focus" section
32
+ new_active = f"## Active focus\n\n**{focus_name}** — opened {_now()}\n\n##"
33
+ text = _ACTIVE_FOCUS_RE.sub(new_active, text, count=1)
34
+
35
+ # Append to history table
36
+ history_match = _HISTORY_TABLE_RE.search(text)
37
+ if history_match:
38
+ # Count existing rows in history table
39
+ rest = text[history_match.end():]
40
+ existing_rows = rest.split("\n\n")[0]
41
+ row_count = len([r for r in existing_rows.split("\n") if r.startswith("|")])
42
+ new_row = f"| {row_count + 1} | {focus_name} | {_now()} | — | 0 | — |\n"
43
+ text = text[:history_match.end()] + new_row + text[history_match.end():]
44
+
45
+ j.write_text(text, encoding="utf-8")
46
+
47
+
48
+ def note_focus_closed(state: PaircodeState, focus_name: str, iterations: int, result: str) -> None:
49
+ """Update JOURNEY.md: mark the focus row as closed with iteration count + result."""
50
+ j = state.journey_path
51
+ if not j.exists():
52
+ return
53
+ text = j.read_text(encoding="utf-8")
54
+ # Find row for this focus and update its Closed/Iterations/Result columns
55
+ pattern = re.compile(
56
+ rf"(\| \d+ \| {re.escape(focus_name)} \| [^|]+\| )—( \| )0( \| )—( \|)"
57
+ )
58
+ text = pattern.sub(
59
+ rf"\g<1>{_now()}\g<2>{iterations}\g<3>{result}\g<4>",
60
+ text,
61
+ count=1,
62
+ )
63
+ j.write_text(text, encoding="utf-8")
paircode/runner.py ADDED
@@ -0,0 +1,114 @@
1
+ """Invoke peer LLM CLIs as subprocesses, capture output to .md files.
2
+
3
+ This is the "spawn an LLM, write its answer to disk" primitive. Each CLI
4
+ has different flags for non-interactive / prompt-mode; we normalize them
5
+ here so upstream code just says "ask <peer> this prompt, save to <path>".
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+
15
+ DEFAULT_TIMEOUT_SECONDS = 600 # 10 min per peer call — generous for cold research
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class PeerRunResult:
20
+ peer_id: str
21
+ cli: str
22
+ ok: bool
23
+ stdout: str
24
+ stderr: str
25
+ duration_s: float
26
+ command: list[str]
27
+
28
+
29
+ def _cli_command(cli: str, prompt: str, model: Optional[str]) -> list[str]:
30
+ """Return the subprocess argv to invoke `cli` non-interactively with `prompt`."""
31
+ if cli == "claude":
32
+ # Claude Code: -p is non-interactive (print) mode
33
+ cmd = ["claude", "-p", prompt]
34
+ if model:
35
+ cmd.extend(["--model", model])
36
+ return cmd
37
+ if cli == "codex":
38
+ # Codex CLI: `codex exec` is non-interactive; --dangerously-bypass needed to
39
+ # write files / skip approvals when running headless.
40
+ cmd = ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox", prompt]
41
+ if model:
42
+ cmd.extend(["--model", model])
43
+ return cmd
44
+ if cli == "gemini":
45
+ cmd = ["gemini", "-p", prompt]
46
+ if model:
47
+ cmd.extend(["--model", model])
48
+ return cmd
49
+ if cli == "ollama":
50
+ # Ollama needs a model name
51
+ return ["ollama", "run", model or "llama3.1", prompt]
52
+ # Unknown CLI — best-effort: pass prompt as last arg
53
+ return [cli, prompt]
54
+
55
+
56
+ def run_peer(
57
+ peer_id: str,
58
+ cli: str,
59
+ prompt: str,
60
+ output_path: Path,
61
+ model: Optional[str] = None,
62
+ timeout_s: int = DEFAULT_TIMEOUT_SECONDS,
63
+ ) -> PeerRunResult:
64
+ """Run one peer LLM against `prompt`, write its stdout to `output_path`.
65
+
66
+ Returns a PeerRunResult capturing success/failure + full stdout + stderr.
67
+ The output file is written regardless of success (empty or error-captured
68
+ text) so that every invocation leaves a file-trace on disk.
69
+ """
70
+ import time
71
+
72
+ cmd = _cli_command(cli, prompt, model)
73
+ start = time.monotonic()
74
+ try:
75
+ proc = subprocess.run(
76
+ cmd,
77
+ capture_output=True,
78
+ text=True,
79
+ timeout=timeout_s,
80
+ check=False,
81
+ )
82
+ duration = time.monotonic() - start
83
+ ok = proc.returncode == 0
84
+ stdout, stderr = proc.stdout, proc.stderr
85
+ except FileNotFoundError:
86
+ duration = time.monotonic() - start
87
+ ok = False
88
+ stdout = ""
89
+ stderr = f"{cli} binary not found on PATH"
90
+ except subprocess.TimeoutExpired:
91
+ duration = time.monotonic() - start
92
+ ok = False
93
+ stdout = ""
94
+ stderr = f"{cli} timed out after {timeout_s}s"
95
+
96
+ output_path.parent.mkdir(parents=True, exist_ok=True)
97
+ header = (
98
+ f"<!-- peer_id: {peer_id} -->\n"
99
+ f"<!-- cli: {cli} -->\n"
100
+ f"<!-- model: {model or '(default)'} -->\n"
101
+ f"<!-- duration_s: {duration:.1f} -->\n"
102
+ f"<!-- ok: {ok} -->\n\n"
103
+ )
104
+ body = stdout if ok else f"# Peer run FAILED\n\n```\n{stderr}\n```\n\n{stdout}"
105
+ output_path.write_text(header + body, encoding="utf-8")
106
+ return PeerRunResult(
107
+ peer_id=peer_id,
108
+ cli=cli,
109
+ ok=ok,
110
+ stdout=stdout,
111
+ stderr=stderr,
112
+ duration_s=duration,
113
+ command=cmd,
114
+ )
paircode/seal.py ADDED
@@ -0,0 +1,52 @@
1
+ """Seal a stage — copy the latest version per peer to {peer}-FINAL.md.
2
+
3
+ Sealing is the declarative exit signal for a stage. Once alpha-FINAL.md and
4
+ each peer's *-FINAL.md exist, the stage is considered complete and the next
5
+ stage can consume its outputs.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import shutil
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+
15
+ VERSION_RE = re.compile(r"^(?P<peer>[a-z0-9][a-z0-9-]*?)-v(?P<version>\d+)\.md$")
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class SealedFile:
20
+ peer_id: str
21
+ source: Path
22
+ final: Path
23
+
24
+
25
+ def discover_latest_versions(stage_dir: Path) -> dict[str, Path]:
26
+ """Walk a stage dir, group files by peer_id, return peer_id → highest-version file."""
27
+ latest: dict[str, tuple[int, Path]] = {}
28
+ for p in stage_dir.iterdir():
29
+ if not p.is_file() or not p.name.endswith(".md"):
30
+ continue
31
+ m = VERSION_RE.match(p.name)
32
+ if not m:
33
+ continue
34
+ peer_id = m.group("peer")
35
+ version = int(m.group("version"))
36
+ if peer_id not in latest or version > latest[peer_id][0]:
37
+ latest[peer_id] = (version, p)
38
+ return {pid: path for pid, (_, path) in latest.items()}
39
+
40
+
41
+ def seal_stage(stage_dir: Path) -> list[SealedFile]:
42
+ """For each peer in the stage, copy their highest-version file to {peer}-FINAL.md.
43
+
44
+ Idempotent: re-running re-seals to the current latest. No-op if no versioned
45
+ files found.
46
+ """
47
+ sealed: list[SealedFile] = []
48
+ for peer_id, src in discover_latest_versions(stage_dir).items():
49
+ final_path = stage_dir / f"{peer_id}-FINAL.md"
50
+ shutil.copy2(src, final_path)
51
+ sealed.append(SealedFile(peer_id=peer_id, source=src, final=final_path))
52
+ return sealed
paircode/state.py ADDED
@@ -0,0 +1,162 @@
1
+ """Read/write paircode state in .paircode/ directories.
2
+
3
+ The .paircode/ layout (from diary/001-step-a-architecture.md):
4
+
5
+ .paircode/
6
+ JOURNEY.md ← fleet log, focus transitions
7
+ peers.yaml ← roster
8
+ peers/
9
+ peer-a-{cli}/ ← per-peer working dir (+profile.md, +code if full-fork)
10
+ focus-01-{slug}/
11
+ FOCUS.md
12
+ research/
13
+ plan/
14
+ execute/
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import datetime as _dt
19
+ import re
20
+ from dataclasses import dataclass
21
+ from importlib import resources
22
+ from pathlib import Path
23
+ from typing import Iterable
24
+
25
+ import yaml
26
+
27
+
28
+ PAIRCODE_DIRNAME = ".paircode"
29
+ PEERS_FILE = "peers.yaml"
30
+ JOURNEY_FILE = "JOURNEY.md"
31
+ FOCUS_FILE = "FOCUS.md"
32
+
33
+
34
+ def _now_iso() -> str:
35
+ return _dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
36
+
37
+
38
+ def _read_template(name: str) -> str:
39
+ return resources.files("paircode.templates").joinpath(name).read_text(encoding="utf-8")
40
+
41
+
42
+ def _render(template: str, vars: dict[str, str]) -> str:
43
+ out = template
44
+ for k, v in vars.items():
45
+ out = out.replace("{{" + k + "}}", v)
46
+ return out
47
+
48
+
49
+ def _slugify(text: str, max_len: int = 40) -> str:
50
+ slug = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
51
+ return slug[:max_len].rstrip("-") or "unnamed"
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class PaircodeState:
56
+ root: Path # path to .paircode/ dir itself
57
+ journey_path: Path
58
+ peers_path: Path
59
+ peers_dir: Path
60
+ focus_dirs: list[Path] # sorted by name
61
+
62
+ @property
63
+ def project_root(self) -> Path:
64
+ return self.root.parent
65
+
66
+ @property
67
+ def active_focus(self) -> Path | None:
68
+ """Latest focus by directory-name order (focus-NN prefix sorts naturally)."""
69
+ return self.focus_dirs[-1] if self.focus_dirs else None
70
+
71
+ @property
72
+ def focus_count(self) -> int:
73
+ return len(self.focus_dirs)
74
+
75
+
76
+ def find_paircode(start: Path | None = None) -> PaircodeState | None:
77
+ """Walk up from `start` (or cwd) looking for .paircode/. Return None if absent."""
78
+ if start is None:
79
+ start = Path.cwd()
80
+ start = start.resolve()
81
+ for ancestor in [start, *start.parents]:
82
+ root = ancestor / PAIRCODE_DIRNAME
83
+ if root.is_dir():
84
+ return load_state(root)
85
+ return None
86
+
87
+
88
+ def load_state(root: Path) -> PaircodeState:
89
+ focus_dirs = sorted(
90
+ [d for d in root.iterdir() if d.is_dir() and d.name.startswith("focus-")]
91
+ )
92
+ return PaircodeState(
93
+ root=root,
94
+ journey_path=root / JOURNEY_FILE,
95
+ peers_path=root / PEERS_FILE,
96
+ peers_dir=root / "peers",
97
+ focus_dirs=focus_dirs,
98
+ )
99
+
100
+
101
+ def init_paircode(project_root: Path | None = None, force: bool = False) -> PaircodeState:
102
+ """Bootstrap .paircode/ in `project_root` (or cwd). Returns the new state."""
103
+ if project_root is None:
104
+ project_root = Path.cwd()
105
+ project_root = project_root.resolve()
106
+ root = project_root / PAIRCODE_DIRNAME
107
+ if root.exists() and not force:
108
+ raise FileExistsError(
109
+ f"{root} already exists. Use --force to overwrite, or `paircode status` to inspect."
110
+ )
111
+ root.mkdir(parents=True, exist_ok=True)
112
+ (root / "peers").mkdir(exist_ok=True)
113
+
114
+ journey = _render(
115
+ _read_template("JOURNEY.md"),
116
+ {"project_name": project_root.name, "created_at": _now_iso()},
117
+ )
118
+ (root / JOURNEY_FILE).write_text(journey, encoding="utf-8")
119
+
120
+ (root / PEERS_FILE).write_text(_read_template("peers.yaml"), encoding="utf-8")
121
+
122
+ return load_state(root)
123
+
124
+
125
+ def open_focus(state: PaircodeState, name: str, prompt: str | None = None) -> Path:
126
+ """Open a new focus dir `focus-NN-{slug}/`. Returns the focus path."""
127
+ next_num = state.focus_count + 1
128
+ slug = _slugify(name)
129
+ focus_dir = state.root / f"focus-{next_num:02d}-{slug}"
130
+ if focus_dir.exists():
131
+ raise FileExistsError(f"{focus_dir} already exists")
132
+ focus_dir.mkdir()
133
+ for stage in ("research", "plan", "execute"):
134
+ (focus_dir / stage).mkdir()
135
+ (focus_dir / "research" / "reviews").mkdir()
136
+ (focus_dir / "plan" / "reviews").mkdir()
137
+
138
+ focus_md = _render(
139
+ _read_template("FOCUS.md"),
140
+ {
141
+ "focus_name": name,
142
+ "created_at": _now_iso(),
143
+ "prompt": prompt or "(none given — edit this file to set the prompt)",
144
+ },
145
+ )
146
+ (focus_dir / FOCUS_FILE).write_text(focus_md, encoding="utf-8")
147
+ return focus_dir
148
+
149
+
150
+ def read_peers(state: PaircodeState) -> list[dict]:
151
+ """Parse peers.yaml into a list of peer dicts. Returns [] if empty/missing."""
152
+ if not state.peers_path.exists():
153
+ return []
154
+ data = yaml.safe_load(state.peers_path.read_text()) or {}
155
+ return list(data.get("peers") or [])
156
+
157
+
158
+ def write_peers(state: PaircodeState, peers: Iterable[dict]) -> None:
159
+ state.peers_path.write_text(
160
+ yaml.safe_dump({"peers": list(peers)}, sort_keys=False, default_flow_style=False),
161
+ encoding="utf-8",
162
+ )
@@ -0,0 +1,35 @@
1
+ # FOCUS — {{focus_name}}
2
+
3
+ **Opened:** {{created_at}}
4
+ **Prompt:** {{prompt}}
5
+
6
+ ## Goal
7
+
8
+ _Edit this section with the concrete goal of this focus._
9
+
10
+ ## Roster override
11
+
12
+ _Leave empty to inherit from `.paircode/peers.yaml`. Or list peer ids to include/exclude for this focus._
13
+
14
+ ```yaml
15
+ include: [] # if set, only these peers participate
16
+ exclude: [] # if set, these peers skip this focus
17
+ ```
18
+
19
+ ## Human gate
20
+
21
+ ```yaml
22
+ mode: auto # auto | manual_between_stages | manual_every_N_rounds | manual_always
23
+ max_rounds_per_stage: 20
24
+ convergence: 3_rounds_no_new_findings
25
+ ```
26
+
27
+ ## Stages
28
+
29
+ - [ ] research
30
+ - [ ] plan
31
+ - [ ] execute
32
+
33
+ ## Notes
34
+
35
+ _Captain's running notes for this focus._
@@ -0,0 +1,21 @@
1
+ # JOURNEY — {{project_name}}
2
+
3
+ > Fleet log for paircode orchestration. Captain steers; LLMs author.
4
+ > Initialized: {{created_at}}
5
+
6
+ ## Active focus
7
+
8
+ _None yet. Run `paircode focus <name>` to open one._
9
+
10
+ ## History
11
+
12
+ | # | Focus | Opened | Closed | Iterations | Result |
13
+ |---|---|---|---|---|---|
14
+
15
+ ## Roster
16
+
17
+ See `peers.yaml` at the root of `.paircode/` for the current peer lineup and per-peer config.
18
+
19
+ ## Notes
20
+
21
+ Add captain's notes here as the journey progresses.
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Run paircode — adversarial journey framework for LLM peer review. Bootstraps .paircode/ if missing, otherwise advances the active focus.
3
+ ---
4
+
5
+ You have access to the `paircode` CLI. It's a Python tool that orchestrates multi-LLM peer review (research → plan → execute) with file-traces on disk.
6
+
7
+ Behavior:
8
+
9
+ 1. **Parse `$ARGUMENTS`.** Everything after `/paircode` is passed as CLI args. If empty, run `paircode` with no args (it prints status or bootstraps).
10
+ 2. **Run the CLI.** Invoke `paircode $ARGUMENTS` via Bash. Capture stdout and stderr.
11
+ 3. **Relay output to the user.** Show what paircode printed. If it asked a question (e.g., "accept proposed roster? y/n"), the user answers in the next turn and you pass their answer back via `paircode` again.
12
+ 4. **If `paircode` spawns its own LLM subprocesses** (for research/plan/execute stages), it handles that internally — you don't need to manage the loop. Just relay status updates back to the captain.
13
+ 5. **If the user's intent was a topic-level drive** (e.g., `/paircode "build a widget thing"`), invoke `paircode drive "<topic>"`.
14
+
15
+ Common subcommands you'll see:
16
+ - `paircode install` — register /paircode in Claude/Codex/Gemini (user already ran it if this command exists)
17
+ - `paircode status` — summarize .paircode/ state in cwd
18
+ - `paircode handshake` — detect installed LLM CLIs, propose peer roster
19
+ - `paircode focus <name>` — open a new focus inside .paircode/
20
+ - `paircode stage {research|plan|execute}` — run one peer-review round at that stage
21
+ - `paircode drive "<topic>"` — high-level: open focus, run research → plan → execute sequentially
22
+
23
+ Always show the captain what happened. Brief, honest, actionable.
@@ -0,0 +1,13 @@
1
+ # paircode — available via CLI
2
+
3
+ When working in this project, you have access to the `paircode` command-line tool. It orchestrates multi-LLM peer review across research → plan → execute stages with file-traces stored in `.paircode/`.
4
+
5
+ Invoke it via shell when relevant:
6
+
7
+ - `paircode status` — show current `.paircode/` state
8
+ - `paircode drive "<topic>"` — high-level workflow driver
9
+ - `paircode stage research` / `plan` / `execute` — run one peer-review round
10
+
11
+ The captain (user) may ask you to act as a peer reviewer to claude's work. Read files under `.paircode/focus-NN-*/` to see what's been produced; write your review files into the same focus dir.
12
+
13
+ File-traces on disk are the communication medium between LLMs. Always read relevant `.paircode/*.md` files before reviewing, and always write your output to `.paircode/` when participating.
@@ -0,0 +1,20 @@
1
+ # paircode peer roster — edit freely.
2
+ #
3
+ # alpha is the project itself — the primary developer (usually you + your
4
+ # main LLM). No entry needed for alpha; its "code" is whatever lives at
5
+ # project root.
6
+ #
7
+ # Every other LLM that participates is a peer. Each peer gets an entry
8
+ # below AND a folder at .paircode/peers/peer-N-{cli}/.
9
+ #
10
+ # Fields:
11
+ # id — peer-a, peer-b, peer-c, ... (unique, immutable)
12
+ # cli — the command-line binary used to invoke this peer (claude/codex/gemini/ollama/aider/...)
13
+ # model — specific model string (optional; CLI default if omitted)
14
+ # mode — full-fork | pair-code | opinion-only
15
+ # priority — high | medium | low (used when budget is tight)
16
+ # daily_budget — advisory only for now; paircode logs when a peer sits out
17
+ #
18
+ # Example roster (auto-generated by `paircode handshake`):
19
+
20
+ peers: []
@@ -0,0 +1,35 @@
1
+ # FOCUS — {{focus_name}}
2
+
3
+ **Opened:** {{created_at}}
4
+ **Prompt:** {{prompt}}
5
+
6
+ ## Goal
7
+
8
+ _Edit this section with the concrete goal of this focus._
9
+
10
+ ## Roster override
11
+
12
+ _Leave empty to inherit from `.paircode/peers.yaml`. Or list peer ids to include/exclude for this focus._
13
+
14
+ ```yaml
15
+ include: [] # if set, only these peers participate
16
+ exclude: [] # if set, these peers skip this focus
17
+ ```
18
+
19
+ ## Human gate
20
+
21
+ ```yaml
22
+ mode: auto # auto | manual_between_stages | manual_every_N_rounds | manual_always
23
+ max_rounds_per_stage: 20
24
+ convergence: 3_rounds_no_new_findings
25
+ ```
26
+
27
+ ## Stages
28
+
29
+ - [ ] research
30
+ - [ ] plan
31
+ - [ ] execute
32
+
33
+ ## Notes
34
+
35
+ _Captain's running notes for this focus._
@@ -0,0 +1,21 @@
1
+ # JOURNEY — {{project_name}}
2
+
3
+ > Fleet log for paircode orchestration. Captain steers; LLMs author.
4
+ > Initialized: {{created_at}}
5
+
6
+ ## Active focus
7
+
8
+ _None yet. Run `paircode focus <name>` to open one._
9
+
10
+ ## History
11
+
12
+ | # | Focus | Opened | Closed | Iterations | Result |
13
+ |---|---|---|---|---|---|
14
+
15
+ ## Roster
16
+
17
+ See `peers.yaml` at the root of `.paircode/` for the current peer lineup and per-peer config.
18
+
19
+ ## Notes
20
+
21
+ Add captain's notes here as the journey progresses.
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Run paircode — adversarial journey framework for LLM peer review. Bootstraps .paircode/ if missing, otherwise advances the active focus.
3
+ ---
4
+
5
+ You have access to the `paircode` CLI. It's a Python tool that orchestrates multi-LLM peer review (research → plan → execute) with file-traces on disk.
6
+
7
+ Behavior:
8
+
9
+ 1. **Parse `$ARGUMENTS`.** Everything after `/paircode` is passed as CLI args. If empty, run `paircode` with no args (it prints status or bootstraps).
10
+ 2. **Run the CLI.** Invoke `paircode $ARGUMENTS` via Bash. Capture stdout and stderr.
11
+ 3. **Relay output to the user.** Show what paircode printed. If it asked a question (e.g., "accept proposed roster? y/n"), the user answers in the next turn and you pass their answer back via `paircode` again.
12
+ 4. **If `paircode` spawns its own LLM subprocesses** (for research/plan/execute stages), it handles that internally — you don't need to manage the loop. Just relay status updates back to the captain.
13
+ 5. **If the user's intent was a topic-level drive** (e.g., `/paircode "build a widget thing"`), invoke `paircode drive "<topic>"`.
14
+
15
+ Common subcommands you'll see:
16
+ - `paircode install` — register /paircode in Claude/Codex/Gemini (user already ran it if this command exists)
17
+ - `paircode status` — summarize .paircode/ state in cwd
18
+ - `paircode handshake` — detect installed LLM CLIs, propose peer roster
19
+ - `paircode focus <name>` — open a new focus inside .paircode/
20
+ - `paircode stage {research|plan|execute}` — run one peer-review round at that stage
21
+ - `paircode drive "<topic>"` — high-level: open focus, run research → plan → execute sequentially
22
+
23
+ Always show the captain what happened. Brief, honest, actionable.
@@ -0,0 +1,13 @@
1
+ # paircode — available via CLI
2
+
3
+ When working in this project, you have access to the `paircode` command-line tool. It orchestrates multi-LLM peer review across research → plan → execute stages with file-traces stored in `.paircode/`.
4
+
5
+ Invoke it via shell when relevant:
6
+
7
+ - `paircode status` — show current `.paircode/` state
8
+ - `paircode drive "<topic>"` — high-level workflow driver
9
+ - `paircode stage research` / `plan` / `execute` — run one peer-review round
10
+
11
+ The captain (user) may ask you to act as a peer reviewer to claude's work. Read files under `.paircode/focus-NN-*/` to see what's been produced; write your review files into the same focus dir.
12
+
13
+ File-traces on disk are the communication medium between LLMs. Always read relevant `.paircode/*.md` files before reviewing, and always write your output to `.paircode/` when participating.
@@ -0,0 +1,20 @@
1
+ # paircode peer roster — edit freely.
2
+ #
3
+ # alpha is the project itself — the primary developer (usually you + your
4
+ # main LLM). No entry needed for alpha; its "code" is whatever lives at
5
+ # project root.
6
+ #
7
+ # Every other LLM that participates is a peer. Each peer gets an entry
8
+ # below AND a folder at .paircode/peers/peer-N-{cli}/.
9
+ #
10
+ # Fields:
11
+ # id — peer-a, peer-b, peer-c, ... (unique, immutable)
12
+ # cli — the command-line binary used to invoke this peer (claude/codex/gemini/ollama/aider/...)
13
+ # model — specific model string (optional; CLI default if omitted)
14
+ # mode — full-fork | pair-code | opinion-only
15
+ # priority — high | medium | low (used when budget is tight)
16
+ # daily_budget — advisory only for now; paircode logs when a peer sits out
17
+ #
18
+ # Example roster (auto-generated by `paircode handshake`):
19
+
20
+ peers: []