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