crossagent 0.1.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.
@@ -0,0 +1,18 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+
14
+ # OS / editor
15
+ .DS_Store
16
+ *.swp
17
+ .idea/
18
+ .vscode/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dat Nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: crossagent
3
+ Version: 0.1.0
4
+ Summary: Get a second opinion from another AI coding agent. Ask Claude, Codex, OpenCode, CommandCode, or Gemini — from inside the agent you already use.
5
+ Project-URL: Homepage, https://github.com/datj9/crossagent
6
+ Project-URL: Repository, https://github.com/datj9/crossagent
7
+ Project-URL: Issues, https://github.com/datj9/crossagent/issues
8
+ Project-URL: Changelog, https://github.com/datj9/crossagent/blob/main/CHANGELOG.md
9
+ Author: Dat Nguyen
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,ai,claude,cli,codex,coding-agent,mcp,opencode,second-opinion
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Requires-Python: >=3.9
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # crossagent
31
+
32
+ **Get a second opinion from another AI agent — without leaving the one you're in.**
33
+
34
+ `crossagent` lets your coding agent (Claude Code, Codex, Cursor, Cline, OpenCode, CommandCode…) pause and ask a *different* agent to independently evaluate a hard call — an architecture decision, a gnarly bug, a prompt rewrite — then hands you both viewpoints so **you** decide. One agent proposes; another one challenges. You get the disagreement, not just an echo.
35
+
36
+ ```bash
37
+ crossagent --agent claude --name payments-retry-design --prompt-file /tmp/decision.md
38
+ ```
39
+
40
+ - 🤝 **Cross-agent, by design.** Ask **Claude, Codex, OpenCode, CommandCode, or Gemini** — from inside whichever agent you already use.
41
+ - 🧠 **A teammate, not an oracle.** Ships a second-opinion protocol that forces an evidence-backed critique with cited files, risks, and a recommendation — then a synthesis step, so the second opinion sharpens *your* judgment instead of replacing it.
42
+ - 🔁 **Named, resumable Claude sessions.** Reuse a name to continue the same decision; fork to explore a branch. No re-explaining context every turn.
43
+ - 📡 **Long-running & streamed.** No default timeout, no default cost cap. It waits until the advisor finishes and streams progress as it goes.
44
+ - 🧩 **Agent Skill + CLI.** Installs as an [Agent Skill](https://agentskills.dev) *and* a standalone `crossagent` command. Zero runtime dependencies.
45
+ - 🔓 **Local & open.** Runs entirely on your machine against CLIs you already have. MIT licensed.
46
+
47
+ ---
48
+
49
+ ## Why
50
+
51
+ Coding agents are confident. That's the problem. The same model that writes the code also reviews it, so a plausible-but-wrong call sails straight through. The fix engineers already use with each other — *"let me get a second pair of eyes"* — works for agents too, but only if the second agent is **actually different** and is briefed well enough to disagree on the merits.
52
+
53
+ `crossagent` makes that a one-liner. Package the decision, hand it to a peer agent, get back a structured critique — Position / Evidence / Risks / Recommendation / Unresolved — and reconcile it with your own view before you commit.
54
+
55
+ ## Install
56
+
57
+ Install the CLI from PyPI (using `pipx` keeps command-line applications isolated):
58
+
59
+ ```bash
60
+ pipx install crossagent
61
+ # or: pip install crossagent
62
+
63
+ crossagent --list-advisors
64
+ ```
65
+
66
+ You also need at least one advisor CLI on your PATH — e.g. [`claude`](https://docs.claude.com/claude-code), `codex`, `opencode`, `commandcode`, or `gemini`.
67
+
68
+ To install both the CLI and the Agent Skill from the repository:
69
+
70
+ ```bash
71
+ git clone https://github.com/datj9/crossagent.git
72
+ cd crossagent
73
+ ./install.sh
74
+ ```
75
+
76
+ The installer checks the supported agent config dirs (`~/.claude`, `~/.codex`, `~/.config/opencode`, `~/.commandcode`, `~/.cursor`), installs the skill into those present, and installs the CLI via `pipx`/`pip`.
77
+
78
+ ## Quick start
79
+
80
+ Write a compact decision brief, then ask a peer:
81
+
82
+ ```bash
83
+ cat > /tmp/decision.md <<'EOF'
84
+ <decision>Should the payment retry live in the worker or the API layer?</decision>
85
+ <current_state>Retries currently inline in the API handler; p99 latency regressed 40%.</current_state>
86
+ <constraints>No new infra this quarter. Must stay idempotent.</constraints>
87
+ <evidence>src/api/payments.ts:88, worker/queue.ts:12, load test in bench/2026-07.md</evidence>
88
+ <questions>1. Which layer, and why? 2. Strongest counterargument? 3. Cheapest validation?</questions>
89
+ EOF
90
+
91
+ crossagent --agent claude --name payments-retry-design --cwd "$PWD" --prompt-file /tmp/decision.md
92
+ ```
93
+
94
+ The advisor's answer prints to **stdout**; progress and session metadata go to **stderr**. Continue the same decision later — context is remembered:
95
+
96
+ ```bash
97
+ crossagent --name payments-retry-design --prompt-file /tmp/followup.md # auto-resumes
98
+ crossagent --name payments-retry-design --fork-session --prompt-file /tmp/alt.md # branch it
99
+ ```
100
+
101
+ Or let your agent do it for you — just say *"ask Claude about this"* / *"hỏi ý với Claude"* / *"get a second opinion from Codex"* and the skill fires.
102
+
103
+ ## How it works
104
+
105
+ ```
106
+ your agent ──▶ crossagent CLI ──▶ peer agent's CLI (claude -p / codex exec / …)
107
+ ▲ │ │
108
+ └── synthesis ─┴──── streamed ◀──────┘
109
+ (you reconcile both views) result + session id
110
+ ```
111
+
112
+ 1. You (or your agent) package the decision using the [second-opinion protocol](skills/crossagent/references/second-opinion-protocol.md).
113
+ 2. `crossagent` builds the right command for the chosen advisor and keeps it alive until it exits.
114
+ 3. For session-capable advisors, it stores the `session_id` keyed by `advisor:name` so the next turn resumes.
115
+ 4. You compare the two viewpoints and make the call.
116
+
117
+ ## Advisors
118
+
119
+ | Advisor | Command | Status | Sessions |
120
+ |---|---|---|---|
121
+ | `claude` | `claude -p` | ✅ full | resume, name, fork, streamed |
122
+ | `codex` | `codex exec` | 🧪 experimental | text output |
123
+ | `opencode` | `opencode run` | 🧪 experimental | text output |
124
+ | `commandcode` | `commandcode -p` | 🧪 experimental | text output |
125
+ | `gemini` | `gemini -p` | 🧪 experimental | text output |
126
+
127
+ Experimental advisors ship best-effort default flags. If your install differs, fix them without touching code — see below.
128
+
129
+ ### Add or fix an advisor
130
+
131
+ Create `~/.config/crossagent/advisors.json`:
132
+
133
+ ```json
134
+ {
135
+ "advisors": {
136
+ "codex": { "executable": "codex", "base_args": ["exec", "--full-auto"] },
137
+ "myllm": { "executable": "myllm", "prompt_delivery": "flag:-q", "model_flag": "--model" }
138
+ }
139
+ }
140
+ ```
141
+
142
+ Fields layer onto the built-ins, so you only specify what differs. `prompt_delivery` is `dashdash` (prompt after `--`), `positional` (prompt as last arg), or `flag:<flag>` (prompt is the value of a flag).
143
+
144
+ ## Skill usage inside an agent
145
+
146
+ Once installed, the skill auto-triggers on phrases like *"ask Claude"*, *"debate with Claude"*, *"ask Codex"*, *"second opinion"*, *"hỏi ý với Claude"*. The agent packages context, runs `crossagent`, and reports both views. See [`skills/crossagent/SKILL.md`](skills/crossagent/SKILL.md).
147
+
148
+ ## Examples
149
+
150
+ - [Architecture decision](examples/architecture-decision.md)
151
+ - [Debugging second opinion](examples/debug-second-opinion.md)
152
+
153
+ ## Security
154
+
155
+ - Secrets never belong in prompts, CLI args, logs, or copied context — the protocol says so and you should enforce it.
156
+ - Second-opinion sessions run read-only by convention; for Claude use `--tools ""` for pure reasoning or restrict with `--allowedTools`.
157
+ - Everything runs locally against CLIs you already trust. `crossagent` adds no network calls of its own.
158
+
159
+ ## Contributing
160
+
161
+ Issues and PRs welcome — especially hardening the experimental advisors against real installs. See [CONTRIBUTING.md](CONTRIBUTING.md).
162
+
163
+ ## License
164
+
165
+ MIT © Dat Nguyen. See [LICENSE](LICENSE).
@@ -0,0 +1,136 @@
1
+ # crossagent
2
+
3
+ **Get a second opinion from another AI agent — without leaving the one you're in.**
4
+
5
+ `crossagent` lets your coding agent (Claude Code, Codex, Cursor, Cline, OpenCode, CommandCode…) pause and ask a *different* agent to independently evaluate a hard call — an architecture decision, a gnarly bug, a prompt rewrite — then hands you both viewpoints so **you** decide. One agent proposes; another one challenges. You get the disagreement, not just an echo.
6
+
7
+ ```bash
8
+ crossagent --agent claude --name payments-retry-design --prompt-file /tmp/decision.md
9
+ ```
10
+
11
+ - 🤝 **Cross-agent, by design.** Ask **Claude, Codex, OpenCode, CommandCode, or Gemini** — from inside whichever agent you already use.
12
+ - 🧠 **A teammate, not an oracle.** Ships a second-opinion protocol that forces an evidence-backed critique with cited files, risks, and a recommendation — then a synthesis step, so the second opinion sharpens *your* judgment instead of replacing it.
13
+ - 🔁 **Named, resumable Claude sessions.** Reuse a name to continue the same decision; fork to explore a branch. No re-explaining context every turn.
14
+ - 📡 **Long-running & streamed.** No default timeout, no default cost cap. It waits until the advisor finishes and streams progress as it goes.
15
+ - 🧩 **Agent Skill + CLI.** Installs as an [Agent Skill](https://agentskills.dev) *and* a standalone `crossagent` command. Zero runtime dependencies.
16
+ - 🔓 **Local & open.** Runs entirely on your machine against CLIs you already have. MIT licensed.
17
+
18
+ ---
19
+
20
+ ## Why
21
+
22
+ Coding agents are confident. That's the problem. The same model that writes the code also reviews it, so a plausible-but-wrong call sails straight through. The fix engineers already use with each other — *"let me get a second pair of eyes"* — works for agents too, but only if the second agent is **actually different** and is briefed well enough to disagree on the merits.
23
+
24
+ `crossagent` makes that a one-liner. Package the decision, hand it to a peer agent, get back a structured critique — Position / Evidence / Risks / Recommendation / Unresolved — and reconcile it with your own view before you commit.
25
+
26
+ ## Install
27
+
28
+ Install the CLI from PyPI (using `pipx` keeps command-line applications isolated):
29
+
30
+ ```bash
31
+ pipx install crossagent
32
+ # or: pip install crossagent
33
+
34
+ crossagent --list-advisors
35
+ ```
36
+
37
+ You also need at least one advisor CLI on your PATH — e.g. [`claude`](https://docs.claude.com/claude-code), `codex`, `opencode`, `commandcode`, or `gemini`.
38
+
39
+ To install both the CLI and the Agent Skill from the repository:
40
+
41
+ ```bash
42
+ git clone https://github.com/datj9/crossagent.git
43
+ cd crossagent
44
+ ./install.sh
45
+ ```
46
+
47
+ The installer checks the supported agent config dirs (`~/.claude`, `~/.codex`, `~/.config/opencode`, `~/.commandcode`, `~/.cursor`), installs the skill into those present, and installs the CLI via `pipx`/`pip`.
48
+
49
+ ## Quick start
50
+
51
+ Write a compact decision brief, then ask a peer:
52
+
53
+ ```bash
54
+ cat > /tmp/decision.md <<'EOF'
55
+ <decision>Should the payment retry live in the worker or the API layer?</decision>
56
+ <current_state>Retries currently inline in the API handler; p99 latency regressed 40%.</current_state>
57
+ <constraints>No new infra this quarter. Must stay idempotent.</constraints>
58
+ <evidence>src/api/payments.ts:88, worker/queue.ts:12, load test in bench/2026-07.md</evidence>
59
+ <questions>1. Which layer, and why? 2. Strongest counterargument? 3. Cheapest validation?</questions>
60
+ EOF
61
+
62
+ crossagent --agent claude --name payments-retry-design --cwd "$PWD" --prompt-file /tmp/decision.md
63
+ ```
64
+
65
+ The advisor's answer prints to **stdout**; progress and session metadata go to **stderr**. Continue the same decision later — context is remembered:
66
+
67
+ ```bash
68
+ crossagent --name payments-retry-design --prompt-file /tmp/followup.md # auto-resumes
69
+ crossagent --name payments-retry-design --fork-session --prompt-file /tmp/alt.md # branch it
70
+ ```
71
+
72
+ Or let your agent do it for you — just say *"ask Claude about this"* / *"hỏi ý với Claude"* / *"get a second opinion from Codex"* and the skill fires.
73
+
74
+ ## How it works
75
+
76
+ ```
77
+ your agent ──▶ crossagent CLI ──▶ peer agent's CLI (claude -p / codex exec / …)
78
+ ▲ │ │
79
+ └── synthesis ─┴──── streamed ◀──────┘
80
+ (you reconcile both views) result + session id
81
+ ```
82
+
83
+ 1. You (or your agent) package the decision using the [second-opinion protocol](skills/crossagent/references/second-opinion-protocol.md).
84
+ 2. `crossagent` builds the right command for the chosen advisor and keeps it alive until it exits.
85
+ 3. For session-capable advisors, it stores the `session_id` keyed by `advisor:name` so the next turn resumes.
86
+ 4. You compare the two viewpoints and make the call.
87
+
88
+ ## Advisors
89
+
90
+ | Advisor | Command | Status | Sessions |
91
+ |---|---|---|---|
92
+ | `claude` | `claude -p` | ✅ full | resume, name, fork, streamed |
93
+ | `codex` | `codex exec` | 🧪 experimental | text output |
94
+ | `opencode` | `opencode run` | 🧪 experimental | text output |
95
+ | `commandcode` | `commandcode -p` | 🧪 experimental | text output |
96
+ | `gemini` | `gemini -p` | 🧪 experimental | text output |
97
+
98
+ Experimental advisors ship best-effort default flags. If your install differs, fix them without touching code — see below.
99
+
100
+ ### Add or fix an advisor
101
+
102
+ Create `~/.config/crossagent/advisors.json`:
103
+
104
+ ```json
105
+ {
106
+ "advisors": {
107
+ "codex": { "executable": "codex", "base_args": ["exec", "--full-auto"] },
108
+ "myllm": { "executable": "myllm", "prompt_delivery": "flag:-q", "model_flag": "--model" }
109
+ }
110
+ }
111
+ ```
112
+
113
+ Fields layer onto the built-ins, so you only specify what differs. `prompt_delivery` is `dashdash` (prompt after `--`), `positional` (prompt as last arg), or `flag:<flag>` (prompt is the value of a flag).
114
+
115
+ ## Skill usage inside an agent
116
+
117
+ Once installed, the skill auto-triggers on phrases like *"ask Claude"*, *"debate with Claude"*, *"ask Codex"*, *"second opinion"*, *"hỏi ý với Claude"*. The agent packages context, runs `crossagent`, and reports both views. See [`skills/crossagent/SKILL.md`](skills/crossagent/SKILL.md).
118
+
119
+ ## Examples
120
+
121
+ - [Architecture decision](examples/architecture-decision.md)
122
+ - [Debugging second opinion](examples/debug-second-opinion.md)
123
+
124
+ ## Security
125
+
126
+ - Secrets never belong in prompts, CLI args, logs, or copied context — the protocol says so and you should enforce it.
127
+ - Second-opinion sessions run read-only by convention; for Claude use `--tools ""` for pure reasoning or restrict with `--allowedTools`.
128
+ - Everything runs locally against CLIs you already trust. `crossagent` adds no network calls of its own.
129
+
130
+ ## Contributing
131
+
132
+ Issues and PRs welcome — especially hardening the experimental advisors against real installs. See [CONTRIBUTING.md](CONTRIBUTING.md).
133
+
134
+ ## License
135
+
136
+ MIT © Dat Nguyen. See [LICENSE](LICENSE).
@@ -0,0 +1,62 @@
1
+ # Example — Architecture decision
2
+
3
+ **Situation:** You're in Codex working on a payments service and you're torn on
4
+ where a retry should live. You want Claude to challenge your leaning before you
5
+ commit.
6
+
7
+ ## 1. Package the decision
8
+
9
+ ```bash
10
+ cat > /tmp/retry.md <<'EOF'
11
+ <role>
12
+ You are a senior teammate in a joint decision. Evaluate independently, inspect
13
+ read-only context if useful, and return an evidence-backed critique. Do not edit files.
14
+ </role>
15
+ <decision>Should the payment retry live in the async worker or the API request path?</decision>
16
+ <current_state>
17
+ Retries are inline in the API handler today. p99 latency regressed 40% under load.
18
+ Idempotency keys already exist on the charge endpoint.
19
+ </current_state>
20
+ <constraints>No new infra this quarter. Must remain idempotent. Ship in 1 week.</constraints>
21
+ <evidence>
22
+ - src/api/payments.ts:88 (inline retry loop)
23
+ - worker/queue.ts:12 (existing queue consumer)
24
+ - bench/2026-07.md (load test showing the regression)
25
+ </evidence>
26
+ <questions>
27
+ 1. Which layer, and why?
28
+ 2. Strongest counterargument to your pick?
29
+ 3. Cheapest way to validate before committing?
30
+ </questions>
31
+ <response_format>Position / Evidence / Risks And Counterarguments / Recommendation / Unresolved Questions</response_format>
32
+ EOF
33
+ ```
34
+
35
+ ## 2. Ask a peer
36
+
37
+ ```bash
38
+ crossagent --agent claude --name payments-retry-design --cwd "$PWD" \
39
+ --tools "Read,Grep" --prompt-file /tmp/retry.md
40
+ ```
41
+
42
+ `--tools "Read,Grep"` lets Claude open the cited files to verify claims, but not modify anything.
43
+
44
+ ## 3. Follow up on the same decision
45
+
46
+ New load numbers came in. Resume — no need to re-explain:
47
+
48
+ ```bash
49
+ cat > /tmp/retry-followup.md <<'EOF'
50
+ Resuming the same decision. New evidence: moving retries to the worker in a spike
51
+ dropped p99 by 35% but added ~2s worst-case settlement delay. Does that change your
52
+ recommendation?
53
+ EOF
54
+
55
+ crossagent --name payments-retry-design --prompt-file /tmp/retry-followup.md
56
+ ```
57
+
58
+ ## 4. Synthesize
59
+
60
+ Reconcile Claude's critique with your own view: where do you agree, where do you
61
+ diverge, and what's the cheapest next check? Make the call — don't just adopt the
62
+ last answer.
@@ -0,0 +1,48 @@
1
+ # Example — Debugging second opinion
2
+
3
+ **Situation:** You've been staring at a flaky test for an hour. Your current agent
4
+ keeps proposing the same fix. Get a fresh, independent diagnosis from a different
5
+ agent before you spend more time.
6
+
7
+ ## 1. Package the symptom, not your theory
8
+
9
+ Give the advisor the evidence and let it form its own hypothesis — don't lead it
10
+ to your conclusion.
11
+
12
+ ```bash
13
+ cat > /tmp/flaky.md <<'EOF'
14
+ <decision>Root cause of the intermittent failure in tests/orders.spec.ts::"cancels a pending order".</decision>
15
+ <current_state>
16
+ Fails ~1 in 5 local runs, ~1 in 3 in CI. Always the same assertion: order.status
17
+ expected "cancelled" but got "pending".
18
+ </current_state>
19
+ <evidence>
20
+ - tests/orders.spec.ts:41
21
+ - src/orders/cancel.ts:20 (awaits a DB write then reads back)
22
+ - No explicit transaction around the write+read.
23
+ </evidence>
24
+ <context_collection>
25
+ You may run: rg, sed, git log -p on these files. Read-only. Do not run the test suite.
26
+ </context_collection>
27
+ <questions>
28
+ 1. Your independent hypothesis for the flakiness?
29
+ 2. What evidence in the files supports or refutes it?
30
+ 3. Smallest reliable reproduction or check?
31
+ </questions>
32
+ <response_format>Position / Evidence / Risks And Counterarguments / Recommendation / Unresolved Questions</response_format>
33
+ EOF
34
+ ```
35
+
36
+ ## 2. Ask a different model than the one that's stuck
37
+
38
+ ```bash
39
+ crossagent --agent claude --name orders-flaky-cancel --cwd "$PWD" \
40
+ --tools "Read,Grep,Bash" --prompt-file /tmp/flaky.md
41
+ ```
42
+
43
+ ## 3. Compare
44
+
45
+ If the advisor independently lands on the same root cause you suspected, your
46
+ confidence should rise. If it points somewhere else — a race between the write
47
+ and the read-back, say — you just saved an hour chasing the wrong fix. Either
48
+ way, **you** verify the cited lines before changing code.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "crossagent"
7
+ dynamic = ["version"]
8
+ description = "Get a second opinion from another AI coding agent. Ask Claude, Codex, OpenCode, CommandCode, or Gemini — from inside the agent you already use."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Dat Nguyen" }]
14
+ keywords = ["claude", "codex", "opencode", "agent", "mcp", "coding-agent", "cli", "ai", "second-opinion"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development :: Libraries",
28
+ ]
29
+ dependencies = []
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/datj9/crossagent"
33
+ Repository = "https://github.com/datj9/crossagent"
34
+ Issues = "https://github.com/datj9/crossagent/issues"
35
+ Changelog = "https://github.com/datj9/crossagent/blob/main/CHANGELOG.md"
36
+
37
+ [project.scripts]
38
+ crossagent = "crossagent.cli:main"
39
+
40
+ [project.optional-dependencies]
41
+ dev = ["pytest>=7"]
42
+
43
+ [tool.hatch.version]
44
+ path = "src/crossagent/__init__.py"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/crossagent"]
48
+
49
+ [tool.hatch.build.targets.sdist]
50
+ include = ["src/crossagent", "skills", "examples", "README.md", "LICENSE"]
51
+
52
+ [tool.pytest.ini_options]
53
+ pythonpath = ["src"]
54
+ testpaths = ["tests"]
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: crossagent
3
+ description: Use when the user wants a second opinion from another AI coding agent before deciding — "crossagent", "ask Claude", "hoi y voi Claude", "ask Codex", "debate with Claude", "get a second opinion", "what would Claude/Codex say". Runs crossagent to ask a peer agent (Claude, Codex, OpenCode, CommandCode, Gemini) via its CLI with named, resumable Claude sessions, streamed progress, a packaged context prompt, and a final synthesis comparing both viewpoints.
4
+ version: 0.1.0
5
+ author: Dat Nguyen
6
+ ---
7
+
8
+ # crossagent
9
+
10
+ ## Overview
11
+
12
+ Treat a peer AI agent as a teammate for decision-quality work, not as an oracle.
13
+ Call the peer's CLI with enough context for it to evaluate the problem
14
+ independently, wait for the answer, then compare its view with your own before
15
+ recommending a path. The default advisor is **Claude** (`claude -p`); Codex,
16
+ OpenCode, CommandCode, and Gemini are also supported.
17
+
18
+ ## Decision Flow
19
+
20
+ 1. Confirm the second-opinion target: decision to make, current state, constraints, options considered, and what evidence would change the answer.
21
+ 2. Decide whether to ask a peer. Do so when the user asks directly, when architecture or prompt decisions benefit from an adversarial second view, or when the problem has credible tradeoffs. Skip for trivial commands, purely mechanical edits, or anything requiring secrets.
22
+ 3. Build a compact context package: absolute paths, relevant files, current findings, known unknowns, and safe read-only commands the advisor may run.
23
+ 4. Choose a stable session name before the first call. Reuse it for the same problem so later turns resume. Use a new name for a different decision; use `--fork-session` for an alternative branch of the same history.
24
+ 5. Call the advisor and keep the process alive until it exits. Do independent local work while it runs; if the next step depends on the answer, wait and monitor progress instead of interrupting.
25
+ 6. Compare outputs. Separate confirmed facts, claims needing verification, disagreements, unresolved questions, and the recommended synthesis. Do not outsource final judgment.
26
+ 7. If material conflict remains, run a follow-up in the same session with the conflict framed explicitly. Stop when the remaining uncertainty is named and decision-useful.
27
+
28
+ ## CLI Helper
29
+
30
+ Prefer the bundled helper for non-trivial second-opinion sessions:
31
+
32
+ ```bash
33
+ crossagent --agent claude \
34
+ --name "project-topic-decision" \
35
+ --cwd "$PWD" \
36
+ --prompt-file /tmp/crossagent.md
37
+ ```
38
+
39
+ The helper:
40
+
41
+ - Invokes the chosen advisor's CLI (default `claude -p`).
42
+ - Streams progress by default (Claude `stream-json`); other advisors capture text output.
43
+ - Stores `session_id` by `advisor:name` in `~/.config/crossagent/sessions.json` and auto-resumes when the same `--name` is reused.
44
+ - Has no default timeout and no default cost cap; it waits until the advisor exits.
45
+ - Writes the advisor's final answer to stdout and progress/metadata to stderr.
46
+
47
+ Switch advisor with `--agent codex|opencode|commandcode|gemini`. List what's available with `crossagent --list-advisors`. Add or fix an advisor in `~/.config/crossagent/advisors.json` (no code change needed). Use `--new-session` to ignore a stored session, `--resume ID_OR_SEARCH` to force a resume target, `--fork-session` to branch, and `--model` to raise the model only when the decision justifies it.
48
+
49
+ ## Prompting The Advisor
50
+
51
+ Read [references/second-opinion-protocol.md](references/second-opinion-protocol.md) when preparing a substantive prompt (architecture, prompt, eval, or debugging decisions).
52
+
53
+ Use XML-like sections. Ask the advisor to evaluate, inspect, compare, or trace; avoid "think step by step". Give it permission to gather more context with read-only commands, and ask it to cite file paths, commands, or assumptions behind claims.
54
+
55
+ ## Operating Rules
56
+
57
+ - Keep secrets out of prompts, CLI args, logs, and copied context.
58
+ - Default to a routine model; raise `--model` only when the decision justifies it.
59
+ - No cost cap by default. Add one only when the user explicitly asks for a spend limit.
60
+ - For Claude: use `--safe-mode` when the advisor does not need repo config/skills/hooks; `--tools ""` for pure reasoning; allow read/search tools only when it must inspect files.
61
+ - Report both the advisor's answer and your own synthesis. Name where the two views agree, diverge, or leave uncertainty.
62
+ - Preserve long-running calls. Do not abandon a started second-opinion session unless the user cancels it or the process fails.
@@ -0,0 +1,138 @@
1
+ # Second-Opinion Protocol
2
+
3
+ Use this reference when a second opinion affects architecture, prompt behavior,
4
+ debugging strategy, eval interpretation, or another decision where the second
5
+ opinion must be evidence-backed.
6
+
7
+ ## Context Package
8
+
9
+ Provide enough context for the advisor to form an independent view:
10
+
11
+ - Target decision: the answer the two agents are trying to settle.
12
+ - Current state: what exists, what has been observed, and what already works.
13
+ - Constraints: time, scope, ownership boundaries, safety rules, explicit cost caps if any, and anti-scope.
14
+ - Evidence: file paths, command outputs, logs, test failures, screenshots, artifacts, or prior conclusions.
15
+ - Candidate options: the realistic options you see so far, including the boring/default option when relevant.
16
+ - Read-only collection plan: commands or files the advisor may inspect to verify claims.
17
+ - Output contract: the exact comparison format you need to synthesize a decision.
18
+
19
+ Keep the package compact. Do not paste large files when file paths and targeted read instructions are enough.
20
+
21
+ ## Session Naming
22
+
23
+ Use names that are stable across turns and narrow enough to avoid cross-topic contamination:
24
+
25
+ - `brandmind-slide-service-architecture`
26
+ - `atlas-kb-ingest-wakeup-boundary`
27
+ - `prompt-eval-rubric-format-drift`
28
+
29
+ Reuse a name when the same decision continues. Send a short delta summary when resuming:
30
+
31
+ ```text
32
+ We are resuming the same decision. New evidence since the last turn:
33
+ - ...
34
+ - ...
35
+ Please update or revise your prior recommendation.
36
+ ```
37
+
38
+ Start a new name when the user changes the problem, project, or decision criterion. Use `--fork-session` when exploring an alternative line while preserving prior context.
39
+
40
+ ## Prompt Template
41
+
42
+ ```xml
43
+ <role>
44
+ You are a senior teammate in a joint decision. Evaluate the problem
45
+ independently, inspect additional context when useful, and return a
46
+ decision-useful critique. Do not edit files unless explicitly asked. Treat this
47
+ as a read-only review.
48
+ </role>
49
+
50
+ <decision>
51
+ [The decision to settle.]
52
+ </decision>
53
+
54
+ <current_state>
55
+ [What is already known. Include exact dates for time-sensitive state.]
56
+ </current_state>
57
+
58
+ <constraints>
59
+ [Scope, anti-scope, safety, ownership, deadlines, and any explicit cost cap.]
60
+ </constraints>
61
+
62
+ <evidence>
63
+ [File paths, logs, command outputs, prior findings. Mark unverified claims.]
64
+ </evidence>
65
+
66
+ <context_collection>
67
+ You may gather more context using read-only inspection such as pwd, ls, find,
68
+ rg, sed, awk, wc, git status, git diff, git log, git show, and opening relevant
69
+ files. Avoid commands that modify files, start services, install packages, call
70
+ paid external services, or expose secrets. If needed context is unavailable,
71
+ name the missing evidence and proceed with caveats.
72
+ </context_collection>
73
+
74
+ <questions>
75
+ 1. What is your independent diagnosis or architecture framing?
76
+ 2. Which option do you recommend, and why?
77
+ 3. What are the strongest risks, failure modes, or counterarguments?
78
+ 4. What evidence supports your claims? Cite files, commands, or assumptions.
79
+ 5. What remains unresolved, and what is the cheapest next validation step?
80
+ </questions>
81
+
82
+ <response_format>
83
+ Return concise markdown with these sections:
84
+ - Position
85
+ - Evidence
86
+ - Risks And Counterarguments
87
+ - Recommendation
88
+ - Unresolved Questions
89
+ </response_format>
90
+ ```
91
+
92
+ ## Running The Helper
93
+
94
+ Prepare the prompt in a temporary file, then call:
95
+
96
+ ```bash
97
+ crossagent --agent claude \
98
+ --name "stable-topic-name" \
99
+ --cwd "$PWD" \
100
+ --model sonnet \
101
+ --prompt-file /tmp/crossagent.md
102
+ ```
103
+
104
+ Useful variants:
105
+
106
+ ```bash
107
+ # Pure reasoning, no tools (Claude).
108
+ crossagent --name "stable-topic-name" --tools "" --prompt-file /tmp/crossagent.md
109
+
110
+ # Ask a different advisor.
111
+ crossagent --agent codex --name "stable-topic-name" --prompt-file /tmp/crossagent.md
112
+
113
+ # Resume a known session directly.
114
+ crossagent --resume "SESSION_ID_OR_SEARCH_TERM" --prompt-file /tmp/followup.md
115
+
116
+ # Branch the same second-opinion history.
117
+ crossagent --name "stable-topic-name" --fork-session --prompt-file /tmp/variant.md
118
+
119
+ # Add a spend cap only when the user explicitly asks (Claude via --raw-arg).
120
+ crossagent --name "stable-topic-name" --raw-arg --max-budget-usd --raw-arg 0.50 \
121
+ --prompt-file /tmp/capped.md
122
+ ```
123
+
124
+ ## Synthesizing The Answer
125
+
126
+ 1. Verify file/path/command claims when they matter and are cheap to check.
127
+ 2. Separate agreement, disagreement, and new information.
128
+ 3. Prefer the recommendation that best satisfies the goal, constraints, and evidence — not the louder or more recent answer.
129
+ 4. Run a follow-up only when the conflict changes the decision or a missing fact is cheap to inspect.
130
+ 5. Finalize with the chosen path, rationale, risks, and next action.
131
+
132
+ ## Failure Handling
133
+
134
+ - If a spend cap makes the advisor stop before the decision settles, remove or raise it deliberately and retry with a tighter context package.
135
+ - For Claude streaming, `stream-json` requires `--verbose` (the helper adds it automatically).
136
+ - If a variadic option like `--tools` precedes the prompt, the helper inserts `--` so the prompt is not swallowed.
137
+ - If a session was created without persistence, do not expect it to resume even if a `session_id` was returned.
138
+ - Experimental advisors (codex, opencode, commandcode, gemini) use best-effort default flags. If your install differs, correct them in `~/.config/crossagent/advisors.json`.
@@ -0,0 +1,3 @@
1
+ """crossagent — get a second opinion from another AI coding agent."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,166 @@
1
+ """Advisor registry: how to invoke each peer coding-agent CLI.
2
+
3
+ An *advisor* is a peer AI agent you ask for a second opinion (Claude, Codex,
4
+ OpenCode, CommandCode, Gemini, ...). Each entry is a small, declarative spec that
5
+ tells the runner how to build the command line, where the prompt goes, and how to
6
+ read the result back out. Built-ins ship sane defaults; users override or add their
7
+ own via ~/.config/crossagent/advisors.json without touching code.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from dataclasses import dataclass, field, replace
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ # Where the prompt string is placed on the argv.
18
+ # "dashdash" -> [..., "--", prompt] (claude: everything after -- is the prompt)
19
+ # "positional" -> [..., prompt] (codex exec / opencode run)
20
+ # "flag:-p" -> [..., "-p", prompt] (gemini: prompt is the value of -p)
21
+ PROMPT_DELIVERIES = frozenset({"dashdash", "positional"})
22
+
23
+ # How to read the advisor's answer back out of its stdout.
24
+ # "claude-stream" -> parse newline-delimited stream-json events, take the result event
25
+ # "text" -> capture raw stdout as the answer
26
+ RESULT_PARSERS = frozenset({"claude-stream", "text"})
27
+
28
+ USER_CONFIG = Path.home() / ".config" / "crossagent" / "advisors.json"
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class Advisor:
33
+ """Declarative recipe for invoking one peer-agent CLI."""
34
+
35
+ name: str
36
+ executable: str
37
+ base_args: tuple[str, ...] = ()
38
+ invoke_args: tuple[str, ...] = ()
39
+ prompt_delivery: str = "positional" # "dashdash" | "positional" | "flag:<flag>"
40
+ model_flag: str | None = None
41
+ stream_args: tuple[str, ...] = ()
42
+ json_args: tuple[str, ...] = ()
43
+ resume_flag: str | None = None
44
+ session_name_flag: str | None = None
45
+ fork_flag: str | None = None
46
+ result_parser: str = "text"
47
+ experimental: bool = False
48
+ notes: str = ""
49
+
50
+ @property
51
+ def supports_sessions(self) -> bool:
52
+ return self.resume_flag is not None or self.session_name_flag is not None
53
+
54
+ @property
55
+ def supports_stream(self) -> bool:
56
+ return self.result_parser == "claude-stream"
57
+
58
+
59
+ # --- Built-in advisors -------------------------------------------------------
60
+ # claude is the reference implementation: fully featured, verified against the
61
+ # upstream `claude -p` behaviour. The rest are pragmatic best-effort defaults —
62
+ # marked experimental — that users can correct via the JSON override file.
63
+
64
+ _BUILTINS: dict[str, Advisor] = {
65
+ "claude": Advisor(
66
+ name="claude",
67
+ executable="claude",
68
+ invoke_args=("-p",),
69
+ prompt_delivery="dashdash",
70
+ model_flag="--model",
71
+ stream_args=("--verbose", "--output-format", "stream-json"),
72
+ json_args=("--output-format", "json"),
73
+ resume_flag="--resume",
74
+ session_name_flag="--name",
75
+ fork_flag="--fork-session",
76
+ result_parser="claude-stream",
77
+ ),
78
+ "codex": Advisor(
79
+ name="codex",
80
+ executable="codex",
81
+ base_args=("exec",),
82
+ prompt_delivery="positional",
83
+ model_flag="--model",
84
+ result_parser="text",
85
+ experimental=True,
86
+ notes="Uses `codex exec <prompt>` (non-interactive). Resume not wired by default.",
87
+ ),
88
+ "opencode": Advisor(
89
+ name="opencode",
90
+ executable="opencode",
91
+ base_args=("run",),
92
+ prompt_delivery="positional",
93
+ model_flag="--model",
94
+ result_parser="text",
95
+ experimental=True,
96
+ notes="Uses `opencode run <prompt>` (headless).",
97
+ ),
98
+ "commandcode": Advisor(
99
+ name="commandcode",
100
+ executable="commandcode",
101
+ invoke_args=("-p",),
102
+ prompt_delivery="positional",
103
+ model_flag="--model",
104
+ result_parser="text",
105
+ experimental=True,
106
+ notes="Uses `commandcode -p <prompt>` (non-interactive). Resume not wired by default.",
107
+ ),
108
+ "gemini": Advisor(
109
+ name="gemini",
110
+ executable="gemini",
111
+ prompt_delivery="flag:-p",
112
+ model_flag="--model",
113
+ result_parser="text",
114
+ experimental=True,
115
+ notes="Uses `gemini -p <prompt>` (non-interactive).",
116
+ ),
117
+ }
118
+
119
+ # Friendly aliases callers may type.
120
+ _ALIASES = {"cmd": "commandcode", "cc": "claude", "oc": "opencode"}
121
+
122
+
123
+ def _coerce(name: str, raw: dict[str, Any]) -> Advisor:
124
+ """Build an Advisor from a user-config dict, layering onto a built-in if one exists."""
125
+ base = _BUILTINS.get(name, Advisor(name=name, executable=raw.get("executable", name)))
126
+ tuple_fields = {"base_args", "invoke_args", "stream_args", "json_args"}
127
+ overrides: dict[str, Any] = {}
128
+ for key, value in raw.items():
129
+ if key in tuple_fields and isinstance(value, list):
130
+ overrides[key] = tuple(value)
131
+ elif key != "name":
132
+ overrides[key] = value
133
+ return replace(base, name=name, **overrides)
134
+
135
+
136
+ def _load_user_config(path: Path) -> dict[str, Advisor]:
137
+ if not path.exists():
138
+ return {}
139
+ try:
140
+ data = json.loads(path.read_text(encoding="utf-8"))
141
+ except (json.JSONDecodeError, OSError):
142
+ return {}
143
+ entries = data.get("advisors", data) if isinstance(data, dict) else {}
144
+ result: dict[str, Advisor] = {}
145
+ if isinstance(entries, dict):
146
+ for name, raw in entries.items():
147
+ if isinstance(raw, dict):
148
+ result[name] = _coerce(name, raw)
149
+ return result
150
+
151
+
152
+ def available(config_path: Path | None = None) -> dict[str, Advisor]:
153
+ """Return the merged advisor registry: built-ins overridden by user config."""
154
+ merged = dict(_BUILTINS)
155
+ merged.update(_load_user_config(config_path or USER_CONFIG))
156
+ return merged
157
+
158
+
159
+ def resolve(name: str, config_path: Path | None = None) -> Advisor:
160
+ """Look up an advisor by name or alias. Raises KeyError with a helpful message."""
161
+ canonical = _ALIASES.get(name.strip().lower(), name.strip().lower())
162
+ registry = available(config_path)
163
+ if canonical not in registry:
164
+ known = ", ".join(sorted(registry))
165
+ raise KeyError(f"Unknown advisor '{name}'. Known advisors: {known}. Add your own in {USER_CONFIG}.")
166
+ return registry[canonical]
@@ -0,0 +1,240 @@
1
+ """Run a named, resumable second-opinion session with a peer coding-agent CLI.
2
+
3
+ `crossagent` lets one AI coding agent get a second opinion from another. It builds
4
+ the right command for the chosen advisor (Claude by default), keeps the process
5
+ alive until the advisor finishes, streams progress to stderr, prints the final
6
+ answer to stdout, and remembers the session so a follow-up can resume it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import shlex
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from . import advisors as advisors_mod
21
+ from . import registry as reg
22
+ from .advisors import Advisor
23
+
24
+
25
+ def read_prompt(args: argparse.Namespace) -> str:
26
+ if args.prompt_file:
27
+ return Path(args.prompt_file).read_text(encoding="utf-8")
28
+ if args.prompt:
29
+ return args.prompt
30
+ if not sys.stdin.isatty():
31
+ return sys.stdin.read()
32
+ raise SystemExit("Provide --prompt-file, --prompt, or pipe the prompt on stdin.")
33
+
34
+
35
+ def _append_prompt(cmd: list[str], advisor: Advisor, prompt: str) -> None:
36
+ delivery = advisor.prompt_delivery
37
+ if delivery == "dashdash":
38
+ cmd.extend(["--", prompt])
39
+ elif delivery.startswith("flag:"):
40
+ cmd.extend([delivery.split(":", 1)[1], prompt])
41
+ else: # "positional"
42
+ cmd.append(prompt)
43
+
44
+
45
+ def _redacted_command(cmd: list[str]) -> str:
46
+ """Format an advisor command without exposing its final prompt argument."""
47
+ if not cmd:
48
+ return "<prompt>"
49
+ return shlex.join([*cmd[:-1], "<prompt>"])
50
+
51
+
52
+ def build_command(advisor: Advisor, args: argparse.Namespace, registry: dict[str, Any]) -> tuple[list[str], str]:
53
+ cmd = [advisor.executable, *advisor.base_args, *advisor.invoke_args]
54
+
55
+ if args.model and advisor.model_flag:
56
+ cmd.extend([advisor.model_flag, args.model])
57
+
58
+ if advisor.supports_stream:
59
+ cmd.extend(args.stream and advisor.stream_args or advisor.json_args)
60
+ if args.stream and args.partial:
61
+ cmd.append("--include-partial-messages")
62
+
63
+ if args.safe_mode:
64
+ cmd.append("--safe-mode")
65
+ if args.permission_mode:
66
+ cmd.extend(["--permission-mode", args.permission_mode])
67
+ if args.tools is not None:
68
+ cmd.extend(["--tools", args.tools])
69
+ for allowed in args.allowed_tools:
70
+ cmd.extend(["--allowedTools", allowed])
71
+ if args.system_prompt:
72
+ cmd.extend(["--system-prompt", args.system_prompt])
73
+ for extra in args.raw_arg:
74
+ cmd.append(extra)
75
+
76
+ key = reg.session_key(advisor.name, args.name)
77
+ stored_id = reg.stored_session_id(registry, key)
78
+
79
+ if advisor.supports_sessions:
80
+ if args.resume and advisor.resume_flag:
81
+ cmd.extend([advisor.resume_flag, args.resume])
82
+ elif stored_id and not args.new_session and advisor.resume_flag:
83
+ cmd.extend([advisor.resume_flag, stored_id])
84
+ elif args.name and advisor.session_name_flag:
85
+ cmd.extend([advisor.session_name_flag, args.name])
86
+ if args.fork_session and advisor.fork_flag:
87
+ cmd.append(advisor.fork_flag)
88
+
89
+ _append_prompt(cmd, advisor, prompt=args._prompt)
90
+ return cmd, key
91
+
92
+
93
+ # --- Claude stream-json handling --------------------------------------------
94
+
95
+ def summarize_event(event: dict[str, Any]) -> None:
96
+ kind = event.get("type")
97
+ if kind == "system" and event.get("subtype") == "init":
98
+ print(f"[crossagent] init session={event.get('session_id')} model={event.get('model')} "
99
+ f"cwd={event.get('cwd')}", file=sys.stderr)
100
+ elif kind == "assistant":
101
+ message = event.get("message", {})
102
+ blocks = message.get("content", []) if isinstance(message, dict) else []
103
+ text = "".join(b.get("text", "") for b in blocks
104
+ if isinstance(b, dict) and b.get("type") == "text")
105
+ if text:
106
+ print(f"[crossagent] assistant: {text.replace(chr(10), ' ')[:240]}", file=sys.stderr)
107
+ elif kind == "result":
108
+ print(f"[crossagent] result subtype={event.get('subtype')} session={event.get('session_id')} "
109
+ f"cost={event.get('total_cost_usd')}", file=sys.stderr)
110
+ elif kind == "rate_limit_event":
111
+ info = event.get("rate_limit_info", {})
112
+ print(f"[crossagent] rate_limit status={info.get('status')} resetsAt={info.get('resetsAt')}",
113
+ file=sys.stderr)
114
+
115
+
116
+ def _run_stream(cmd: list[str], cwd: str | None) -> tuple[int, dict[str, Any] | None]:
117
+ proc = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
118
+ text=True, bufsize=1)
119
+ assert proc.stdout is not None and proc.stderr is not None
120
+ final: dict[str, Any] | None = None
121
+ for line in proc.stdout:
122
+ stripped = line.strip()
123
+ if not stripped:
124
+ continue
125
+ try:
126
+ event = json.loads(stripped)
127
+ except json.JSONDecodeError:
128
+ print(stripped, file=sys.stderr)
129
+ continue
130
+ summarize_event(event)
131
+ if event.get("type") == "result":
132
+ final = event
133
+ err = proc.stderr.read()
134
+ if err:
135
+ print(err, file=sys.stderr, end="")
136
+ return proc.wait(), final
137
+
138
+
139
+ def _run_text(cmd: list[str], cwd: str | None) -> tuple[int, str]:
140
+ completed = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True, check=False)
141
+ if completed.stderr:
142
+ print(completed.stderr, file=sys.stderr, end="")
143
+ return completed.returncode, completed.stdout
144
+
145
+
146
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
147
+ parser = argparse.ArgumentParser(prog="crossagent", description=__doc__)
148
+ parser.add_argument("--agent", "--advisor", dest="agent", default="claude",
149
+ help="Peer agent to ask: claude, codex, opencode, commandcode, gemini, or a custom advisor. Default: claude.")
150
+ parser.add_argument("--name", help="Stable session name. Reused names auto-resume the stored session.")
151
+ parser.add_argument("--resume", help="Force a --resume target: session id or search term.")
152
+ parser.add_argument("--new-session", action="store_true", help="Ignore any stored session for --name and start fresh.")
153
+ parser.add_argument("--fork-session", action="store_true", help="Fork a new session from the existing conversation.")
154
+ parser.add_argument("--prompt-file", help="Path to the second-opinion prompt.")
155
+ parser.add_argument("--prompt", help="Prompt text. Prefer --prompt-file for long prompts.")
156
+ parser.add_argument("--cwd", help="Working directory for the advisor. Defaults to the current directory.")
157
+ parser.add_argument("--model", default="", help="Advisor model or alias (advisor-specific). Empty = advisor default.")
158
+ parser.add_argument("--safe-mode", action="store_true", help="Claude: run with --safe-mode (skip repo config/skills/hooks).")
159
+ parser.add_argument("--no-stream", dest="stream", action="store_false", help="Claude: use single JSON output instead of streaming.")
160
+ parser.add_argument("--partial", action="store_true", help="Claude: include partial stream messages.")
161
+ parser.add_argument("--tools", help='Claude: pass --tools, e.g. "" for none or "Read,Bash".')
162
+ parser.add_argument("--allowed-tools", action="append", default=[], help="Claude: repeatable --allowedTools value.")
163
+ parser.add_argument("--permission-mode", help="Claude: --permission-mode value.")
164
+ parser.add_argument("--system-prompt", help="Claude: --system-prompt value.")
165
+ parser.add_argument("--raw-arg", action="append", default=[], help="Repeatable raw argument passed straight to the advisor CLI.")
166
+ parser.add_argument("--registry", default=str(reg.DEFAULT_REGISTRY), help="Session registry path.")
167
+ parser.add_argument("--list-advisors", action="store_true", help="Print known advisors and exit.")
168
+ parser.set_defaults(stream=True)
169
+ return parser.parse_args(argv)
170
+
171
+
172
+ def _print_advisors() -> int:
173
+ for name, adv in sorted(advisors_mod.available().items()):
174
+ tag = " (experimental)" if adv.experimental else ""
175
+ print(f"{name:14} -> {adv.executable}{tag}")
176
+ if adv.notes:
177
+ print(f"{'':14} {adv.notes}")
178
+ return 0
179
+
180
+
181
+ def main(argv: list[str] | None = None) -> int:
182
+ args = parse_args(argv)
183
+ if args.list_advisors:
184
+ return _print_advisors()
185
+
186
+ try:
187
+ advisor = advisors_mod.resolve(args.agent)
188
+ except KeyError as exc:
189
+ print(f"[crossagent] {exc}", file=sys.stderr)
190
+ return 2
191
+
192
+ if advisor.experimental:
193
+ print(f"[crossagent] advisor '{advisor.name}' is experimental — verify flags for your install.",
194
+ file=sys.stderr)
195
+
196
+ args._prompt = read_prompt(args)
197
+ registry_path = Path(args.registry).expanduser()
198
+ registry = reg.load(registry_path)
199
+ cmd, key = build_command(advisor, args, registry)
200
+
201
+ print(f"[crossagent] running: {_redacted_command(cmd)}", file=sys.stderr)
202
+
203
+ try:
204
+ return _dispatch(advisor, args, cmd, key, registry, registry_path)
205
+ except FileNotFoundError:
206
+ print(f"[crossagent] advisor CLI not found on PATH: '{advisor.executable}'. "
207
+ f"Install it, or point '{advisor.name}' at the right executable in "
208
+ f"{advisors_mod.USER_CONFIG}.", file=sys.stderr)
209
+ return 127
210
+
211
+
212
+ def _dispatch(advisor: Advisor, args: argparse.Namespace, cmd: list[str], key: str,
213
+ registry: dict[str, Any], registry_path: Path) -> int:
214
+ if advisor.supports_stream and args.stream:
215
+ code, final = _run_stream(cmd, args.cwd)
216
+ if final:
217
+ if final.get("is_error"):
218
+ errors = final.get("errors") or final.get("api_error_status") or "unknown error"
219
+ print(f"[crossagent] {advisor.name} returned error: {errors}", file=sys.stderr)
220
+ result = final.get("result")
221
+ structured = final.get("structured_output")
222
+ if result:
223
+ print(result)
224
+ elif structured is not None:
225
+ print(json.dumps(structured, indent=2, sort_keys=True))
226
+ session_id = final.get("session_id")
227
+ if key and session_id:
228
+ reg.record(registry_path, registry, key, session_id=session_id, name=args.name,
229
+ cwd=args.cwd or os.getcwd(), advisor=advisor.name, model=args.model)
230
+ print(f"[crossagent] saved session name={key} id={session_id}", file=sys.stderr)
231
+ return code
232
+
233
+ code, out = _run_text(cmd, args.cwd)
234
+ if out.strip():
235
+ print(out, end="" if out.endswith("\n") else "\n")
236
+ return code
237
+
238
+
239
+ if __name__ == "__main__":
240
+ raise SystemExit(main())
@@ -0,0 +1,72 @@
1
+ """Named, resumable second-opinion sessions.
2
+
3
+ Stores each session's underlying session id keyed by ``advisor:slug`` so a
4
+ later turn on the same decision can resume the same conversation. Only advisors
5
+ that emit a session id (currently Claude) populate this; the file is otherwise a
6
+ harmless no-op.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ import sys
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ DEFAULT_REGISTRY = Path.home() / ".config" / "crossagent" / "sessions.json"
19
+
20
+
21
+ def slugify(value: str) -> str:
22
+ slug = re.sub(r"[^a-zA-Z0-9._-]+", "-", value.strip().lower())
23
+ slug = re.sub(r"-{2,}", "-", slug).strip("-")
24
+ return slug or "second-opinion"
25
+
26
+
27
+ def session_key(advisor: str, name: str | None) -> str:
28
+ return f"{advisor}:{slugify(name)}" if name else ""
29
+
30
+
31
+ def load(path: Path) -> dict[str, Any]:
32
+ if not path.exists():
33
+ return {"sessions": {}}
34
+ try:
35
+ data = json.loads(path.read_text(encoding="utf-8"))
36
+ except json.JSONDecodeError:
37
+ backup = path.with_suffix(path.suffix + ".corrupt")
38
+ path.replace(backup)
39
+ print(f"[crossagent] Registry was invalid JSON; moved to {backup}", file=sys.stderr)
40
+ return {"sessions": {}}
41
+ if not isinstance(data, dict):
42
+ return {"sessions": {}}
43
+ if not isinstance(data.get("sessions"), dict):
44
+ data["sessions"] = {}
45
+ return data
46
+
47
+
48
+ def save(path: Path, data: dict[str, Any]) -> None:
49
+ path.parent.mkdir(parents=True, exist_ok=True)
50
+ path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
51
+
52
+
53
+ def stored_session_id(registry: dict[str, Any], key: str) -> str | None:
54
+ entry = registry.get("sessions", {}).get(key) if key else None
55
+ return entry.get("session_id") if isinstance(entry, dict) else None
56
+
57
+
58
+ def record(path: Path, registry: dict[str, Any], key: str, *, session_id: str, name: str | None,
59
+ cwd: str, advisor: str, model: str) -> dict[str, Any]:
60
+ """Return a NEW registry dict with the session recorded, and persist it."""
61
+ sessions = dict(registry.get("sessions", {}))
62
+ sessions[key] = {
63
+ "session_id": session_id,
64
+ "name": name,
65
+ "advisor": advisor,
66
+ "cwd": str(Path(cwd).resolve()),
67
+ "model": model,
68
+ "updated_at": datetime.now(timezone.utc).isoformat(),
69
+ }
70
+ updated = {**registry, "sessions": sessions}
71
+ save(path, updated)
72
+ return updated