paircode 0.12.1__tar.gz → 0.12.4__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.
- {paircode-0.12.1 → paircode-0.12.4}/PKG-INFO +1 -1
- {paircode-0.12.1 → paircode-0.12.4}/pyproject.toml +1 -1
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/__init__.py +1 -1
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/peerlab.py +61 -7
- paircode-0.12.4/src/paircode/templates/claude/commands/peerlab.md +113 -0
- {paircode-0.12.1 → paircode-0.12.4}/tests/test_peerlab.py +77 -0
- paircode-0.12.1/src/paircode/templates/claude/commands/peerlab.md +0 -71
- {paircode-0.12.1 → paircode-0.12.4}/.gitignore +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/LICENSE +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/README.md +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/diary/001-step-a-architecture.md +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/diary/002-v0.10-release-pipeline.md +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/diary/003-arch-b-pivot-grappling.md +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/__main__.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/cli.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/converge.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/detect.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/handshake.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/installer.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/runner.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/state.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/templates/FOCUS.md +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/templates/JOURNEY.md +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/templates/claude/commands/paircode.md +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/templates/codex/commands/paircode.md +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/templates/gemini/commands/paircode.toml +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/templates/peers.yaml +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/src/paircode/util.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/tests/__init__.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/tests/test_cli_smoke.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/tests/test_converge.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/tests/test_smoke.py +0 -0
- {paircode-0.12.1 → paircode-0.12.4}/tests/test_state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paircode
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.4
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "paircode"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.4"
|
|
8
8
|
description = "Adversarial journey framework — orchestrate multiple LLM peers through research, plan, and execute stages with file-trace peer review."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -41,6 +41,30 @@ SEED_EXCLUDES: tuple[str, ...] = (
|
|
|
41
41
|
|
|
42
42
|
GITIGNORE_BLOCK_HEADER = "# peerlab — per-peer independent parallel labs"
|
|
43
43
|
|
|
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
|
+
LAB_GITIGNORE_BLOCK = """\
|
|
49
|
+
# peerlab defaults — python + editor junk (keep peer commits clean)
|
|
50
|
+
__pycache__/
|
|
51
|
+
*.py[cod]
|
|
52
|
+
*$py.class
|
|
53
|
+
*.egg-info/
|
|
54
|
+
.pytest_cache/
|
|
55
|
+
.mypy_cache/
|
|
56
|
+
.ruff_cache/
|
|
57
|
+
.tox/
|
|
58
|
+
.coverage
|
|
59
|
+
.coverage.*
|
|
60
|
+
htmlcov/
|
|
61
|
+
.DS_Store
|
|
62
|
+
.idea/
|
|
63
|
+
.vscode/
|
|
64
|
+
*.swp
|
|
65
|
+
"""
|
|
66
|
+
LAB_GITIGNORE_MARKER = "peerlab defaults"
|
|
67
|
+
|
|
44
68
|
|
|
45
69
|
@dataclass(frozen=True)
|
|
46
70
|
class EnsureResult:
|
|
@@ -108,6 +132,25 @@ def _rsync_project(src: Path, dst: Path, excludes: Iterable[str] = SEED_EXCLUDES
|
|
|
108
132
|
shutil.copytree(src, dst, ignore=_ignore)
|
|
109
133
|
|
|
110
134
|
|
|
135
|
+
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
|
+
"""
|
|
145
|
+
gitignore = lab / ".gitignore"
|
|
146
|
+
existing = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
|
|
147
|
+
if LAB_GITIGNORE_MARKER in existing:
|
|
148
|
+
return False
|
|
149
|
+
new_text = (existing.rstrip() + "\n\n" if existing.strip() else "") + LAB_GITIGNORE_BLOCK
|
|
150
|
+
gitignore.write_text(new_text, encoding="utf-8")
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
|
|
111
154
|
def _git_init(lab: Path) -> None:
|
|
112
155
|
"""`git init` inside `lab/`. No-op if already inited."""
|
|
113
156
|
if (lab / ".git").exists():
|
|
@@ -121,17 +164,24 @@ def _git_init(lab: Path) -> None:
|
|
|
121
164
|
subprocess.run(["git", "init", "--quiet", str(lab)], check=True)
|
|
122
165
|
|
|
123
166
|
|
|
124
|
-
def _git_initial_commit(
|
|
167
|
+
def _git_initial_commit(
|
|
168
|
+
lab: Path, peer_id: str, message: str = "initial seed from project root"
|
|
169
|
+
) -> bool:
|
|
125
170
|
"""Stage everything + commit. Returns True if a commit was made.
|
|
126
171
|
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
129
176
|
"""
|
|
130
177
|
if not any(lab.iterdir()):
|
|
131
178
|
return False
|
|
132
|
-
# Local git config — scoped to this
|
|
133
|
-
subprocess.run(["git", "-C", str(lab), "config", "user.name",
|
|
134
|
-
subprocess.run(
|
|
179
|
+
# Local git config — scoped to this lab, doesn't touch global identity.
|
|
180
|
+
subprocess.run(["git", "-C", str(lab), "config", "user.name", peer_id], check=False)
|
|
181
|
+
subprocess.run(
|
|
182
|
+
["git", "-C", str(lab), "config", "user.email", f"{peer_id}@peerlab.local"],
|
|
183
|
+
check=False,
|
|
184
|
+
)
|
|
135
185
|
subprocess.run(["git", "-C", str(lab), "add", "-A"], check=False)
|
|
136
186
|
# Commit only if there's something staged
|
|
137
187
|
diff_check = subprocess.run(
|
|
@@ -183,8 +233,12 @@ def ensure_peer_labs(state: PaircodeState) -> list[EnsureResult]:
|
|
|
183
233
|
lab.mkdir(exist_ok=True)
|
|
184
234
|
# Seed from project root (might be empty for greenfield — rsync handles fine)
|
|
185
235
|
_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
|
+
ensure_lab_gitignore(lab)
|
|
186
240
|
_git_init(lab)
|
|
187
|
-
_git_initial_commit(lab, message="initial seed from project root")
|
|
241
|
+
_git_initial_commit(lab, pid, message="initial seed from project root")
|
|
188
242
|
results.append(EnsureResult(peer_id=pid, lab_path=lab, status="created"))
|
|
189
243
|
return results
|
|
190
244
|
|
|
@@ -0,0 +1,113 @@
|
|
|
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.
|
|
@@ -11,8 +11,10 @@ from click.testing import CliRunner
|
|
|
11
11
|
from paircode.cli import main
|
|
12
12
|
from paircode.handshake import ProposedPeer
|
|
13
13
|
from paircode.peerlab import (
|
|
14
|
+
LAB_GITIGNORE_MARKER,
|
|
14
15
|
PEERLAB_DIRNAME,
|
|
15
16
|
ensure_gitignore,
|
|
17
|
+
ensure_lab_gitignore,
|
|
16
18
|
ensure_peer_labs,
|
|
17
19
|
peer_lab_path,
|
|
18
20
|
)
|
|
@@ -176,6 +178,81 @@ def test_paircode_peerlab_help_is_reachable():
|
|
|
176
178
|
# Seed excludes honored
|
|
177
179
|
# ---------------------------------------------------------------------------
|
|
178
180
|
|
|
181
|
+
def test_lab_gitignore_has_pycache_on_fresh_lab(tmp_path, monkeypatch):
|
|
182
|
+
"""Fresh lab's .gitignore excludes __pycache__ and bytecode so peer
|
|
183
|
+
commits don't pull in build junk. Regression for v0.12.0 bug where
|
|
184
|
+
`git add -A` in peer labs committed .pyc files."""
|
|
185
|
+
_init_and_scaffold(tmp_path, monkeypatch)
|
|
186
|
+
state = find_paircode()
|
|
187
|
+
ensure_peer_labs(state)
|
|
188
|
+
lab = peer_lab_path(state, "peer-a-codex")
|
|
189
|
+
gitignore = lab / ".gitignore"
|
|
190
|
+
assert gitignore.exists()
|
|
191
|
+
content = gitignore.read_text()
|
|
192
|
+
assert "__pycache__/" in content
|
|
193
|
+
assert "*.py[cod]" in content
|
|
194
|
+
assert LAB_GITIGNORE_MARKER in content
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_ensure_lab_gitignore_appends_to_existing(tmp_path):
|
|
198
|
+
"""If user's project already has a .gitignore (rsync brought it over),
|
|
199
|
+
the peerlab block is APPENDED — user rules preserved."""
|
|
200
|
+
lab = tmp_path
|
|
201
|
+
(lab / ".gitignore").write_text("# user project rules\nsecrets.env\n")
|
|
202
|
+
assert ensure_lab_gitignore(lab) is True
|
|
203
|
+
content = (lab / ".gitignore").read_text()
|
|
204
|
+
assert "secrets.env" in content # user rule preserved
|
|
205
|
+
assert LAB_GITIGNORE_MARKER in content # our block added
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_ensure_lab_gitignore_is_idempotent(tmp_path):
|
|
209
|
+
"""Second call no-ops if marker already there."""
|
|
210
|
+
lab = tmp_path
|
|
211
|
+
assert ensure_lab_gitignore(lab) is True
|
|
212
|
+
before = (lab / ".gitignore").read_text()
|
|
213
|
+
assert ensure_lab_gitignore(lab) is False
|
|
214
|
+
assert (lab / ".gitignore").read_text() == before
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_initial_commit_authored_as_peer_id(tmp_path, monkeypatch):
|
|
218
|
+
"""Initial seed commit must be authored as the peer_id so replay
|
|
219
|
+
(git log) attributes work to each peer. Fixes v0.12.0 where commits
|
|
220
|
+
were authored as generic 'paircode-peerlab'."""
|
|
221
|
+
_init_and_scaffold(tmp_path, monkeypatch)
|
|
222
|
+
state = find_paircode()
|
|
223
|
+
ensure_peer_labs(state)
|
|
224
|
+
for pid in ("peer-a-codex", "peer-b-gemini"):
|
|
225
|
+
lab = peer_lab_path(state, pid)
|
|
226
|
+
r = subprocess.run(
|
|
227
|
+
["git", "-C", str(lab), "log", "-1", "--format=%an <%ae>"],
|
|
228
|
+
capture_output=True, text=True, check=False,
|
|
229
|
+
)
|
|
230
|
+
assert pid in r.stdout, f"commit author should contain {pid}; got {r.stdout}"
|
|
231
|
+
assert f"{pid}@peerlab.local" in r.stdout
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_different_peers_produce_different_initial_commits(tmp_path, monkeypatch):
|
|
235
|
+
"""Distinct peer_ids as commit authors mean the seed commit SHAs differ
|
|
236
|
+
across labs — proves independence beyond 'same content, same author,
|
|
237
|
+
accidental SHA collision'."""
|
|
238
|
+
_init_and_scaffold(tmp_path, monkeypatch)
|
|
239
|
+
state = find_paircode()
|
|
240
|
+
ensure_peer_labs(state)
|
|
241
|
+
codex_lab = peer_lab_path(state, "peer-a-codex")
|
|
242
|
+
gemini_lab = peer_lab_path(state, "peer-b-gemini")
|
|
243
|
+
|
|
244
|
+
def head_sha(lab):
|
|
245
|
+
r = subprocess.run(
|
|
246
|
+
["git", "-C", str(lab), "rev-parse", "HEAD"],
|
|
247
|
+
capture_output=True, text=True, check=False,
|
|
248
|
+
)
|
|
249
|
+
return r.stdout.strip()
|
|
250
|
+
|
|
251
|
+
assert head_sha(codex_lab) != head_sha(gemini_lab), (
|
|
252
|
+
"initial seed commits should differ across labs since authors differ"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
179
256
|
def test_seed_excludes_dot_paircode_and_dot_git(tmp_path, monkeypatch):
|
|
180
257
|
"""Seeded labs must not contain `.paircode/` (recursion) or the outer
|
|
181
258
|
repo's `.git/` (each lab gets its own fresh git instead)."""
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: peerlab — each peer does real work in its own parallel lab with its own git; team lead reads the diffs and 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 — they write code, run tests, commit as they go. Your job: fire every peer on the user's prompt, read what they actually did (the diffs), and synthesize.
|
|
6
|
-
|
|
7
|
-
This is a separate concept from `/paircode` — no focus dirs, no stages, no markdown consensus files. The deliverable is real code evolution per lab + your synthesis at the end.
|
|
8
|
-
|
|
9
|
-
`$ARGUMENTS` is the user's raw prompt. Pass it through verbatim.
|
|
10
|
-
|
|
11
|
-
## Step 1 — Bootstrap (silent via Bash)
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
paircode peerlab ensure # scaffolds + seeds + git-inits each lab (idempotent)
|
|
15
|
-
PEERS=$(paircode roster --alpha claude)
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
`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.
|
|
19
|
-
|
|
20
|
-
## Step 2 — Fire every peer in parallel
|
|
21
|
-
|
|
22
|
-
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.
|
|
23
|
-
|
|
24
|
-
Each subagent's prompt:
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
You are monitoring the {peer-id} peer. Run this shell command and wait:
|
|
28
|
-
|
|
29
|
-
paircode peerlab invoke {peer-id} "<the user's prompt verbatim>"
|
|
30
|
-
|
|
31
|
-
That fires the peer with its cwd set to .peerlab/{peer-id}/ (its own lab).
|
|
32
|
-
The peer is expected to commit its work in its own .git before finishing.
|
|
33
|
-
|
|
34
|
-
After it returns, gather:
|
|
35
|
-
1. The peer's stdout (its narrative of what it did).
|
|
36
|
-
2. `git -C .peerlab/{peer-id} log --oneline -5` — recent commits.
|
|
37
|
-
3. `git -C .peerlab/{peer-id} diff HEAD~1 HEAD --stat` — what changed.
|
|
38
|
-
|
|
39
|
-
Report back to team lead in under 400 words: peer-id, ok=yes/no, duration,
|
|
40
|
-
1-line summary of what the peer claims it did, plus the git stat output.
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Step 3 — Read the actual code each peer wrote
|
|
44
|
-
|
|
45
|
-
When every peer has reported, for each peer-id:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
git -C .peerlab/<peer-id> log --oneline -5
|
|
49
|
-
git -C .peerlab/<peer-id> diff HEAD~1 HEAD
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Read the actual diffs (use the Read tool on the changed files inside each lab, or just the full `git diff` output). This is the real content — NOT the peer's narrative in stdout. Trust the code over the claims.
|
|
53
|
-
|
|
54
|
-
## Step 4 — Synthesize (one message to user, ≤300 words)
|
|
55
|
-
|
|
56
|
-
Produce a tight team-lead synthesis:
|
|
57
|
-
|
|
58
|
-
- **Per-peer summary**: 1–2 lines each — files touched, LOC delta, the shape of the approach.
|
|
59
|
-
- **Cross-cutting themes**: did multiple peers hit the same issue/solution? That's signal.
|
|
60
|
-
- **Honest head-to-head**: which lab made the cleanest move? Which went off-track? Don't play favorites but don't hedge.
|
|
61
|
-
- **What to pull into alpha**: if any peer's approach is objectively better than what alpha would do, name the files + the approach. Don't do the pull yourself — just recommend.
|
|
62
|
-
|
|
63
|
-
No focus dirs. No consensus.md. No markdown artifacts on disk. Just the synthesis as your reply to the user.
|
|
64
|
-
|
|
65
|
-
## Guardrails
|
|
66
|
-
|
|
67
|
-
- **Alpha (this session) does NOT edit `.peerlab/<peer-id>/`.** Peers own their labs. You only read.
|
|
68
|
-
- **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 it, I read the diffs."
|
|
69
|
-
- **Fire every peer** unless the user's prompt explicitly names `--peer`/`--peers`. Watchdog protects against hangs.
|
|
70
|
-
- **If a peer fails** (hung, errored, no commits), note it in the synthesis and continue. One flaky peer doesn't sink the run.
|
|
71
|
-
- **No AI attribution** in anything you write — see the maintainer's CLAUDE.md.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|