paircode 0.12.1__tar.gz → 0.12.3__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.3}/PKG-INFO +1 -1
- {paircode-0.12.1 → paircode-0.12.3}/pyproject.toml +1 -1
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/__init__.py +1 -1
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/peerlab.py +61 -7
- {paircode-0.12.1 → paircode-0.12.3}/tests/test_peerlab.py +77 -0
- {paircode-0.12.1 → paircode-0.12.3}/.gitignore +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/LICENSE +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/README.md +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/diary/001-step-a-architecture.md +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/diary/002-v0.10-release-pipeline.md +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/diary/003-arch-b-pivot-grappling.md +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/__main__.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/cli.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/converge.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/detect.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/handshake.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/installer.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/runner.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/state.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/templates/FOCUS.md +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/templates/JOURNEY.md +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/templates/claude/commands/paircode.md +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/templates/claude/commands/peerlab.md +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/templates/codex/commands/paircode.md +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/templates/gemini/commands/paircode.toml +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/templates/peers.yaml +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/src/paircode/util.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/tests/__init__.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/tests/test_cli_smoke.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/tests/test_converge.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/tests/test_smoke.py +0 -0
- {paircode-0.12.1 → paircode-0.12.3}/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.3
|
|
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.3"
|
|
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
|
|
|
@@ -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)."""
|
|
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
|
|
File without changes
|