paircode 0.12.4__py3-none-any.whl → 0.13.2__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/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  See README.md and diary/001-step-a-architecture.md for the full design.
4
4
  """
5
- __version__ = "0.12.3"
5
+ __version__ = "0.13.1"
paircode/cli.py CHANGED
@@ -28,6 +28,7 @@ from paircode.handshake import propose_roster, proposed_as_yaml_dicts
28
28
  from paircode.installer import install_all, uninstall_all
29
29
  from paircode.runner import run_peer
30
30
  from paircode.state import (
31
+ ensure_gitignore,
31
32
  ensure_peer_dirs,
32
33
  find_paircode,
33
34
  init_paircode,
@@ -108,6 +109,9 @@ def ensure_scaffold() -> None:
108
109
  if state is None:
109
110
  state = init_paircode()
110
111
  did_init = True
112
+ else:
113
+ # Retroactively protect projects whose .paircode/ predates this guard.
114
+ ensure_gitignore(state.project_root)
111
115
 
112
116
  peers = read_peers(state)
113
117
  proposed_count = 0
@@ -324,12 +328,15 @@ def peerlab() -> None:
324
328
  @peerlab.command("ensure")
325
329
  def peerlab_ensure() -> None:
326
330
  """Scaffold .peerlab/<peer-id>/ per peer; seed from project root on first
327
- creation; git init per lab; add .peerlab/ to outer .gitignore. Idempotent."""
328
- from paircode.peerlab import ensure_peer_labs
331
+ creation; git init per lab; add .peerlab/ to outer .gitignore. Idempotent.
329
332
 
330
- state = find_paircode()
333
+ Uses .peerlab/peers.yaml (peerlab's own roster), not .paircode/.
334
+ """
335
+ from paircode.peerlab import ensure_peer_labs, find_peerlab, init_peerlab
336
+
337
+ state = find_peerlab()
331
338
  if state is None:
332
- state = init_paircode()
339
+ state = init_peerlab()
333
340
  results = ensure_peer_labs(state)
334
341
  for r in results:
335
342
  if r.status == "created":
@@ -350,14 +357,14 @@ def peerlab_ensure() -> None:
350
357
  help="Apply cliworker speed flags")
351
358
  def peerlab_invoke(peer_id: str, prompt: str, out_path: str | None, timeout: int, fast: bool) -> None:
352
359
  """Fire a peer CLI with cwd set to its lab. Peer works in-place."""
353
- from paircode.peerlab import peer_lab_path
360
+ from paircode.peerlab import find_peerlab, peer_lab_path, read_peerlab_peers
354
361
 
355
- state = find_paircode()
362
+ state = find_peerlab()
356
363
  if state is None:
357
364
  raise click.ClickException(
358
- "No .paircode/ found. Run `paircode ensure-scaffold` + `paircode peerlab ensure` first."
365
+ "No .peerlab/ found. Run `paircode peerlab ensure` first."
359
366
  )
360
- peers = read_peers(state)
367
+ peers = read_peerlab_peers(state)
361
368
  peer = next((p for p in peers if p.get("id") == peer_id), None)
362
369
  if peer is None:
363
370
  known = [p.get("id") for p in peers]
@@ -368,14 +375,21 @@ def peerlab_invoke(peer_id: str, prompt: str, out_path: str | None, timeout: int
368
375
  f"Lab dir missing: {lab}. Run `paircode peerlab ensure` first."
369
376
  )
370
377
 
371
- # Preamble so the peer knows it's in a lab with its own git
378
+ # Preamble: peer knows where its lab is + that cross-reads outside the
379
+ # lab are allowed when the task says so (cross-review reads alpha's
380
+ # project root at ../../ and sibling peer labs at ../).
372
381
  framed_prompt = (
373
- f"You are the {peer_id} peer working in your own lab at {lab}.\n"
374
- f"Your cwd is already this lab. You have your own `.git/` here — commit\n"
375
- f"your work (stage + commit) before finishing so the team lead can read\n"
376
- f"the diff. Don't worry about alpha's repo; your lab is fully independent.\n"
382
+ f"You are the {peer_id} peer in a paircode peerlab run.\n"
383
+ f"Your own lab is at {lab} — that is your cwd. You have your own `.git/`\n"
384
+ f"here; commit your work in this lab before finishing so the team lead\n"
385
+ f"can read the diff. Modify files ONLY inside your own lab.\n"
386
+ f"\n"
387
+ f"The task below may instruct you to READ outside your lab — e.g.,\n"
388
+ f"alpha's project root at `../../` (alpha's lab) or sibling peer labs\n"
389
+ f"at `../<other-peer-id>/`. Read-only cross-reads are expected during\n"
390
+ f"cross-review rounds. Do not write outside your own lab.\n"
377
391
  f"\n"
378
- f"Work:\n"
392
+ f"Task:\n"
379
393
  f"{prompt}"
380
394
  )
381
395
 
@@ -423,15 +437,15 @@ def peerlab_invoke(peer_id: str, prompt: str, out_path: str | None, timeout: int
423
437
  @peerlab.command("list")
424
438
  def peerlab_list() -> None:
425
439
  """Show peer labs with creation status + HEAD commit (if any)."""
426
- from paircode.peerlab import peer_lab_path
440
+ from paircode.peerlab import find_peerlab, peer_lab_path, read_peerlab_peers
427
441
 
428
- state = find_paircode()
442
+ state = find_peerlab()
429
443
  if state is None:
430
- click.echo("No .paircode/ found. Nothing to list.")
444
+ click.echo("No .peerlab/ found. Run `paircode peerlab ensure` first.")
431
445
  return
432
- peers = read_peers(state)
446
+ peers = read_peerlab_peers(state)
433
447
  if not peers:
434
- click.echo("No peers in roster.")
448
+ click.echo("No peers in .peerlab/peers.yaml roster.")
435
449
  return
436
450
  ptable = Table(show_header=True, header_style="bold")
437
451
  ptable.add_column("peer")
@@ -458,6 +472,51 @@ def peerlab_list() -> None:
458
472
  console.print(ptable)
459
473
 
460
474
 
475
+ @peerlab.command("roster")
476
+ @click.option("--alpha", "alpha_cli", default=None,
477
+ help="CLI acting as alpha (excluded from peers unless last resort).")
478
+ @click.option("--peer", "peer_filter", default=None,
479
+ help="Narrow to a single peer id. Silently falls back if missing.")
480
+ @click.option("--peers", "peers_filter", default=None,
481
+ help="Comma-separated peer ids. Silently falls back if none match.")
482
+ def peerlab_roster(alpha_cli: str | None, peer_filter: str | None, peers_filter: str | None) -> None:
483
+ """Print peerlab peer ids, one per line — best-effort, never errors.
484
+
485
+ Reads .peerlab/peers.yaml (peerlab's own roster, fully independent of
486
+ .paircode/). Same filter semantics as `paircode roster`.
487
+ """
488
+ from paircode.peerlab import find_peerlab, read_peerlab_peers
489
+
490
+ state = find_peerlab()
491
+ if state is None:
492
+ return
493
+ all_peers = [p for p in read_peerlab_peers(state) if p.get("id")]
494
+
495
+ if peers_filter:
496
+ wanted = [s.strip() for s in peers_filter.split(",") if s.strip()]
497
+ filtered = [p for p in all_peers if p.get("id") in wanted]
498
+ elif peer_filter:
499
+ filtered = [p for p in all_peers if p.get("id") == peer_filter]
500
+ else:
501
+ filtered = list(all_peers)
502
+
503
+ def _exclude_alpha(peers: list[dict]) -> list[dict]:
504
+ if not alpha_cli:
505
+ return peers
506
+ return [p for p in peers if p.get("cli") != alpha_cli]
507
+
508
+ result = _exclude_alpha(filtered)
509
+ if not result and (peer_filter or peers_filter):
510
+ result = _exclude_alpha(all_peers)
511
+ if not result:
512
+ result = list(all_peers)
513
+
514
+ for p in result:
515
+ pid = p.get("id")
516
+ if pid:
517
+ click.echo(pid)
518
+
519
+
461
520
  # ---------------------------------------------------------------------------
462
521
  # Bare paircode — show state
463
522
  # ---------------------------------------------------------------------------
paircode/handshake.py CHANGED
@@ -16,6 +16,11 @@ class ProposedPeer:
16
16
  cli: str
17
17
  priority: str
18
18
  notes: str = ""
19
+ # Empty = let the CLI pick its own default model. Pin a specific model string
20
+ # (e.g. a Pro tier) to avoid the silent free-tier trap: an unpinned peer on a
21
+ # free/oauth login can quietly run a weak flash model and rubber-stamp consensus.
22
+ # Emitted into every generated roster so the field is discoverable, not hidden.
23
+ model: str = ""
19
24
 
20
25
 
21
26
  # Default ranking of peers when auto-proposing a roster.
@@ -25,7 +30,7 @@ _PEER_RANK: list[tuple[str, str, str]] = [
25
30
  # (cli, priority, notes)
26
31
  ("codex", "high", "Second opinion, good for silent-agreement hunts"),
27
32
  ("ollama", "medium", "Local unlimited; good if a capable model is pulled"),
28
- ("gemini", "low", "Free tier — use for quick opinions, not full forks"),
33
+ ("gemini", "low", "Pin model: to a Pro tier — unpinned + a free/oauth login silently runs weak flash"),
29
34
  ]
30
35
 
31
36
 
paircode/peerlab.py CHANGED
@@ -1,14 +1,25 @@
1
1
  """Per-peer independent labs — each peer owns a parallel implementation
2
2
  of the project, with its own `.git/` for independent history.
3
3
 
4
- Laid out under `.peerlab/<peer-id>/` at the project root, gitignored from
5
- the outer repo. Seeding is one-time on first creation (rsync from project
6
- root minus a standard exclude list). After that, each peer's lab evolves
7
- independently — the peer commits, diffs, experiments in its own git.
8
-
9
- Used by the `/peerlab` slash command (separate concept from `/paircode`):
10
- team lead fires each peer with cwd = its lab, peers do real work and commit,
11
- team lead reads the resulting diffs and synthesizes.
4
+ **Fully independent from `/paircode`.** `/peerlab` maintains its own state
5
+ under `.peerlab/` at the project root:
6
+
7
+ .peerlab/
8
+ peers.yaml # peerlab's own roster
9
+ alpha-critique.md # alpha's cross-review output (optional, written by slash command)
10
+ peer-a-codex/ # external peer lab (full parallel implementation, own .git)
11
+ peer-b-gemini/
12
+ ...
13
+
14
+ `/peerlab` does NOT read or write `.paircode/`. If you use both `/paircode`
15
+ and `/peerlab`, each maintains its own roster independently. That's
16
+ intentional — `/peerlab` is designed to be useful as a standalone concept.
17
+
18
+ Architecture: **alpha (the interactive LLM session, e.g. Claude Code) is
19
+ itself a peer**. Alpha's "lab" is the project root — alpha codes directly
20
+ in the real repo. This module manages the EXTERNAL peer labs (codex,
21
+ gemini, ollama, ...). During cross-review every participant reads every
22
+ other participant's code.
12
23
  """
13
24
  from __future__ import annotations
14
25
 
@@ -18,10 +29,11 @@ from dataclasses import dataclass
18
29
  from pathlib import Path
19
30
  from typing import Iterable
20
31
 
21
- from paircode.state import PaircodeState, read_peers
32
+ import yaml
22
33
 
23
34
 
24
35
  PEERLAB_DIRNAME = ".peerlab"
36
+ PEERLAB_PEERS_FILE = "peers.yaml"
25
37
 
26
38
  SEED_EXCLUDES: tuple[str, ...] = (
27
39
  ".git",
@@ -41,10 +53,6 @@ SEED_EXCLUDES: tuple[str, ...] = (
41
53
 
42
54
  GITIGNORE_BLOCK_HEADER = "# peerlab — per-peer independent parallel labs"
43
55
 
44
- # Defaults dropped into each new peer lab's own .gitignore so peer commits
45
- # don't accidentally include Python bytecode, editor junk, OS cruft.
46
- # If the lab inherited a .gitignore from the project root (rsync seed), we
47
- # APPEND this block — user's rules win, these are a safety net.
48
56
  LAB_GITIGNORE_BLOCK = """\
49
57
  # peerlab defaults — python + editor junk (keep peer commits clean)
50
58
  __pycache__/
@@ -66,6 +74,21 @@ htmlcov/
66
74
  LAB_GITIGNORE_MARKER = "peerlab defaults"
67
75
 
68
76
 
77
+ # ---------------------------------------------------------------------------
78
+ # PeerlabState — independent of PaircodeState
79
+ # ---------------------------------------------------------------------------
80
+
81
+ @dataclass(frozen=True)
82
+ class PeerlabState:
83
+ """`.peerlab/` state. Fully independent from `.paircode/`."""
84
+ root: Path # path to .peerlab/ dir
85
+ peers_path: Path # .peerlab/peers.yaml
86
+
87
+ @property
88
+ def project_root(self) -> Path:
89
+ return self.root.parent
90
+
91
+
69
92
  @dataclass(frozen=True)
70
93
  class EnsureResult:
71
94
  peer_id: str
@@ -73,16 +96,61 @@ class EnsureResult:
73
96
  status: str # "created" | "already-exists" | "missing-id"
74
97
 
75
98
 
99
+ def find_peerlab(start: Path | None = None) -> PeerlabState | None:
100
+ """Walk up from `start` (or cwd) looking for `.peerlab/`. Return None."""
101
+ if start is None:
102
+ start = Path.cwd()
103
+ start = start.resolve()
104
+ for ancestor in [start, *start.parents]:
105
+ root = ancestor / PEERLAB_DIRNAME
106
+ if root.is_dir():
107
+ return PeerlabState(root=root, peers_path=root / PEERLAB_PEERS_FILE)
108
+ return None
109
+
110
+
111
+ def init_peerlab(project_root: Path | None = None) -> PeerlabState:
112
+ """Bootstrap `.peerlab/` in `project_root` (or cwd).
113
+
114
+ Creates the dir, runs handshake to detect peer CLIs, writes a fresh
115
+ `peers.yaml` to `.peerlab/peers.yaml` (if missing). `handshake.propose_roster`
116
+ is pure — reads PATH, doesn't touch any paircode state on disk.
117
+ """
118
+ if project_root is None:
119
+ project_root = Path.cwd()
120
+ project_root = project_root.resolve()
121
+ root = project_root / PEERLAB_DIRNAME
122
+ root.mkdir(exist_ok=True)
123
+ peers_path = root / PEERLAB_PEERS_FILE
124
+ if not peers_path.exists():
125
+ # Pure-function import; doesn't trigger any paircode state writes.
126
+ from paircode.handshake import propose_roster, proposed_as_yaml_dicts
127
+
128
+ proposed = propose_roster()
129
+ write_peerlab_peers(peers_path, proposed_as_yaml_dicts(proposed))
130
+ return PeerlabState(root=root, peers_path=peers_path)
131
+
132
+
133
+ def read_peerlab_peers(state: PeerlabState) -> list[dict]:
134
+ """Parse `.peerlab/peers.yaml` into list of peer dicts. `[]` if missing."""
135
+ if not state.peers_path.exists():
136
+ return []
137
+ data = yaml.safe_load(state.peers_path.read_text(encoding="utf-8")) or {}
138
+ return list(data.get("peers") or [])
139
+
140
+
141
+ def write_peerlab_peers(peers_path: Path, peers: Iterable[dict]) -> None:
142
+ peers_path.write_text(
143
+ yaml.safe_dump({"peers": list(peers)}, sort_keys=False, default_flow_style=False),
144
+ encoding="utf-8",
145
+ )
146
+
147
+
76
148
  # ---------------------------------------------------------------------------
77
- # Helpersgitignore, rsync, git init, initial commit
149
+ # Outer-repo gitignore keep `.peerlab/` out of alpha's git
78
150
  # ---------------------------------------------------------------------------
79
151
 
80
152
  def ensure_gitignore(project_root: Path) -> bool:
81
- """Append `.peerlab/` to outer .gitignore if not already present.
82
-
83
- Returns True if the line was added (or the file was created), False if
84
- it was already there.
85
- """
153
+ """Append `.peerlab/` to outer `.gitignore` if not already present."""
86
154
  gitignore = project_root / ".gitignore"
87
155
  existing = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
88
156
  for line in existing.splitlines():
@@ -95,9 +163,12 @@ def ensure_gitignore(project_root: Path) -> bool:
95
163
  return True
96
164
 
97
165
 
166
+ # ---------------------------------------------------------------------------
167
+ # Per-lab helpers — rsync seed, git init, initial commit, lab .gitignore
168
+ # ---------------------------------------------------------------------------
169
+
98
170
  def _rsync_project(src: Path, dst: Path, excludes: Iterable[str] = SEED_EXCLUDES) -> None:
99
- """Copy src → dst with standard excludes. Uses rsync if available, falls
100
- back to `shutil.copytree` with an ignore function."""
171
+ """Copy src → dst with standard excludes. rsync if available, else shutil."""
101
172
  dst.mkdir(parents=True, exist_ok=True)
102
173
  rsync = shutil.which("rsync")
103
174
  if rsync:
@@ -108,13 +179,11 @@ def _rsync_project(src: Path, dst: Path, excludes: Iterable[str] = SEED_EXCLUDES
108
179
  subprocess.run(args, check=True)
109
180
  return
110
181
 
111
- # Pure-Python fallback: copytree with an ignore func.
112
182
  exclude_set = set(excludes)
113
183
 
114
184
  def _ignore(_directory: str, names: list[str]) -> list[str]:
115
185
  ignored = []
116
186
  for n in names:
117
- # Match exact name or a simple *-prefix/suffix wildcard
118
187
  if n in exclude_set:
119
188
  ignored.append(n)
120
189
  continue
@@ -125,23 +194,13 @@ def _rsync_project(src: Path, dst: Path, excludes: Iterable[str] = SEED_EXCLUDES
125
194
  ignored.append(n); break
126
195
  return ignored
127
196
 
128
- # copytree fails if dst exists — but our dst was just mkdir'd.
129
- # Remove empty dst and let copytree re-create.
130
197
  if dst.exists() and not any(dst.iterdir()):
131
198
  dst.rmdir()
132
199
  shutil.copytree(src, dst, ignore=_ignore)
133
200
 
134
201
 
135
202
  def ensure_lab_gitignore(lab: Path) -> bool:
136
- """Ensure the peer lab has Python/editor junk gitignored. Idempotent.
137
-
138
- If the lab already has a `.gitignore` (inherited via rsync from the
139
- project root), our block is appended so user rules win. If none exists
140
- (greenfield), our block IS the .gitignore.
141
-
142
- Returns True if the block was added this call, False if it was already
143
- there (marker detection).
144
- """
203
+ """Append the peerlab-defaults .gitignore block into the lab. Idempotent."""
145
204
  gitignore = lab / ".gitignore"
146
205
  existing = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
147
206
  if LAB_GITIGNORE_MARKER in existing:
@@ -152,10 +211,8 @@ def ensure_lab_gitignore(lab: Path) -> bool:
152
211
 
153
212
 
154
213
  def _git_init(lab: Path) -> None:
155
- """`git init` inside `lab/`. No-op if already inited."""
156
214
  if (lab / ".git").exists():
157
215
  return
158
- # Try modern flag first, fall back for older git.
159
216
  result = subprocess.run(
160
217
  ["git", "init", "--quiet", "--initial-branch=main", str(lab)],
161
218
  capture_output=True, text=True, check=False,
@@ -167,32 +224,22 @@ def _git_init(lab: Path) -> None:
167
224
  def _git_initial_commit(
168
225
  lab: Path, peer_id: str, message: str = "initial seed from project root"
169
226
  ) -> bool:
170
- """Stage everything + commit. Returns True if a commit was made.
171
-
172
- Authors the commit as the peer itself (`{peer_id} <{peer_id}@peerlab.local>`)
173
- via lab-local git config. Every subsequent peer commit inside this lab
174
- inherits that local identity — so `git log` in a peer's lab attributes
175
- all work to that peer, clean replay.
176
- """
227
+ """Stage everything + commit as peer_id. Returns True if a commit was made."""
177
228
  if not any(lab.iterdir()):
178
229
  return False
179
- # Local git config — scoped to this lab, doesn't touch global identity.
180
230
  subprocess.run(["git", "-C", str(lab), "config", "user.name", peer_id], check=False)
181
231
  subprocess.run(
182
232
  ["git", "-C", str(lab), "config", "user.email", f"{peer_id}@peerlab.local"],
183
233
  check=False,
184
234
  )
185
235
  subprocess.run(["git", "-C", str(lab), "add", "-A"], check=False)
186
- # Commit only if there's something staged
187
236
  diff_check = subprocess.run(
188
- ["git", "-C", str(lab), "diff", "--cached", "--quiet"],
189
- check=False,
237
+ ["git", "-C", str(lab), "diff", "--cached", "--quiet"], check=False,
190
238
  )
191
239
  if diff_check.returncode == 0:
192
- return False # nothing staged
240
+ return False
193
241
  subprocess.run(
194
- ["git", "-C", str(lab), "commit", "--quiet", "-m", message],
195
- check=False,
242
+ ["git", "-C", str(lab), "commit", "--quiet", "-m", message], check=False,
196
243
  )
197
244
  return True
198
245
 
@@ -201,41 +248,34 @@ def _git_initial_commit(
201
248
  # Public — ensure per-peer labs
202
249
  # ---------------------------------------------------------------------------
203
250
 
204
- def ensure_peer_labs(state: PaircodeState) -> list[EnsureResult]:
205
- """Scaffold `.peerlab/<peer-id>/` for every peer in `peers.yaml`.
251
+ def ensure_peer_labs(state: PeerlabState) -> list[EnsureResult]:
252
+ """Scaffold `.peerlab/<peer-id>/` for every peer in `.peerlab/peers.yaml`.
206
253
 
207
254
  Idempotent. On first creation of a lab:
208
- - mkdir `.peerlab/<peer-id>/`
255
+ - mkdir
209
256
  - rsync project root → lab (minus SEED_EXCLUDES)
210
- - `git init` inside lab (own repo)
211
- - initial commit so HEAD exists → peers can `git diff HEAD~1` immediately
257
+ - drop lab .gitignore
258
+ - `git init` inside lab
259
+ - initial commit authored as peer_id so HEAD exists + log attributes clean
212
260
 
213
261
  On subsequent calls: skip labs that already have `.git/` — never re-seed.
214
- Also ensures the outer `.gitignore` contains `.peerlab/` so the outer repo
215
- doesn't accidentally track peer labs.
262
+ Also ensures the outer `.gitignore` contains `.peerlab/`.
216
263
  """
217
264
  project_root = state.project_root
218
- peerlab_root = project_root / PEERLAB_DIRNAME
219
- peerlab_root.mkdir(exist_ok=True)
220
-
221
265
  ensure_gitignore(project_root)
222
266
 
223
267
  results: list[EnsureResult] = []
224
- for p in read_peers(state):
268
+ for p in read_peerlab_peers(state):
225
269
  pid = p.get("id") if isinstance(p, dict) else getattr(p, "id", None)
226
270
  if not pid:
227
- results.append(EnsureResult(peer_id="?", lab_path=peerlab_root, status="missing-id"))
271
+ results.append(EnsureResult(peer_id="?", lab_path=state.root, status="missing-id"))
228
272
  continue
229
- lab = peerlab_root / pid
273
+ lab = state.root / pid
230
274
  if (lab / ".git").exists():
231
275
  results.append(EnsureResult(peer_id=pid, lab_path=lab, status="already-exists"))
232
276
  continue
233
277
  lab.mkdir(exist_ok=True)
234
- # Seed from project root (might be empty for greenfield — rsync handles fine)
235
278
  _rsync_project(project_root, lab)
236
- # Drop peerlab-default .gitignore so peer commits don't pull in
237
- # __pycache__, .pytest_cache, editor junk etc. Appends if the lab
238
- # already inherited a .gitignore from the project root.
239
279
  ensure_lab_gitignore(lab)
240
280
  _git_init(lab)
241
281
  _git_initial_commit(lab, pid, message="initial seed from project root")
@@ -243,6 +283,6 @@ def ensure_peer_labs(state: PaircodeState) -> list[EnsureResult]:
243
283
  return results
244
284
 
245
285
 
246
- def peer_lab_path(state: PaircodeState, peer_id: str) -> Path:
286
+ def peer_lab_path(state: PeerlabState, peer_id: str) -> Path:
247
287
  """Return the absolute lab path for a peer id. Does not create."""
248
- return state.project_root / PEERLAB_DIRNAME / peer_id
288
+ return state.root / peer_id
paircode/state.py CHANGED
@@ -31,6 +31,8 @@ PEERS_FILE = "peers.yaml"
31
31
  JOURNEY_FILE = "JOURNEY.md"
32
32
  FOCUS_FILE = "FOCUS.md"
33
33
 
34
+ GITIGNORE_BLOCK_HEADER = "# paircode — adversarial multi-LLM peer review state"
35
+
34
36
 
35
37
  def _now_iso() -> str:
36
38
  return _dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -127,6 +129,24 @@ def ensure_peer_dirs(state: PaircodeState, proposed: Iterable) -> list[Path]:
127
129
  return created
128
130
 
129
131
 
132
+ def ensure_gitignore(project_root: Path) -> bool:
133
+ """Append `.paircode/` to outer `.gitignore` if not already present.
134
+
135
+ Mirrors `peerlab.ensure_gitignore`. Idempotent; returns True when a line
136
+ is added, False when `.paircode/` (or `.paircode`) is already listed.
137
+ """
138
+ gitignore = project_root / ".gitignore"
139
+ existing = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
140
+ for line in existing.splitlines():
141
+ s = line.strip()
142
+ if s in (".paircode/", ".paircode"):
143
+ return False
144
+ tail = existing.rstrip()
145
+ new_text = (tail + "\n\n" if tail else "") + GITIGNORE_BLOCK_HEADER + "\n.paircode/\n"
146
+ gitignore.write_text(new_text, encoding="utf-8")
147
+ return True
148
+
149
+
130
150
  def init_paircode(project_root: Path | None = None, force: bool = False) -> PaircodeState:
131
151
  """Bootstrap .paircode/ in `project_root` (or cwd). Returns the new state.
132
152
 
@@ -143,6 +163,7 @@ def init_paircode(project_root: Path | None = None, force: bool = False) -> Pair
143
163
  )
144
164
  root.mkdir(parents=True, exist_ok=True)
145
165
  (root / "sandbox").mkdir(exist_ok=True)
166
+ ensure_gitignore(project_root)
146
167
 
147
168
  journey = _render(
148
169
  _read_template("JOURNEY.md"),
@@ -1,34 +1,34 @@
1
1
  ---
2
- description: peerlab — each peer owns a parallel lab with its own git. Peers write real code, then cross-review each other's labs, then team lead synthesizes.
2
+ description: peerlab — alpha and peers each build in their own lab, then cross-review each other's code. Alpha is one of the peers.
3
3
  ---
4
4
 
5
- You are the team lead for a `/peerlab` run. Each peer has its own persistent lab at `.peerlab/<peer-id>/` with its own `.git/`. Peers **own** their labs. The flow is:
5
+ You are the orchestrator of a `/peerlab` run. In `/peerlab`, **alpha (this Claude session) is one of the peers** — alpha builds in the project root while external peers build in their own labs at `.peerlab/<peer-id>/`. Then every participant reads every other participant's code and writes a critique. Finally you (wearing the orchestrator hat) synthesize the full cross-review into a single chat message.
6
6
 
7
- 1. **Work round**every peer builds in its own lab.
8
- 2. **Cross-review round** — every peer reads the OTHER peers' labs and writes an honest critique (`CRITIQUES.md`) in its own lab. Peer-reviews-peer is the adversarial content; do NOT skip this.
9
- 3. **Team-lead synthesis** — alpha reads both the code diffs AND each peer's critiques, delivers the final take.
7
+ This mirrors mlmodel's `/peerkickoff`: claude has `LABS/claude-kiss/`, codex has `LABS/codex-kiss/`, they build in parallel, cross-audit, iterate. Same patternalpha's lab is just the project root instead of a subdir.
10
8
 
11
- Separate concept from `/paircode` — no focus dirs, no stages, no consensus.md. Real code evolution + peer critiques on disk, synthesis in the chat.
9
+ Separate concept from `/paircode` — no focus dirs, no stages, no consensus.md. Real code in real labs + peer critiques on disk + your final synthesis in chat.
12
10
 
13
- `$ARGUMENTS` is the user's raw prompt. Pass it through verbatim.
11
+ `$ARGUMENTS` is the user's raw prompt.
14
12
 
15
13
  ## Step 1 — Bootstrap (silent via Bash)
16
14
 
17
15
  ```bash
18
- paircode peerlab ensure # scaffolds + seeds + git-inits each lab (idempotent)
19
- PEERS=$(paircode roster --alpha claude)
16
+ paircode peerlab ensure # idempotent: scaffold/seed/git-init each external peer lab
17
+ PEERS=$(paircode peerlab roster --alpha claude) # external peers (excludes alpha/claude)
20
18
  ```
21
19
 
22
- `paircode peerlab ensure` is safe to run every time. First time: copies the project root into each peer's lab (minus standard excludes) and `git init`s it. Subsequent times: no-op.
20
+ Note: `/peerlab` is fully independent from `/paircode` it maintains its own roster at `.peerlab/peers.yaml` and never touches `.paircode/`. You can use `/peerlab` on a project that has never seen `/paircode`.
23
21
 
24
- ## Step 2 — Work round (fire every peer in parallel)
22
+ ## Step 2 — Work round (alpha + peers in parallel)
25
23
 
26
- For each peer-id in `$PEERS`, spawn one subagent via the Agent tool (`subagent_type=general-purpose`, `run_in_background=true`) — all in a single message so they run concurrently.
24
+ **Two streams happen concurrently.** Do NOT serialize them.
27
25
 
28
- Each subagent's prompt:
26
+ **Stream A — you, alpha:** implement `$ARGUMENTS` directly in the project root using your own tools (Read, Write, Edit, Bash, run tests). The project root IS alpha's lab. Leave your changes uncommitted in the working tree — the user reviews alpha's diff and commits manually (or discards). You are a peer building alongside others, not a distant reader.
27
+
28
+ **Stream B — external peers:** for each peer-id in `$PEERS`, spawn one subagent via Agent tool (`subagent_type=general-purpose`, `run_in_background=true`) — ALL in a single message so subagents run concurrently with each other AND with Stream A. Each subagent's prompt:
29
29
 
30
30
  ```
31
- Monitor peer {peer-id} on the work round. Run this shell command and wait:
31
+ Monitor peer {peer-id} on the work round. Run:
32
32
 
33
33
  paircode peerlab invoke {peer-id} "<the user's prompt verbatim>"
34
34
 
@@ -36,78 +36,84 @@ That fires the peer with cwd = .peerlab/{peer-id}/ (its own lab). The peer
36
36
  commits its work in its own .git before finishing.
37
37
 
38
38
  After it returns, gather:
39
- 1. The peer's stdout (narrative).
39
+ 1. Peer's stdout narrative.
40
40
  2. git -C .peerlab/{peer-id} log --oneline -5
41
41
  3. git -C .peerlab/{peer-id} diff HEAD~1 HEAD --stat
42
42
 
43
43
  Report back under 400 words: peer-id, ok=yes/no, duration, 1-line summary,
44
- and the git stat.
44
+ git stat output.
45
45
  ```
46
46
 
47
- Wait for every peer to finish the work round before starting Step 3.
47
+ Once every Stream B subagent has reported AND you've finished Stream A, proceed to Step 3.
48
+
49
+ ## Step 3 — Cross-review round (everyone reads everyone)
50
+
51
+ **This is the adversarial heart of /peerlab.** Every participant — alpha AND every external peer — reads every OTHER participant's code and writes a critique. Do not skip.
48
52
 
49
- ## Step 3 — Cross-review round (peers read each other's labs)
53
+ If fewer than 2 participants made real progress in Step 2 (e.g., only alpha; every peer failed), you can skip Step 3 — note it in the synthesis. Otherwise:
50
54
 
51
- This is the adversarial heart of `/peerlab`. Every peer now reads every OTHER peer's lab and writes an honest critique. Do not skip this — it's what makes `/peerlab` different from "parallel output".
55
+ **Stream A you, alpha, critiquing every peer lab:** for each peer-id in `$PEERS`:
56
+ - `git -C .peerlab/<peer-id> log -p HEAD~1..HEAD` — read the full diff
57
+ - `Read` individual files inside the lab if the diff doesn't give enough context
52
58
 
53
- If only ONE peer finished Step 2 successfully, skip Step 3 (nobody to cross-review) and note it in the final synthesis. Otherwise:
59
+ Write your consolidated critique of the external peer labs to `.peerlab/alpha-critique.md` (at the `.peerlab/` root, NOT inside any peer's lab). Severity-ranked, file:line citations pointing at each peer's files. One section per external peer. Don't pile on, don't rubber-stamp.
54
60
 
55
- For each peer-id in the set of peers that succeeded in Step 2, spawn one subagent (Agent tool, `run_in_background=true`) — all in one message. Each subagent's prompt:
61
+ **Stream B — each external peer, critiquing alpha + siblings:** for each peer-id in `$PEERS`, spawn a subagent (Agent tool, `run_in_background=true`) — ALL in one message, parallel to Stream A. Each subagent's prompt:
56
62
 
57
63
  ```
58
- Monitor peer {peer-id} on the cross-review round. Run this shell command
59
- and wait:
64
+ Monitor peer {peer-id} on the cross-review round. Run:
60
65
 
61
66
  paircode peerlab invoke {peer-id} "You already committed your own work
62
- in your lab your cwd. Now cross-review every OTHER peer's lab.
63
- Siblings live at ../<other-peer-id>/ relative to your cwd. List them
64
- with `ls ..` and exclude your own id ({peer-id}).
67
+ in your lab (your cwd). Now cross-review every OTHER participant:
65
68
 
66
- For each other peer:
67
- git -C ../<other-peer-id> log --oneline
68
- git -C ../<other-peer-id> diff HEAD~1 HEAD
69
- # read the changed files directly if the diff isn't enough
69
+ (1) Alpha's work is at the project root. From your cwd that's ../../ .
70
+ Read what alpha did — either:
71
+ git -C ../../ diff HEAD (if alpha's changes are uncommitted — usually the case)
72
+ git -C ../../ log --oneline -5 + diff HEAD~1 HEAD (if alpha committed)
73
+ Then Read the specific files alpha touched.
70
74
 
71
- Write CRITIQUES.md in YOUR cwd (your own lab) with one section per
72
- peer you reviewed. Severity-ranked findings with file:line citations
73
- pointing at the OTHER peer's files. Be specific. Don't pile on,
74
- don't rubber-stamp. Cite real lines, not vibes.
75
+ (2) Sibling peer labs are at ../<other-peer-id>/ . For each:
76
+ git -C ../<other-peer-id> log -p HEAD~1..HEAD
77
+
78
+ Write CRITIQUES.md in your own cwd (your own lab) with one section per
79
+ participant reviewed. Include alpha. Severity-ranked. file:line citations
80
+ pointing at the OTHER participant's files. Don't pile on, don't rubber-stamp.
75
81
 
76
- Then commit your critique:
77
82
  git add CRITIQUES.md
78
83
  git commit -m 'cross-review'"
79
84
 
80
- After it returns, verify:
81
- - .peerlab/{peer-id}/CRITIQUES.md exists and has non-trivial content
82
- - git -C .peerlab/{peer-id} log -1 --oneline mentions 'cross-review'
83
-
84
- Report back under 300 words: peer-id, ok=yes/no, duration, and a 1-line
85
- summary of whose code this peer found most/least convincing.
85
+ After it returns, verify .peerlab/{peer-id}/CRITIQUES.md exists with a
86
+ non-trivial section for alpha plus each sibling. Report back under 300
87
+ words: peer-id, ok=yes/no, duration, 1-line summary of whose code this
88
+ peer found most/least convincing.
86
89
  ```
87
90
 
88
- Wait for every peer to finish cross-review before Step 4.
91
+ Wait for Stream A AND every Stream B subagent before Step 4.
89
92
 
90
93
  ## Step 4 — Synthesize (one message to user, ≤300 words)
91
94
 
92
- For each peer, read BOTH:
93
- - The work diff: `git -C .peerlab/<peer-id> log -p HEAD~2..HEAD` (covers work commit + critique commit 2 commits if both rounds ran, 1 if Step 3 was skipped).
94
- - The critique: `.peerlab/<peer-id>/CRITIQUES.md` — what THIS peer thought of the OTHERS.
95
+ Read the full cross-review:
96
+ - `.peerlab/alpha-critique.md` your own critique of the peer labs
97
+ - Each `.peerlab/<peer-id>/CRITIQUES.md` — each peer's critique of alpha + siblings
98
+ - Alpha's own work in the project root (you remember; you did it in Step 2)
99
+ - Each peer's work diff (already gathered in Step 2)
95
100
 
96
- Then produce a tight team-lead synthesis that leans on the peers' own voices, not just yours:
101
+ Synthesize with the peers' voices, not just yours:
97
102
 
98
- - **Per-peer implementation summary**: 1–2 lines each files touched, LOC delta, the shape of the approach.
99
- - **What peers said about each other**: extract the sharpest findings from each `CRITIQUES.md`. If codex called out a bug in gemini's lab with a file:line, quote it. If gemini pointed at an edge case codex missed, quote it. These are the adversarial moments — don't bury them.
100
- - **Cross-cutting themes**: where did peers agree about what's wrong? Where did they disagree? Disagreement is signal.
101
- - **Honest head-to-head**: which lab made the cleanest move, informed by the peer critiques? Don't play favorites, don't hedge.
102
- - **What to pull into alpha**: if a peer's approach is objectively better, name files + approach. Don't do the pull yourself — recommend.
103
+ - **Implementation summaries** alpha's approach in project root, each peer's approach in its lab. 1–2 lines each.
104
+ - **What peers said about alpha** pull the sharpest findings from each peer's CRITIQUES.md about YOUR work. Quote them, don't paraphrase away the teeth.
105
+ - **What alpha said about peers** reference your alpha-critique.md findings.
106
+ - **Cross-cutting themes** where did critiques converge? That's high-signal consensus.
107
+ - **Head-to-head** which implementation won, informed by the cross-review? Don't play favorites, don't hedge.
108
+ - **Next action** — merge a peer's pattern into alpha? Adjust alpha's implementation based on a peer's critique? Fire a peer to self-heal based on alpha's critique? Recommend; user decides.
103
109
 
104
- No focus dirs. No consensus.md. Just the synthesis as your chat reply.
110
+ No focus dirs, no consensus.md file. Just your synthesis as the chat reply.
105
111
 
106
112
  ## Guardrails
107
113
 
108
- - **Alpha (this session) does NOT edit `.peerlab/<peer-id>/`.** Peers own their labs. You only read.
109
- - **Alpha does NOT implement the user's prompt in the project root.** That's a `/paircode` job or a direct Claude Code ask. `/peerlab` is strictly "peers go build + cross-review; I read the diffs and critiques."
110
- - **Fire every peer** unless the user's prompt explicitly names `--peer`/`--peers`. Watchdog protects against hangs.
111
- - **Do not skip Step 3.** The cross-review round is the point. If only one peer made it through Step 2, note why in the synthesis and skip Step 3 explicitly.
112
- - **If a peer fails** (hung, errored, no commits), note it in the synthesis and continue. One flaky peer doesn't sink the run.
113
- - **No AI attribution** in anything you write see the maintainer's CLAUDE.md.
114
+ - **Alpha IS a peer.** You build in project root during Step 2 not sitting out.
115
+ - **Uncommitted changes in project root stay uncommitted.** User reviews alpha's diff + commits manually. Do NOT `git add` or `git commit` in the project root as part of `/peerlab`.
116
+ - **External peers commit inside their own labs** their `.git/` is isolated, `.peerlab/` is gitignored from the outer repo.
117
+ - **Fire every external peer** unless the user's prompt explicitly names `--peer`/`--peers`. Watchdog caps silent hangs at ~32s.
118
+ - **Do not skip Step 3** unless fewer than 2 participants succeeded in Step 2. Cross-review is the point.
119
+ - **No AI attribution** in any artifact you write (alpha-critique.md, chat synthesis). See the maintainer's CLAUDE.md.
@@ -10,7 +10,11 @@
10
10
  # Fields:
11
11
  # id — peer-a, peer-b, peer-c, ... (unique, immutable)
12
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)
13
+ # model — specific model string. Optional but RECOMMENDED. If omitted,
14
+ # the CLI picks its own default — which on a free/misconfigured
15
+ # login can silently be a weak flash-tier model that just agrees
16
+ # with you. Pin a Pro tier for real fork weight (e.g. gemini: a
17
+ # *-pro* model; claude: sonnet/opus). Empty = CLI default.
14
18
  # priority — high | medium | low (used when budget is tight)
15
19
  # daily_budget — advisory only for now; paircode logs when a peer sits out
16
20
  #
@@ -0,0 +1,119 @@
1
+ ---
2
+ description: peerlab — alpha and peers each build in their own lab, then cross-review each other's code. Alpha is one of the peers.
3
+ ---
4
+
5
+ You are the orchestrator of a `/peerlab` run. In `/peerlab`, **alpha (this Claude session) is one of the peers** — alpha builds in the project root while external peers build in their own labs at `.peerlab/<peer-id>/`. Then every participant reads every other participant's code and writes a critique. Finally you (wearing the orchestrator hat) synthesize the full cross-review into a single chat message.
6
+
7
+ This mirrors mlmodel's `/peerkickoff`: claude has `LABS/claude-kiss/`, codex has `LABS/codex-kiss/`, they build in parallel, cross-audit, iterate. Same pattern — alpha's lab is just the project root instead of a subdir.
8
+
9
+ Separate concept from `/paircode` — no focus dirs, no stages, no consensus.md. Real code in real labs + peer critiques on disk + your final synthesis in chat.
10
+
11
+ `$ARGUMENTS` is the user's raw prompt.
12
+
13
+ ## Step 1 — Bootstrap (silent via Bash)
14
+
15
+ ```bash
16
+ paircode peerlab ensure # idempotent: scaffold/seed/git-init each external peer lab
17
+ PEERS=$(paircode peerlab roster --alpha claude) # external peers (excludes alpha/claude)
18
+ ```
19
+
20
+ Note: `/peerlab` is fully independent from `/paircode` — it maintains its own roster at `.peerlab/peers.yaml` and never touches `.paircode/`. You can use `/peerlab` on a project that has never seen `/paircode`.
21
+
22
+ ## Step 2 — Work round (alpha + peers in parallel)
23
+
24
+ **Two streams happen concurrently.** Do NOT serialize them.
25
+
26
+ **Stream A — you, alpha:** implement `$ARGUMENTS` directly in the project root using your own tools (Read, Write, Edit, Bash, run tests). The project root IS alpha's lab. Leave your changes uncommitted in the working tree — the user reviews alpha's diff and commits manually (or discards). You are a peer building alongside others, not a distant reader.
27
+
28
+ **Stream B — external peers:** for each peer-id in `$PEERS`, spawn one subagent via Agent tool (`subagent_type=general-purpose`, `run_in_background=true`) — ALL in a single message so subagents run concurrently with each other AND with Stream A. Each subagent's prompt:
29
+
30
+ ```
31
+ Monitor peer {peer-id} on the work round. Run:
32
+
33
+ paircode peerlab invoke {peer-id} "<the user's prompt verbatim>"
34
+
35
+ That fires the peer with cwd = .peerlab/{peer-id}/ (its own lab). The peer
36
+ commits its work in its own .git before finishing.
37
+
38
+ After it returns, gather:
39
+ 1. Peer's stdout narrative.
40
+ 2. git -C .peerlab/{peer-id} log --oneline -5
41
+ 3. git -C .peerlab/{peer-id} diff HEAD~1 HEAD --stat
42
+
43
+ Report back under 400 words: peer-id, ok=yes/no, duration, 1-line summary,
44
+ git stat output.
45
+ ```
46
+
47
+ Once every Stream B subagent has reported AND you've finished Stream A, proceed to Step 3.
48
+
49
+ ## Step 3 — Cross-review round (everyone reads everyone)
50
+
51
+ **This is the adversarial heart of /peerlab.** Every participant — alpha AND every external peer — reads every OTHER participant's code and writes a critique. Do not skip.
52
+
53
+ If fewer than 2 participants made real progress in Step 2 (e.g., only alpha; every peer failed), you can skip Step 3 — note it in the synthesis. Otherwise:
54
+
55
+ **Stream A — you, alpha, critiquing every peer lab:** for each peer-id in `$PEERS`:
56
+ - `git -C .peerlab/<peer-id> log -p HEAD~1..HEAD` — read the full diff
57
+ - `Read` individual files inside the lab if the diff doesn't give enough context
58
+
59
+ Write your consolidated critique of the external peer labs to `.peerlab/alpha-critique.md` (at the `.peerlab/` root, NOT inside any peer's lab). Severity-ranked, file:line citations pointing at each peer's files. One section per external peer. Don't pile on, don't rubber-stamp.
60
+
61
+ **Stream B — each external peer, critiquing alpha + siblings:** for each peer-id in `$PEERS`, spawn a subagent (Agent tool, `run_in_background=true`) — ALL in one message, parallel to Stream A. Each subagent's prompt:
62
+
63
+ ```
64
+ Monitor peer {peer-id} on the cross-review round. Run:
65
+
66
+ paircode peerlab invoke {peer-id} "You already committed your own work
67
+ in your lab (your cwd). Now cross-review every OTHER participant:
68
+
69
+ (1) Alpha's work is at the project root. From your cwd that's ../../ .
70
+ Read what alpha did — either:
71
+ git -C ../../ diff HEAD (if alpha's changes are uncommitted — usually the case)
72
+ git -C ../../ log --oneline -5 + diff HEAD~1 HEAD (if alpha committed)
73
+ Then Read the specific files alpha touched.
74
+
75
+ (2) Sibling peer labs are at ../<other-peer-id>/ . For each:
76
+ git -C ../<other-peer-id> log -p HEAD~1..HEAD
77
+
78
+ Write CRITIQUES.md in your own cwd (your own lab) with one section per
79
+ participant reviewed. Include alpha. Severity-ranked. file:line citations
80
+ pointing at the OTHER participant's files. Don't pile on, don't rubber-stamp.
81
+
82
+ git add CRITIQUES.md
83
+ git commit -m 'cross-review'"
84
+
85
+ After it returns, verify .peerlab/{peer-id}/CRITIQUES.md exists with a
86
+ non-trivial section for alpha plus each sibling. Report back under 300
87
+ words: peer-id, ok=yes/no, duration, 1-line summary of whose code this
88
+ peer found most/least convincing.
89
+ ```
90
+
91
+ Wait for Stream A AND every Stream B subagent before Step 4.
92
+
93
+ ## Step 4 — Synthesize (one message to user, ≤300 words)
94
+
95
+ Read the full cross-review:
96
+ - `.peerlab/alpha-critique.md` — your own critique of the peer labs
97
+ - Each `.peerlab/<peer-id>/CRITIQUES.md` — each peer's critique of alpha + siblings
98
+ - Alpha's own work in the project root (you remember; you did it in Step 2)
99
+ - Each peer's work diff (already gathered in Step 2)
100
+
101
+ Synthesize with the peers' voices, not just yours:
102
+
103
+ - **Implementation summaries** — alpha's approach in project root, each peer's approach in its lab. 1–2 lines each.
104
+ - **What peers said about alpha** — pull the sharpest findings from each peer's CRITIQUES.md about YOUR work. Quote them, don't paraphrase away the teeth.
105
+ - **What alpha said about peers** — reference your alpha-critique.md findings.
106
+ - **Cross-cutting themes** — where did critiques converge? That's high-signal consensus.
107
+ - **Head-to-head** — which implementation won, informed by the cross-review? Don't play favorites, don't hedge.
108
+ - **Next action** — merge a peer's pattern into alpha? Adjust alpha's implementation based on a peer's critique? Fire a peer to self-heal based on alpha's critique? Recommend; user decides.
109
+
110
+ No focus dirs, no consensus.md file. Just your synthesis as the chat reply.
111
+
112
+ ## Guardrails
113
+
114
+ - **Alpha IS a peer.** You build in project root during Step 2 — not sitting out.
115
+ - **Uncommitted changes in project root stay uncommitted.** User reviews alpha's diff + commits manually. Do NOT `git add` or `git commit` in the project root as part of `/peerlab`.
116
+ - **External peers commit inside their own labs** — their `.git/` is isolated, `.peerlab/` is gitignored from the outer repo.
117
+ - **Fire every external peer** unless the user's prompt explicitly names `--peer`/`--peers`. Watchdog caps silent hangs at ~32s.
118
+ - **Do not skip Step 3** unless fewer than 2 participants succeeded in Step 2. Cross-review is the point.
119
+ - **No AI attribution** in any artifact you write (alpha-critique.md, chat synthesis). See the maintainer's CLAUDE.md.
@@ -10,7 +10,11 @@
10
10
  # Fields:
11
11
  # id — peer-a, peer-b, peer-c, ... (unique, immutable)
12
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)
13
+ # model — specific model string. Optional but RECOMMENDED. If omitted,
14
+ # the CLI picks its own default — which on a free/misconfigured
15
+ # login can silently be a weak flash-tier model that just agrees
16
+ # with you. Pin a Pro tier for real fork weight (e.g. gemini: a
17
+ # *-pro* model; claude: sonnet/opus). Empty = CLI default.
14
18
  # priority — high | medium | low (used when budget is tight)
15
19
  # daily_budget — advisory only for now; paircode logs when a peer sits out
16
20
  #
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paircode
3
- Version: 0.12.4
3
+ Version: 0.13.2
4
4
  Summary: Adversarial journey framework — orchestrate multiple LLM peers through research, plan, and execute stages with file-trace peer review.
5
5
  Project-URL: Homepage, https://github.com/starshipagentic/paircode
6
6
  Project-URL: Repository, https://github.com/starshipagentic/paircode
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Software Development
22
22
  Requires-Python: >=3.10
23
23
  Requires-Dist: click>=8.1
24
- Requires-Dist: cliworker>=0.8.3
24
+ Requires-Dist: cliworker>=0.8.5
25
25
  Requires-Dist: pyyaml>=6.0
26
26
  Requires-Dist: rich>=13.0
27
27
  Description-Content-Type: text/markdown
@@ -0,0 +1,30 @@
1
+ paircode/__init__.py,sha256=K8qZD2jJXH_pWsZiBgeyuBX65A7ejdGldxRzPtI38vY,167
2
+ paircode/__main__.py,sha256=nj3xFbNqz8UN8UQJSJ1mDyPTD2PZ-7PZ1TF-eTyBbgA,69
3
+ paircode/cli.py,sha256=12pRd0gGP8gZ4iqD702f8VNZRtoQCWQABDZNqLYWL0M,21573
4
+ paircode/converge.py,sha256=zt5hHQUffiD-12enuASHGnp3Xc4LfufbF13rpWLI4sE,1966
5
+ paircode/detect.py,sha256=41XxPw7sTRdxieLPc9L2zsxFteIPBr59ObYj13kUVl0,1426
6
+ paircode/handshake.py,sha256=hlvwPLdxlaqNekQXBPlKU78SPwckHGSXI71LwQW44ck,2098
7
+ paircode/installer.py,sha256=0RVHBg1qgm4v7rdp4a4k8_rx04F_DEN_9uFfHIsuf4o,13155
8
+ paircode/peerlab.py,sha256=YELTyU4HdPqo5zhvH9Nol0xrVmdXc0btnTtxxgeMWaU,10167
9
+ paircode/runner.py,sha256=9-Kn3wdd87PjnfOlKbHDSapucJe2kCb1Kho99DEX73I,4699
10
+ paircode/state.py,sha256=cEzBPZ-5QZMst113kSLH_YfcE0GF6ZEVvYJnfFnb1us,7416
11
+ paircode/util.py,sha256=eqvd0RCaOZgK0g6a7G03y-sJ_w4xYjCZ9k4oH2gmytw,477
12
+ paircode/templates/FOCUS.md,sha256=BL3I6bjwezqzBSksRb2aYXY_L8Ff4vqZfhvGAPb9r9E,689
13
+ paircode/templates/JOURNEY.md,sha256=GAM2RJ42T5JElFYPiv0nIUCG0vr5q7csXoRsrm4VMwY,464
14
+ paircode/templates/peers.yaml,sha256=iTlKrfy9Czsfslbv0YsihZmNedwCEihtw9vMvpIXtKo,1177
15
+ paircode/templates/claude/commands/paircode.md,sha256=K5FBXhDbJFt0IunBPvHSfbeYp2yDrZRjR1phpWJQ8Fg,11432
16
+ paircode/templates/claude/commands/peerlab.md,sha256=7svj_kBag_S_CRMYSRxc1CglK7MI7n75H-H0NAQKpbM,7209
17
+ paircode/templates/codex/commands/paircode.md,sha256=QEnq-JG_CuAU5Y4KSVXZvUHbvrIy0bTCXxZRV01ZNWE,8396
18
+ paircode/templates/gemini/commands/paircode.toml,sha256=ruvY6xV5BpAKxdNK7dyEnSMreNRykDfmgKk9DAK1YxA,7551
19
+ paircode-0.13.2.data/data/paircode/templates/FOCUS.md,sha256=BL3I6bjwezqzBSksRb2aYXY_L8Ff4vqZfhvGAPb9r9E,689
20
+ paircode-0.13.2.data/data/paircode/templates/JOURNEY.md,sha256=GAM2RJ42T5JElFYPiv0nIUCG0vr5q7csXoRsrm4VMwY,464
21
+ paircode-0.13.2.data/data/paircode/templates/peers.yaml,sha256=iTlKrfy9Czsfslbv0YsihZmNedwCEihtw9vMvpIXtKo,1177
22
+ paircode-0.13.2.data/data/paircode/templates/claude/commands/paircode.md,sha256=K5FBXhDbJFt0IunBPvHSfbeYp2yDrZRjR1phpWJQ8Fg,11432
23
+ paircode-0.13.2.data/data/paircode/templates/claude/commands/peerlab.md,sha256=7svj_kBag_S_CRMYSRxc1CglK7MI7n75H-H0NAQKpbM,7209
24
+ paircode-0.13.2.data/data/paircode/templates/codex/commands/paircode.md,sha256=QEnq-JG_CuAU5Y4KSVXZvUHbvrIy0bTCXxZRV01ZNWE,8396
25
+ paircode-0.13.2.data/data/paircode/templates/gemini/commands/paircode.toml,sha256=ruvY6xV5BpAKxdNK7dyEnSMreNRykDfmgKk9DAK1YxA,7551
26
+ paircode-0.13.2.dist-info/METADATA,sha256=P3Ugf67lqVZsW03EFRWURGtMnw31f2SAnXr9ksrcJp8,7724
27
+ paircode-0.13.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
28
+ paircode-0.13.2.dist-info/entry_points.txt,sha256=Xws2Px0qjasysoJikWE8LokoojG2_7qwOh8vTF-v9K0,47
29
+ paircode-0.13.2.dist-info/licenses/LICENSE,sha256=2z2DTvqat4kmyHNEC8ehgOC5fBtyENHg2-Z0MiY_rxM,1074
30
+ paircode-0.13.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,113 +0,0 @@
1
- ---
2
- description: peerlab — each peer owns a parallel lab with its own git. Peers write real code, then cross-review each other's labs, then team lead synthesizes.
3
- ---
4
-
5
- You are the team lead for a `/peerlab` run. Each peer has its own persistent lab at `.peerlab/<peer-id>/` with its own `.git/`. Peers **own** their labs. The flow is:
6
-
7
- 1. **Work round** — every peer builds in its own lab.
8
- 2. **Cross-review round** — every peer reads the OTHER peers' labs and writes an honest critique (`CRITIQUES.md`) in its own lab. Peer-reviews-peer is the adversarial content; do NOT skip this.
9
- 3. **Team-lead synthesis** — alpha reads both the code diffs AND each peer's critiques, delivers the final take.
10
-
11
- Separate concept from `/paircode` — no focus dirs, no stages, no consensus.md. Real code evolution + peer critiques on disk, synthesis in the chat.
12
-
13
- `$ARGUMENTS` is the user's raw prompt. Pass it through verbatim.
14
-
15
- ## Step 1 — Bootstrap (silent via Bash)
16
-
17
- ```bash
18
- paircode peerlab ensure # scaffolds + seeds + git-inits each lab (idempotent)
19
- PEERS=$(paircode roster --alpha claude)
20
- ```
21
-
22
- `paircode peerlab ensure` is safe to run every time. First time: copies the project root into each peer's lab (minus standard excludes) and `git init`s it. Subsequent times: no-op.
23
-
24
- ## Step 2 — Work round (fire every peer in parallel)
25
-
26
- For each peer-id in `$PEERS`, spawn one subagent via the Agent tool (`subagent_type=general-purpose`, `run_in_background=true`) — all in a single message so they run concurrently.
27
-
28
- Each subagent's prompt:
29
-
30
- ```
31
- Monitor peer {peer-id} on the work round. Run this shell command and wait:
32
-
33
- paircode peerlab invoke {peer-id} "<the user's prompt verbatim>"
34
-
35
- That fires the peer with cwd = .peerlab/{peer-id}/ (its own lab). The peer
36
- commits its work in its own .git before finishing.
37
-
38
- After it returns, gather:
39
- 1. The peer's stdout (narrative).
40
- 2. git -C .peerlab/{peer-id} log --oneline -5
41
- 3. git -C .peerlab/{peer-id} diff HEAD~1 HEAD --stat
42
-
43
- Report back under 400 words: peer-id, ok=yes/no, duration, 1-line summary,
44
- and the git stat.
45
- ```
46
-
47
- Wait for every peer to finish the work round before starting Step 3.
48
-
49
- ## Step 3 — Cross-review round (peers read each other's labs)
50
-
51
- This is the adversarial heart of `/peerlab`. Every peer now reads every OTHER peer's lab and writes an honest critique. Do not skip this — it's what makes `/peerlab` different from "parallel output".
52
-
53
- If only ONE peer finished Step 2 successfully, skip Step 3 (nobody to cross-review) and note it in the final synthesis. Otherwise:
54
-
55
- For each peer-id in the set of peers that succeeded in Step 2, spawn one subagent (Agent tool, `run_in_background=true`) — all in one message. Each subagent's prompt:
56
-
57
- ```
58
- Monitor peer {peer-id} on the cross-review round. Run this shell command
59
- and wait:
60
-
61
- paircode peerlab invoke {peer-id} "You already committed your own work
62
- in your lab — your cwd. Now cross-review every OTHER peer's lab.
63
- Siblings live at ../<other-peer-id>/ relative to your cwd. List them
64
- with `ls ..` and exclude your own id ({peer-id}).
65
-
66
- For each other peer:
67
- git -C ../<other-peer-id> log --oneline
68
- git -C ../<other-peer-id> diff HEAD~1 HEAD
69
- # read the changed files directly if the diff isn't enough
70
-
71
- Write CRITIQUES.md in YOUR cwd (your own lab) with one section per
72
- peer you reviewed. Severity-ranked findings with file:line citations
73
- pointing at the OTHER peer's files. Be specific. Don't pile on,
74
- don't rubber-stamp. Cite real lines, not vibes.
75
-
76
- Then commit your critique:
77
- git add CRITIQUES.md
78
- git commit -m 'cross-review'"
79
-
80
- After it returns, verify:
81
- - .peerlab/{peer-id}/CRITIQUES.md exists and has non-trivial content
82
- - git -C .peerlab/{peer-id} log -1 --oneline mentions 'cross-review'
83
-
84
- Report back under 300 words: peer-id, ok=yes/no, duration, and a 1-line
85
- summary of whose code this peer found most/least convincing.
86
- ```
87
-
88
- Wait for every peer to finish cross-review before Step 4.
89
-
90
- ## Step 4 — Synthesize (one message to user, ≤300 words)
91
-
92
- For each peer, read BOTH:
93
- - The work diff: `git -C .peerlab/<peer-id> log -p HEAD~2..HEAD` (covers work commit + critique commit — 2 commits if both rounds ran, 1 if Step 3 was skipped).
94
- - The critique: `.peerlab/<peer-id>/CRITIQUES.md` — what THIS peer thought of the OTHERS.
95
-
96
- Then produce a tight team-lead synthesis that leans on the peers' own voices, not just yours:
97
-
98
- - **Per-peer implementation summary**: 1–2 lines each — files touched, LOC delta, the shape of the approach.
99
- - **What peers said about each other**: extract the sharpest findings from each `CRITIQUES.md`. If codex called out a bug in gemini's lab with a file:line, quote it. If gemini pointed at an edge case codex missed, quote it. These are the adversarial moments — don't bury them.
100
- - **Cross-cutting themes**: where did peers agree about what's wrong? Where did they disagree? Disagreement is signal.
101
- - **Honest head-to-head**: which lab made the cleanest move, informed by the peer critiques? Don't play favorites, don't hedge.
102
- - **What to pull into alpha**: if a peer's approach is objectively better, name files + approach. Don't do the pull yourself — recommend.
103
-
104
- No focus dirs. No consensus.md. Just the synthesis as your chat reply.
105
-
106
- ## Guardrails
107
-
108
- - **Alpha (this session) does NOT edit `.peerlab/<peer-id>/`.** Peers own their labs. You only read.
109
- - **Alpha does NOT implement the user's prompt in the project root.** That's a `/paircode` job or a direct Claude Code ask. `/peerlab` is strictly "peers go build + cross-review; I read the diffs and critiques."
110
- - **Fire every peer** unless the user's prompt explicitly names `--peer`/`--peers`. Watchdog protects against hangs.
111
- - **Do not skip Step 3.** The cross-review round is the point. If only one peer made it through Step 2, note why in the synthesis and skip Step 3 explicitly.
112
- - **If a peer fails** (hung, errored, no commits), note it in the synthesis and continue. One flaky peer doesn't sink the run.
113
- - **No AI attribution** in anything you write — see the maintainer's CLAUDE.md.
@@ -1,30 +0,0 @@
1
- paircode/__init__.py,sha256=KaQh-Ad75QE_KNJInKMirtsdbq84QM_xQfpaTbIkS4E,167
2
- paircode/__main__.py,sha256=nj3xFbNqz8UN8UQJSJ1mDyPTD2PZ-7PZ1TF-eTyBbgA,69
3
- paircode/cli.py,sha256=XgMRVH56cxK8J36vut9pmNeofhql6UxBo6zDwdxUuTU,19065
4
- paircode/converge.py,sha256=zt5hHQUffiD-12enuASHGnp3Xc4LfufbF13rpWLI4sE,1966
5
- paircode/detect.py,sha256=41XxPw7sTRdxieLPc9L2zsxFteIPBr59ObYj13kUVl0,1426
6
- paircode/handshake.py,sha256=QDYZOiBVeZsmPogr9RX_ky9iu7wbAimU34bT7JWlboI,1713
7
- paircode/installer.py,sha256=0RVHBg1qgm4v7rdp4a4k8_rx04F_DEN_9uFfHIsuf4o,13155
8
- paircode/peerlab.py,sha256=7rYIge71AalpHSvhjPHatj7S33dtgKYCq4WWLNBT5UE,9082
9
- paircode/runner.py,sha256=9-Kn3wdd87PjnfOlKbHDSapucJe2kCb1Kho99DEX73I,4699
10
- paircode/state.py,sha256=LG2YQ6a92HEXf70F655jUk7-vtJZfs7kTBS01TI3GwI,6563
11
- paircode/util.py,sha256=eqvd0RCaOZgK0g6a7G03y-sJ_w4xYjCZ9k4oH2gmytw,477
12
- paircode/templates/FOCUS.md,sha256=BL3I6bjwezqzBSksRb2aYXY_L8Ff4vqZfhvGAPb9r9E,689
13
- paircode/templates/JOURNEY.md,sha256=GAM2RJ42T5JElFYPiv0nIUCG0vr5q7csXoRsrm4VMwY,464
14
- paircode/templates/peers.yaml,sha256=4c6NYi0MDvUXvWxr0slF82wDl4YmjfNndtD5kkNVaCE,839
15
- paircode/templates/claude/commands/paircode.md,sha256=K5FBXhDbJFt0IunBPvHSfbeYp2yDrZRjR1phpWJQ8Fg,11432
16
- paircode/templates/claude/commands/peerlab.md,sha256=0-vI4S9HChJylLC_wbZ5bScAqWjmHMsSfYsIq_CNl_8,6079
17
- paircode/templates/codex/commands/paircode.md,sha256=QEnq-JG_CuAU5Y4KSVXZvUHbvrIy0bTCXxZRV01ZNWE,8396
18
- paircode/templates/gemini/commands/paircode.toml,sha256=ruvY6xV5BpAKxdNK7dyEnSMreNRykDfmgKk9DAK1YxA,7551
19
- paircode-0.12.4.data/data/paircode/templates/FOCUS.md,sha256=BL3I6bjwezqzBSksRb2aYXY_L8Ff4vqZfhvGAPb9r9E,689
20
- paircode-0.12.4.data/data/paircode/templates/JOURNEY.md,sha256=GAM2RJ42T5JElFYPiv0nIUCG0vr5q7csXoRsrm4VMwY,464
21
- paircode-0.12.4.data/data/paircode/templates/peers.yaml,sha256=4c6NYi0MDvUXvWxr0slF82wDl4YmjfNndtD5kkNVaCE,839
22
- paircode-0.12.4.data/data/paircode/templates/claude/commands/paircode.md,sha256=K5FBXhDbJFt0IunBPvHSfbeYp2yDrZRjR1phpWJQ8Fg,11432
23
- paircode-0.12.4.data/data/paircode/templates/claude/commands/peerlab.md,sha256=0-vI4S9HChJylLC_wbZ5bScAqWjmHMsSfYsIq_CNl_8,6079
24
- paircode-0.12.4.data/data/paircode/templates/codex/commands/paircode.md,sha256=QEnq-JG_CuAU5Y4KSVXZvUHbvrIy0bTCXxZRV01ZNWE,8396
25
- paircode-0.12.4.data/data/paircode/templates/gemini/commands/paircode.toml,sha256=ruvY6xV5BpAKxdNK7dyEnSMreNRykDfmgKk9DAK1YxA,7551
26
- paircode-0.12.4.dist-info/METADATA,sha256=Z44dZ6F3wlod75WRlH-G6FxPLIwN2Ap-fw5tFXWb4uE,7724
27
- paircode-0.12.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
28
- paircode-0.12.4.dist-info/entry_points.txt,sha256=Xws2Px0qjasysoJikWE8LokoojG2_7qwOh8vTF-v9K0,47
29
- paircode-0.12.4.dist-info/licenses/LICENSE,sha256=2z2DTvqat4kmyHNEC8ehgOC5fBtyENHg2-Z0MiY_rxM,1074
30
- paircode-0.12.4.dist-info/RECORD,,