paircode 0.13.1__tar.gz → 0.14.0__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.13.1 → paircode-0.14.0}/.gitignore +4 -0
- {paircode-0.13.1 → paircode-0.14.0}/PKG-INFO +2 -2
- paircode-0.14.0/diary/004-gemini-silent-flash-downgrade.md +44 -0
- {paircode-0.13.1 → paircode-0.14.0}/pyproject.toml +2 -2
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/__init__.py +1 -1
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/cli.py +4 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/detect.py +5 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/handshake.py +6 -1
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/installer.py +28 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/state.py +21 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/templates/peers.yaml +5 -1
- paircode-0.14.0/src/paircode/templates/pi/prompts/paircode.md +95 -0
- paircode-0.14.0/tests/test_handshake.py +28 -0
- {paircode-0.13.1 → paircode-0.14.0}/LICENSE +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/README.md +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/diary/001-step-a-architecture.md +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/diary/002-v0.10-release-pipeline.md +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/diary/003-arch-b-pivot-grappling.md +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/__main__.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/converge.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/peerlab.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/runner.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/templates/FOCUS.md +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/templates/JOURNEY.md +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/templates/claude/commands/paircode.md +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/templates/claude/commands/peerlab.md +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/templates/codex/commands/paircode.md +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/templates/gemini/commands/paircode.toml +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/src/paircode/util.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/tests/__init__.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/tests/test_cli_smoke.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/tests/test_converge.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/tests/test_peerlab.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/tests/test_smoke.py +0 -0
- {paircode-0.13.1 → paircode-0.14.0}/tests/test_state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paircode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
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.
|
|
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,44 @@
|
|
|
1
|
+
# Diary 004 — The silent free-flash downgrade: an unpinned peer rubber-stamped a consensus
|
|
2
|
+
|
|
3
|
+
**Stardate:** 2026-06-17
|
|
4
|
+
**Verified state:** `main`, 81 tests passing. Changes this entry: `handshake.py` (add `ProposedPeer.model`, correct the stale gemini note), `templates/peers.yaml` (model-field warning), `tests/test_handshake.py` (new).
|
|
5
|
+
**TL;DR:** paircode *already* supports per-peer model pinning end-to-end. The bug was that pinning is optional and invisible, and a misconfigured CLI fails **silently** — so a `peer-b-gemini` ran `gemini-2.0-flash-001` for an entire plan consensus while masquerading as a real fork peer.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What triggered this
|
|
10
|
+
|
|
11
|
+
During an awali working session, `/paircode` was used to review an implementation plan. The Gemini peer "converged" enthusiastically with alpha — suspiciously agreeable. The maintainer's instinct: *"I don't think it's running Gemini Pro... I needed the best model possible instead of small models that just agree with you."*
|
|
12
|
+
|
|
13
|
+
He was right. Investigation found the peer was running **`gemini-2.0-flash-001`** — the free-tier default — not a Pro model. Its "agreement" was flash-tier noise that polluted the consensus. The only load-bearing peer that session was Codex.
|
|
14
|
+
|
|
15
|
+
## Why it happened — four silent failures stacked
|
|
16
|
+
|
|
17
|
+
1. **gemini-cli auth was `oauth-personal`.** That ignores any API key entirely and uses the free Google login → flash. The paid key in the env was never consulted.
|
|
18
|
+
2. **The account was free tier.** `gemini-2.5-pro` returned `limit: 0` — free tier grants *zero* Pro requests, not "rate-limited," literally none. (Fixed by the maintainer adding $25 credit → tier-1 paid.)
|
|
19
|
+
3. **No model was pinned.** paircode passes no `-m` unless a peer has a `model:` field; the awali roster had none, so the CLI default (flash) won.
|
|
20
|
+
4. **Nothing surfaced any of it.** The file-trace header read `<!-- model: (default) -->` — technically honest, operationally a lie. A weak model rubber-stamping consensus is the worst silent failure for an adversarial-review tool.
|
|
21
|
+
|
|
22
|
+
## Key discoveries (the plumbing was already right)
|
|
23
|
+
|
|
24
|
+
- **paircode supports per-peer model pinning, fully wired:** `peers.yaml model:` → `cli.py:309 peer.get("model")` → `runner.run_peer(model=...)` → `cliworker.get_spec("gemini", model=...)` → `gemini -m <model>`. Proven by `cliworker/tests/test_registry.py:64-67`. No code was missing — only discoverability and a sane default.
|
|
25
|
+
- **cliworker already strips the stale key.** `registry.py` gemini spec: `env_strip=["GOOGLE_API_KEY", "GEMINI_API_KEY"]`. So even with a dead `AIza` key baked into the running Claude Code session, cliworker removes it before invoking gemini, which then falls through to `~/.gemini/.env`. **No shell restart or `~/.zshenv` edit is needed** — an earlier "you must restart" hypothesis was wrong and is retracted here.
|
|
26
|
+
- **gemini-cli env loader has two traps** (`chunk-*.js` `loadEnvironment`/`findEnvFile`): (a) dotenv **never overrides** an existing `process.env` var; (b) discovery returns the **first** `.env` walking up — a Laravel repo root `.env` wins unless `advanced.ignoreLocalEnv: true`. Both matter for getting the key to land.
|
|
27
|
+
- **There is no Gemini 3.5 Pro.** Pro and Flash version independently: Flash reached 3.5 (`gemini-3.5-flash`); the newest *Pro* is `gemini-3.1-pro-preview`. Cost magnitude (paid, /1M tok in/out, ≤200k): 3.1 Pro Preview $2/$12 · 2.5 Pro $1.25/$10 · 3.5 Flash $1.50/$9 (Flash output now near-Pro!) · 2.5 Flash $0.30/$2.50. A paircode peer call ≈ 1–5¢ regardless — cost is not the constraint, quality is.
|
|
28
|
+
|
|
29
|
+
## The fix (this repo)
|
|
30
|
+
|
|
31
|
+
The systemic root cause lived in `handshake.py`: `ProposedPeer` had no `model` field, so every auto-generated roster was born unpinned, and `_PEER_RANK` hardcoded the now-false belief *"gemini = Free tier, not for full forks."*
|
|
32
|
+
|
|
33
|
+
- `ProposedPeer` gains `model: str = ""`. Because `asdict()` includes defaulted fields, **every generated roster now emits `model: ''`** — the lever is visible instead of hidden. Empty stays safely falsy downstream (`cli.py` passes no `-m`).
|
|
34
|
+
- The stale gemini note is corrected to: *"Pin model: to a Pro tier — unpinned + a free/oauth login silently runs weak flash."* No model version is hardcoded (they rot fast — see "no 3.5 Pro").
|
|
35
|
+
- `templates/peers.yaml` documents the `model` field with the silent-downgrade warning.
|
|
36
|
+
- `tests/test_handshake.py` guards the new surface (model key present + pin round-trips).
|
|
37
|
+
|
|
38
|
+
External (not this repo, recorded for trace): the maintainer's gemini-cli was reconfigured — `settings.json` auth → `gemini-api-key`, `advanced.ignoreLocalEnv: true`, `~/.gemini/.env` holding the paid key. The awali `.paircode/peers.yaml` should add `model: gemini-3.1-pro-preview` to its gemini peer — that is the per-peer, paircode-native pin and supersedes any global settings.json model.
|
|
39
|
+
|
|
40
|
+
## Open paper cuts
|
|
41
|
+
|
|
42
|
+
- **Resolved model is still not surfaced.** The header shows `(default)` when unpinned; paircode can't report the model the CLI actually chose because cliworker doesn't return it. A future improvement: have cliworker echo the resolved model back in `CLIResult` so the file-trace records what *ran*, not what was *requested*. That would have caught this in seconds.
|
|
43
|
+
- **No preflight auth/tier check.** paircode can't tell a Pro peer from a free-flash one before spending a fork on it. A `paircode doctor`-style probe (cheap 1-token call asking tier) could warn "peer-b-gemini resolves to flash — pin a model?" at roster time.
|
|
44
|
+
- **`propose_roster()` still emits `model: ''` for all CLIs.** It does not suggest a concrete Pro default per CLI (deliberately, to avoid version rot). If model-name stability ever improves, a per-CLI recommended default could be added behind `_PEER_RANK`.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "paircode"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.14.0"
|
|
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" }
|
|
@@ -39,7 +39,7 @@ dependencies = [
|
|
|
39
39
|
"click>=8.1",
|
|
40
40
|
"pyyaml>=6.0",
|
|
41
41
|
"rich>=13.0",
|
|
42
|
-
"cliworker>=0.8.
|
|
42
|
+
"cliworker>=0.8.5",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
[project.urls]
|
|
@@ -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
|
|
@@ -33,6 +33,11 @@ KNOWN_CLIS: dict[str, tuple[str, Path, str]] = {
|
|
|
33
33
|
Path.home() / ".gemini",
|
|
34
34
|
"Install Gemini CLI: `npm i -g @google/gemini-cli`",
|
|
35
35
|
),
|
|
36
|
+
"pi": (
|
|
37
|
+
"pi",
|
|
38
|
+
Path.home() / ".pi",
|
|
39
|
+
"Install Pi coding agent: `npm i -g @earendil-works/pi-coding-agent`",
|
|
40
|
+
),
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
|
|
@@ -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", "
|
|
33
|
+
("gemini", "low", "Pin model: to a Pro tier — unpinned + a free/oauth login silently runs weak flash"),
|
|
29
34
|
]
|
|
30
35
|
|
|
31
36
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Strategy per CLI:
|
|
4
4
|
- Claude Code: file-drop ~/.claude/commands/paircode.md
|
|
5
|
+
- Pi: file-drop ~/.pi/agent/prompts/paircode.md ($@ args, bash-parallel peers)
|
|
5
6
|
Claude's native plugin-install requires publishing a marketplace;
|
|
6
7
|
overkill for one command. File-drop is first-class + documented.
|
|
7
8
|
- Codex CLI: native register via `codex marketplace add starshipagentic/paircode-codex`
|
|
@@ -80,6 +81,31 @@ def install_claude(info: CliInfo) -> InstallResult:
|
|
|
80
81
|
)
|
|
81
82
|
|
|
82
83
|
|
|
84
|
+
def install_pi(info: CliInfo) -> InstallResult:
|
|
85
|
+
"""Write ~/.pi/agent/prompts/paircode.md — file-drop (native for Pi coding agent).
|
|
86
|
+
|
|
87
|
+
Pi discovers prompt templates in ~/.pi/agent/prompts/ as /name slash commands.
|
|
88
|
+
The pi variant of the template uses `$@` for arguments (Pi's convention) and
|
|
89
|
+
fires peers as parallel background bash jobs (Pi has no stock Agent tool).
|
|
90
|
+
"""
|
|
91
|
+
if not info.installed:
|
|
92
|
+
return InstallResult(
|
|
93
|
+
cli_name="pi", action="skipped", path=None,
|
|
94
|
+
message=f"pi CLI not on PATH. {info.install_hint}",
|
|
95
|
+
)
|
|
96
|
+
prompts_dir = info.config_dir / "agent" / "prompts"
|
|
97
|
+
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
target = prompts_dir / "paircode.md"
|
|
99
|
+
target.write_text(
|
|
100
|
+
_read_template("pi/prompts/paircode.md"),
|
|
101
|
+
encoding="utf-8",
|
|
102
|
+
)
|
|
103
|
+
return InstallResult(
|
|
104
|
+
cli_name="pi", action="installed", path=target,
|
|
105
|
+
message=f"Wrote /paircode prompt template to {prompts_dir}.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
83
109
|
def _codex_already_installed() -> bool:
|
|
84
110
|
"""Idempotency check: is the paircode marketplace already registered?"""
|
|
85
111
|
config = Path.home() / ".codex" / "config.toml"
|
|
@@ -181,6 +207,7 @@ def install_all() -> list[InstallResult]:
|
|
|
181
207
|
"claude": install_claude,
|
|
182
208
|
"codex": install_codex,
|
|
183
209
|
"gemini": install_gemini,
|
|
210
|
+
"pi": install_pi,
|
|
184
211
|
}
|
|
185
212
|
results: list[InstallResult] = []
|
|
186
213
|
for name, info in detected.items():
|
|
@@ -227,6 +254,7 @@ def uninstall_all() -> list[InstallResult]:
|
|
|
227
254
|
claude_paths = [
|
|
228
255
|
claude_home / "commands" / "paircode.md",
|
|
229
256
|
claude_home / "commands" / "peerlab.md",
|
|
257
|
+
Path.home() / ".pi" / "agent" / "prompts" / "paircode.md",
|
|
230
258
|
claude_home / "agents" / "paircode-peer.md",
|
|
231
259
|
]
|
|
232
260
|
removed = []
|
|
@@ -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"),
|
|
@@ -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
|
|
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,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "paircode — adversarial multi-LLM peer review. Fire peers in parallel, synthesize, write consensus to .paircode/. Usage: /paircode \"<prompt>\" [--peer id | --peers id,id]"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
You are the **team lead** for a paircode peer-review cycle, running inside a pi session
|
|
6
|
+
(alpha = you, whatever model this pi session runs). Your job: get the user a genuinely
|
|
7
|
+
adversarial second opinion by firing every peer LLM in parallel, reading what they wrote,
|
|
8
|
+
and synthesizing an honest consensus. All thoughts land on disk as Markdown under `.paircode/`.
|
|
9
|
+
|
|
10
|
+
**$@** is the full user input after `/paircode`: a quoted prompt, optionally `--peer <id>`
|
|
11
|
+
or `--peers <id,id>` filter flags.
|
|
12
|
+
|
|
13
|
+
## File naming (inside every $FOCUS/{stage}/)
|
|
14
|
+
- `alpha-vN.md` — your successive takes · `{peer-id}-vN.md` — each peer's takes
|
|
15
|
+
- `reviews/round-N-{peer-id}-critiques-alpha.md` — peer critiques of alpha
|
|
16
|
+
- `*-FINAL.md` (via `paircode converge {stage}`) · `consensus.md` — your synthesis (last write)
|
|
17
|
+
|
|
18
|
+
**Reports vs code:** files in `$FOCUS/{stage}/` are always markdown reports. Code lives in
|
|
19
|
+
sandboxes: peers in `.paircode/sandbox/{peer-id}/`, alpha (you) directly in the project root.
|
|
20
|
+
Sandboxes are always available, never required — code only when it materially sharpens an answer.
|
|
21
|
+
|
|
22
|
+
## Step 1 — Bootstrap (silent, via bash)
|
|
23
|
+
```bash
|
|
24
|
+
paircode ensure-scaffold
|
|
25
|
+
FOCUS=$(paircode focus new "<slug-from-prompt>" --prompt "<the quoted prompt verbatim>")
|
|
26
|
+
PEERS=$(paircode roster --alpha pi <user's --peer/--peers flags if any>)
|
|
27
|
+
```
|
|
28
|
+
Trust the roster. $FOCUS has research/ plan/ execute/ ask/ subdirs ready (each with reviews/).
|
|
29
|
+
**Bootstrap discipline:** run `paircode focus new` EXACTLY ONCE and capture its stdout into $FOCUS. If you lose the value, do NOT re-run it — recover with `FOCUS=$(ls -td .paircode/focus-* | head -1)`. Strip `--peer`/`--peers` flags OUT of the text before passing the quoted prompt to `--prompt` (flags go to `paircode roster`, never into FOCUS.md).
|
|
30
|
+
|
|
31
|
+
## Step 2 — Pick the flow (commit to the WHOLE flow)
|
|
32
|
+
| Flow | Trigger |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `ask` | "what do you think", "review/critique X", "second opinion" |
|
|
35
|
+
| `research` | "figure out", "explore", "how should we" |
|
|
36
|
+
| `research → plan` | "plan/design/lay out X" |
|
|
37
|
+
| `research → plan → execute` | "build/fix/implement/ship/do" — most imperatives. **Default when ambiguous.** |
|
|
38
|
+
| `execute` | "execute the plan at focus-NN" |
|
|
39
|
+
|
|
40
|
+
Append `flow: <flow>` to `$FOCUS/FOCUS.md`. {stage} = first stage of {flow}.
|
|
41
|
+
|
|
42
|
+
## Step 3 — Fire peers in PARALLEL (background bash, one job per peer)
|
|
43
|
+
Construct a stage-appropriate peer prompt (see shapes below), then launch ALL peers at once:
|
|
44
|
+
```bash
|
|
45
|
+
for P in $PEERS; do
|
|
46
|
+
nohup paircode invoke "$P" "<stage-appropriate prompt>" --out "$FOCUS/{stage}/$P-v1.md" \
|
|
47
|
+
> "$FOCUS/{stage}/$P-invoke.log" 2>&1 &
|
|
48
|
+
done
|
|
49
|
+
wait # blocks until every peer returns
|
|
50
|
+
```
|
|
51
|
+
Stage prompt shapes:
|
|
52
|
+
- **research**: honest, skeptical, specific cold take on the prompt; sandbox scripts welcome to ground claims in real data; clean markdown, no preamble.
|
|
53
|
+
- **plan**: concrete numbered plan (goal, scope, steps, risks, success criteria), KISS; read `../research/*-FINAL.md` if present.
|
|
54
|
+
- **execute**: carry out `$FOCUS/plan/*-FINAL.md`; code/tests in your sandbox `.paircode/sandbox/{peer-id}/` only; report = what you built/ran, files, verification, open items.
|
|
55
|
+
- **ask**: severity-ranked critique with file:line citations of the artifact FOCUS.md points to.
|
|
56
|
+
|
|
57
|
+
## Step 4 — Alpha's own take (while peers run, before `wait` if you can)
|
|
58
|
+
Write `$FOCUS/{stage}/alpha-v1.md` yourself — same stage rules. Your edge: this session's full
|
|
59
|
+
context. Execute-stage asymmetry: peers code in sandboxes; you code directly in the project root
|
|
60
|
+
(alpha-vN.md is the report; code lives in the repo).
|
|
61
|
+
|
|
62
|
+
## Step 5 — Read everything
|
|
63
|
+
After `wait`: read every `{peer-id}-v1.md` + your alpha file. Where did peers agree, clash,
|
|
64
|
+
surface what you missed? Spot-check 1-2 cited claims against the actual repo.
|
|
65
|
+
|
|
66
|
+
## Step 6 — Round convergence
|
|
67
|
+
Quick one-shot prompt answered well in round 1 → converge. **Otherwise default to another
|
|
68
|
+
round**: peers critique alpha (`reviews/round-N-{peer-id}-critiques-alpha.md` via the same
|
|
69
|
+
parallel-invoke pattern), you write `alpha-v(N+1).md`, loop to Step 5. Stop when a round adds
|
|
70
|
+
no new friction. No hard cap; use judgment.
|
|
71
|
+
|
|
72
|
+
## Step 7 — Converge stage + consensus
|
|
73
|
+
```bash
|
|
74
|
+
paircode converge {stage}
|
|
75
|
+
```
|
|
76
|
+
Read every `*-FINAL.md`, then WRITE `$FOCUS/{stage}/consensus.md`:
|
|
77
|
+
`# Consensus — {focus} — {stage}` with sections: **Where peers agreed / Where peers clashed /
|
|
78
|
+
Team-lead verdict (2-3 honest paragraphs) / Next action (one concrete thing)**.
|
|
79
|
+
Adversarial-but-honest. No pile-on, no rubber-stamping.
|
|
80
|
+
|
|
81
|
+
## Step 8 — Advance the flow (mechanical — do NOT ask permission)
|
|
82
|
+
{stage} = next stage in {flow}; jump to Step 3 (skip Steps 1-2). Previous stage's `*-FINAL.md`
|
|
83
|
+
feed the next stage's prompts. The user's prompt was implicit go-ahead for the WHOLE flow;
|
|
84
|
+
stop early only on a hard blocker, and say so in the final report. Last stage done → Step 9.
|
|
85
|
+
|
|
86
|
+
## Step 9 — Final report (under 200 words)
|
|
87
|
+
Focus path · stages+rounds run (e.g. `research (2) → plan (1) → execute (1)`) · peers that
|
|
88
|
+
participated/failed · one-sentence headline from the last consensus.md.
|
|
89
|
+
|
|
90
|
+
## Guardrails
|
|
91
|
+
- Never skip Step 1; never invent peer commands — always `paircode invoke <id> "..." --out <path>`.
|
|
92
|
+
- Fire EVERY rostered peer unless the user passed `--peer`/`--peers`.
|
|
93
|
+
- Peers never touch files outside their sandbox; you never touch theirs.
|
|
94
|
+
- If a peer invoke hard-fails, note it and continue with the rest — one dead peer never blocks the cycle.
|
|
95
|
+
- No AI attribution in any artifact you write.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Handshake roster shape — guards the silent free-tier downgrade fix.
|
|
2
|
+
|
|
3
|
+
Diary 004: an unpinned gemini peer on a free/oauth login silently ran a weak
|
|
4
|
+
flash model and rubber-stamped a consensus. The fix makes the `model` field
|
|
5
|
+
present in every generated roster so pinning is discoverable, and keeps an
|
|
6
|
+
empty pin safely falsy downstream (cli.py treats "" as "CLI default").
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from paircode.handshake import ProposedPeer, proposed_as_yaml_dicts
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_generated_roster_exposes_model_key_for_pinning():
|
|
14
|
+
"""Every proposed peer must surface a `model` key, even when unpinned,
|
|
15
|
+
so users see the lever instead of falling into the silent-flash trap."""
|
|
16
|
+
peers = [ProposedPeer(id="peer-a-gemini", cli="gemini", priority="low", notes="x")]
|
|
17
|
+
dicts = proposed_as_yaml_dicts(peers)
|
|
18
|
+
assert "model" in dicts[0]
|
|
19
|
+
assert dicts[0]["model"] == "" # empty = CLI default; falsy, so no -m flag is passed
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_model_pin_round_trips_into_roster_dict():
|
|
23
|
+
"""A pinned model survives into the yaml dict that gets written to peers.yaml."""
|
|
24
|
+
p = ProposedPeer(
|
|
25
|
+
id="peer-a-gemini", cli="gemini", priority="high",
|
|
26
|
+
notes="", model="gemini-3.1-pro-preview",
|
|
27
|
+
)
|
|
28
|
+
assert proposed_as_yaml_dicts([p])[0]["model"] == "gemini-3.1-pro-preview"
|
|
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
|