tandem-cli 0.2.0__tar.gz → 0.3.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.
Files changed (118) hide show
  1. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/.claude/settings.local.json +6 -1
  2. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/.gitignore +3 -0
  3. tandem_cli-0.3.0/PKG-INFO +183 -0
  4. tandem_cli-0.3.0/README.md +157 -0
  5. tandem_cli-0.3.0/docs/configuration.md +77 -0
  6. tandem_cli-0.3.0/docs/development.md +29 -0
  7. tandem_cli-0.3.0/docs/how-it-works.md +106 -0
  8. tandem_cli-0.3.0/docs/plans/2026-08-09-meta-harness-frame.md +1860 -0
  9. tandem_cli-0.3.0/docs/plans/2026-08-09-readme-restructure.md +367 -0
  10. tandem_cli-0.3.0/docs/plans/2026-08-10-idle-flip-status-probe.md +595 -0
  11. tandem_cli-0.3.0/docs/plans/2026-08-10-retire-interstitial-prompt.md +857 -0
  12. tandem_cli-0.3.0/docs/plans/2026-08-13-pr1-nharness-core.md +1813 -0
  13. tandem_cli-0.3.0/docs/plans/2026-08-13-pr2-opencode-adapter.md +1907 -0
  14. tandem_cli-0.3.0/docs/specs/2026-08-09-meta-harness-frame-design.md +186 -0
  15. tandem_cli-0.3.0/docs/specs/2026-08-09-model-pin-recovery-design.md +140 -0
  16. tandem_cli-0.3.0/docs/specs/2026-08-09-readme-restructure-design.md +117 -0
  17. tandem_cli-0.3.0/docs/specs/2026-08-10-remove-interstitial-prompt-design.md +102 -0
  18. tandem_cli-0.3.0/docs/specs/2026-08-13-opencode-harness-design.md +442 -0
  19. tandem_cli-0.3.0/docs/subagents.md +132 -0
  20. tandem_cli-0.3.0/docs/superpowers/plans/2026-08-11-process-warmup-live-validation.md +97 -0
  21. tandem_cli-0.3.0/docs/superpowers/plans/2026-08-11-process-warmup.md +1706 -0
  22. tandem_cli-0.3.0/docs/superpowers/plans/2026-08-12-process-warmup-pipelined.md +286 -0
  23. tandem_cli-0.3.0/docs/superpowers/specs/2026-08-10-idle-flip-status-probe.md +101 -0
  24. tandem_cli-0.3.0/docs/superpowers/specs/2026-08-11-process-warmup-design.md +221 -0
  25. tandem_cli-0.3.0/docs/superpowers/specs/2026-08-12-process-warmup-pipelined-design.md +125 -0
  26. tandem_cli-0.3.0/docs/why.md +69 -0
  27. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/plugin/.claude-plugin/plugin.json +1 -1
  28. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/plugin/README.md +18 -11
  29. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/plugin/agents/codex-worker.md +8 -0
  30. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/plugin/agents/gpt.md +8 -0
  31. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/pyproject.toml +1 -1
  32. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/cli.py +171 -114
  33. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/compat.py +7 -2
  34. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/config.py +66 -1
  35. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/constants.py +1 -0
  36. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/converter.py +5 -8
  37. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/doctor.py +50 -129
  38. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/events.py +30 -15
  39. tandem_cli-0.3.0/src/tandem/flip.py +325 -0
  40. tandem_cli-0.3.0/src/tandem/frame.py +278 -0
  41. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/harness/__init__.py +0 -4
  42. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/harness/base.py +55 -0
  43. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/harness/claude_code.py +111 -11
  44. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/harness/codex.py +41 -0
  45. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/hookroute.py +36 -0
  46. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/ops.py +121 -52
  47. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/paths.py +8 -0
  48. tandem_cli-0.3.0/src/tandem/pinstash.py +97 -0
  49. tandem_cli-0.3.0/src/tandem/ptyrun.py +433 -0
  50. tandem_cli-0.3.0/src/tandem/runner.py +828 -0
  51. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/state.py +81 -59
  52. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/sync.py +16 -25
  53. tandem_cli-0.3.0/src/tandem/warm.py +171 -0
  54. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/conftest.py +119 -9
  55. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_cli.py +44 -27
  56. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_config.py +73 -1
  57. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_converter.py +11 -5
  58. tandem_cli-0.3.0/tests/test_flip.py +586 -0
  59. tandem_cli-0.3.0/tests/test_frame.py +559 -0
  60. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_hookroute.py +65 -0
  61. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_memory_doctor.py +37 -1
  62. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_ops.py +94 -12
  63. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_parse.py +41 -2
  64. tandem_cli-0.3.0/tests/test_participants.py +109 -0
  65. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_paths_compat.py +16 -0
  66. tandem_cli-0.3.0/tests/test_pinstash.py +94 -0
  67. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_plugin.py +8 -1
  68. tandem_cli-0.3.0/tests/test_ptyrun.py +560 -0
  69. tandem_cli-0.3.0/tests/test_runner.py +1704 -0
  70. tandem_cli-0.3.0/tests/test_state.py +185 -0
  71. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_sub.py +120 -28
  72. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_sync.py +3 -3
  73. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_tail.py +10 -6
  74. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_toolmap.py +16 -10
  75. tandem_cli-0.3.0/tests/test_warm.py +223 -0
  76. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/uv.lock +1 -1
  77. tandem_cli-0.2.0/.superpowers/sdd/.gitignore +0 -1
  78. tandem_cli-0.2.0/PKG-INFO +0 -389
  79. tandem_cli-0.2.0/README.md +0 -363
  80. tandem_cli-0.2.0/src/tandem/ptyrun.py +0 -99
  81. tandem_cli-0.2.0/src/tandem/runner.py +0 -261
  82. tandem_cli-0.2.0/src/tandem/shell.py +0 -207
  83. tandem_cli-0.2.0/tests/test_ptyrun.py +0 -14
  84. tandem_cli-0.2.0/tests/test_runner.py +0 -104
  85. tandem_cli-0.2.0/tests/test_shell.py +0 -402
  86. tandem_cli-0.2.0/tests/test_state.py +0 -147
  87. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/.claude-plugin/marketplace.json +0 -0
  88. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/.github/workflows/ci.yml +0 -0
  89. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/LICENSE +0 -0
  90. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/formats.md +0 -0
  91. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/gpt-subagent.gif +0 -0
  92. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/plans/2026-07-29-native-tool-call-translation.md +0 -0
  93. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/plans/2026-07-31-codex-subagents.md +0 -0
  94. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/plans/2026-08-01-plugin-marketplace-install.md +0 -0
  95. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/plans/2026-08-01-sandbox-consent-and-manual-routing.md +0 -0
  96. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/plans/2026-08-02-plugin-auto-install.md +0 -0
  97. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/plans/2026-08-03-manual-default-model-passthrough.md +0 -0
  98. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/plans/2026-08-06-harness-startup-args.md +0 -0
  99. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/specs/2026-07-29-native-tool-call-translation-design.md +0 -0
  100. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/specs/2026-07-31-codex-subagents-design.md +0 -0
  101. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/specs/2026-08-02-plugin-auto-install-design.md +0 -0
  102. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/specs/2026-08-03-manual-default-model-passthrough-design.md +0 -0
  103. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/specs/2026-08-06-harness-startup-args-design.md +0 -0
  104. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/superpowers/plans/2026-07-29-tandem-shell.md +0 -0
  105. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/docs/superpowers/specs/2026-07-29-tandem-shell-design.md +0 -0
  106. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/plugin/hooks/hooks.json +0 -0
  107. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/__init__.py +0 -0
  108. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/memory_sync.py +0 -0
  109. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/modelcat.py +0 -0
  110. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/plugin_setup.py +0 -0
  111. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/summarize.py +0 -0
  112. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/tailer.py +0 -0
  113. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/toolmap.py +0 -0
  114. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/src/tandem/util.py +0 -0
  115. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/golden/claude-probe.jsonl +0 -0
  116. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/golden/codex-probe.jsonl +0 -0
  117. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_modelcat.py +0 -0
  118. {tandem_cli-0.2.0 → tandem_cli-0.3.0}/tests/test_plugin_setup.py +0 -0
@@ -7,7 +7,12 @@
7
7
  "Bash(curl -sL --max-time 30 -o bl_books_full.csv \"https://raw.githubusercontent.com/realpython/python-data-cleaning/master/Datasets/BL-Flickr-Images-Book.csv\")",
8
8
  "Bash(curl -sL --max-time 30 -o chipotle.tsv \"https://raw.githubusercontent.com/justmarkham/DAT8/master/data/chipotle.tsv\")",
9
9
  "Bash(git reset *)",
10
- "Bash(/Users/bhavya/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/skills/subagent-driven-development/scripts/task-brief *)"
10
+ "Bash(/Users/bhavya/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/skills/subagent-driven-development/scripts/task-brief *)",
11
+ "WebFetch(domain:subreddittraffic.live)",
12
+ "Bash(grep -n *)",
13
+ "Read(//Users/bhavya/.tandem/**)",
14
+ "Read(//Users/bhavya/.config/**)",
15
+ "Bash(uv run *)"
11
16
  ]
12
17
  }
13
18
  }
@@ -5,3 +5,6 @@ __pycache__/
5
5
  dist/
6
6
  build/
7
7
  .pytest_cache/
8
+
9
+ # superpowers brainstorm mockups
10
+ .superpowers/
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.5
2
+ Name: tandem-cli
3
+ Version: 0.3.0
4
+ Summary: Meta-harness that runs Claude Code and Codex CLI with live trace sync.
5
+ Project-URL: Homepage, https://github.com/Bhavya6187/tandem
6
+ Project-URL: Repository, https://github.com/Bhavya6187/tandem
7
+ Project-URL: Issues, https://github.com/Bhavya6187/tandem/issues
8
+ Author-email: Bhavya Agarwal <bhavya.6187@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,claude-code,cli,codex,session-sync
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: POSIX
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: click>=8.1
22
+ Requires-Dist: pexpect>=4.9
23
+ Requires-Dist: pydantic>=2.7
24
+ Requires-Dist: watchdog>=4.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ <div align="center">
28
+
29
+ # 🤝 tandem
30
+
31
+ **One coding session. Two AI agents. Zero lost context.**
32
+
33
+ Run [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and
34
+ [OpenAI Codex CLI](https://github.com/openai/codex) as a single paired
35
+ session — each model in its own native harness. Work in either one,
36
+ flip to the other with **Ctrl-]**, and pick up exactly where you left
37
+ off. Only one model runs per turn; the other stays in sync through pure
38
+ local file translation.
39
+
40
+ [![CI](https://github.com/Bhavya6187/tandem/actions/workflows/ci.yml/badge.svg)](https://github.com/Bhavya6187/tandem/actions/workflows/ci.yml)
41
+ [![PyPI](https://img.shields.io/pypi/v/tandem-cli)](https://pypi.org/project/tandem-cli/)
42
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://github.com/Bhavya6187/tandem/blob/main/pyproject.toml)
43
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/Bhavya6187/tandem/blob/main/LICENSE)
44
+
45
+ ```bash
46
+ uv tool install tandem-cli
47
+ ```
48
+
49
+ ![tandem demo — one session moving between Claude Code and Codex](https://raw.githubusercontent.com/Bhavya6187/tandem/main/docs/demo.gif)
50
+
51
+ </div>
52
+
53
+ ---
54
+
55
+ ## Quick start
56
+
57
+ You'll need Python 3.11+ and the `claude` and `codex` CLIs on your PATH.
58
+
59
+ ```bash
60
+ uv tool install tandem-cli # or: pip install tandem-cli
61
+ cd your-project
62
+ tandem # fresh paired session; drops you into claude
63
+ ```
64
+
65
+ Work normally — that's the real Claude Code TUI. When you want the other
66
+ model, press **Ctrl-]**: tandem closes out the harness at the turn
67
+ boundary and reopens the same conversation in Codex a couple of seconds
68
+ later. Press it again to come back. The bottom row tracks who you're
69
+ facing:
70
+
71
+ ```
72
+ claude ● │ codex ○ ^] flips
73
+ ```
74
+
75
+ Pressed mid-turn, the flip arms and fires the moment the model finishes
76
+ (the bar says so; press again to cancel).
77
+
78
+ Exit the harness the usual way and you're back at your shell, with the
79
+ session saved and a hint for picking it up again:
80
+
81
+ ```
82
+ to continue this session: tandem resume a1b2c3d4e5f6
83
+ ```
84
+
85
+ Come back anytime:
86
+
87
+ ```bash
88
+ tandem resume # most recent session in this directory
89
+ tandem resume a1b2c3d4e5f6 # a specific one (id from the exit hint)
90
+ ```
91
+
92
+ ## Why tandem?
93
+
94
+ ### 🖥️ One CLI, two harnesses, zero ceremony.
95
+
96
+ `tandem` is the terminal you live in. It fronts the real Claude Code or
97
+ Codex TUI — pixel-for-pixel native — and **Ctrl-]** flips to the other
98
+ one in a couple of seconds, same conversation, same files, same history.
99
+ A one-line tab bar on the bottom row shows which model you're facing;
100
+ everything above it is the untouched native UI.
101
+
102
+ ### 🆕 0.2 — GPT subagents inside Claude Code
103
+
104
+ Type "get the code reviewed by gpt" and tandem's plugin hands the task
105
+ to a local codex worker; the verdict lands back in your Claude session,
106
+ and your Claude quota stays on the main thread. Pin any model your
107
+ codex account offers ("ask sol to review it") or set a cheap default
108
+ once. Setup and routing:
109
+ [GPT subagents guide](https://github.com/Bhavya6187/tandem/blob/main/docs/subagents.md).
110
+
111
+ ![gpt subagent demo — ask for a GPT review in Claude Code, tandem dispatches a codex worker, the verdict comes back, fixes get committed](https://raw.githubusercontent.com/Bhavya6187/tandem/main/docs/gpt-subagent.gif)
112
+
113
+ ### ⏳ Hit a usage limit? Just keep going.
114
+
115
+ Claude runs out of its usage window mid-refactor? Press **Ctrl-]** and
116
+ Codex continues the **same conversation** a second later — same files,
117
+ same history, same plan. Two subscriptions become one long runway.
118
+
119
+ ### 🌩️ Immune to outages.
120
+
121
+ An Anthropic or OpenAI outage doesn't stop your work: **Ctrl-]**, keep
122
+ going in the other harness, and flip back whenever it clears.
123
+
124
+ Five more reasons — subscriptions not API bills, two model families on
125
+ one problem, native harnesses, instant switching, privacy:
126
+ [Why tandem?](https://github.com/Bhavya6187/tandem/blob/main/docs/why.md)
127
+
128
+ ## How it works
129
+
130
+ - Only one model runs per turn — the other side is never invoked to
131
+ "catch up".
132
+ - As you work, tandem translates the growing transcript into the other
133
+ CLI's native session format — pure local file I/O, no model calls —
134
+ so the other side is always resume-ready.
135
+ - Each CLI is the real thing running on a PTY: your keybindings, slash
136
+ commands, and MCP servers all work exactly as they do today — tandem
137
+ reserves exactly one key (Ctrl-]) and one terminal row (the tab bar).
138
+ - A flip waits for the turn boundary, exits the fronted CLI gracefully,
139
+ lets the sync settle, and resumes the other side — no stop in
140
+ between.
141
+ - Everything stays local: the CLIs' own session files plus a small
142
+ SQLite database in `~/.tandem`. No cloud sync, no telemetry.
143
+ - Uninstall tandem tomorrow and both sessions still resume natively
144
+ with `claude --resume` and `codex resume`.
145
+
146
+ Full mechanics — sync engine, crash safety, compatibility ranges:
147
+ [How tandem works](https://github.com/Bhavya6187/tandem/blob/main/docs/how-it-works.md).
148
+
149
+ ## Command cheat sheet
150
+
151
+ | Command | What it does |
152
+ | --- | --- |
153
+ | `tandem` | Start a fresh paired session (Claude active; `--active codex` to flip) |
154
+ | `Ctrl-]` | Flip to the other harness from inside a running session (rebindable in `[frame]`) |
155
+ | `tandem resume [id]` | Continue the most recent (or a specific) session |
156
+ | `tandem run --on codex "…"` | One-off prompt to the *other* agent, with full context |
157
+ | `tandem sub "…"` | Run one delegated task on a codex model (what GPT subagents use under the hood) |
158
+ | `tandem status` | Show pairing, roles, and sync position |
159
+ | `tandem plugin install` | Install the Claude Code plugin (also offered on first `tandem` launch) |
160
+
161
+ Three maintenance commands round it out: `tandem doctor` (health
162
+ check), `tandem sync` (manual catch-up translation), and `tandem
163
+ sync-mcp` (share MCP server configs between the tools).
164
+
165
+ ## Docs
166
+
167
+ - [GPT subagents](https://github.com/Bhavya6187/tandem/blob/main/docs/subagents.md) —
168
+ plugin install, worker model, routing modes, sandbox & trust boundary
169
+ - [Why tandem?](https://github.com/Bhavya6187/tandem/blob/main/docs/why.md) —
170
+ the full pitch, all eight reasons
171
+ - [Configuration](https://github.com/Bhavya6187/tandem/blob/main/docs/configuration.md) —
172
+ the optional `~/.tandem/config.toml`: subagent workers, per-harness
173
+ startup args, the flip key and tab bar
174
+ - [How tandem works](https://github.com/Bhavya6187/tandem/blob/main/docs/how-it-works.md) —
175
+ sync engine, PTY passthrough, compatibility, where your data lives
176
+ - [Developing tandem](https://github.com/Bhavya6187/tandem/blob/main/docs/development.md) —
177
+ dev setup and the converter adapter interface
178
+ - [Observed session formats](https://github.com/Bhavya6187/tandem/blob/main/docs/formats.md) —
179
+ the claude/codex transcript details tandem is pinned against
180
+
181
+ ## License
182
+
183
+ [MIT](https://github.com/Bhavya6187/tandem/blob/main/LICENSE)
@@ -0,0 +1,157 @@
1
+ <div align="center">
2
+
3
+ # 🤝 tandem
4
+
5
+ **One coding session. Two AI agents. Zero lost context.**
6
+
7
+ Run [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and
8
+ [OpenAI Codex CLI](https://github.com/openai/codex) as a single paired
9
+ session — each model in its own native harness. Work in either one,
10
+ flip to the other with **Ctrl-]**, and pick up exactly where you left
11
+ off. Only one model runs per turn; the other stays in sync through pure
12
+ local file translation.
13
+
14
+ [![CI](https://github.com/Bhavya6187/tandem/actions/workflows/ci.yml/badge.svg)](https://github.com/Bhavya6187/tandem/actions/workflows/ci.yml)
15
+ [![PyPI](https://img.shields.io/pypi/v/tandem-cli)](https://pypi.org/project/tandem-cli/)
16
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://github.com/Bhavya6187/tandem/blob/main/pyproject.toml)
17
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/Bhavya6187/tandem/blob/main/LICENSE)
18
+
19
+ ```bash
20
+ uv tool install tandem-cli
21
+ ```
22
+
23
+ ![tandem demo — one session moving between Claude Code and Codex](https://raw.githubusercontent.com/Bhavya6187/tandem/main/docs/demo.gif)
24
+
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## Quick start
30
+
31
+ You'll need Python 3.11+ and the `claude` and `codex` CLIs on your PATH.
32
+
33
+ ```bash
34
+ uv tool install tandem-cli # or: pip install tandem-cli
35
+ cd your-project
36
+ tandem # fresh paired session; drops you into claude
37
+ ```
38
+
39
+ Work normally — that's the real Claude Code TUI. When you want the other
40
+ model, press **Ctrl-]**: tandem closes out the harness at the turn
41
+ boundary and reopens the same conversation in Codex a couple of seconds
42
+ later. Press it again to come back. The bottom row tracks who you're
43
+ facing:
44
+
45
+ ```
46
+ claude ● │ codex ○ ^] flips
47
+ ```
48
+
49
+ Pressed mid-turn, the flip arms and fires the moment the model finishes
50
+ (the bar says so; press again to cancel).
51
+
52
+ Exit the harness the usual way and you're back at your shell, with the
53
+ session saved and a hint for picking it up again:
54
+
55
+ ```
56
+ to continue this session: tandem resume a1b2c3d4e5f6
57
+ ```
58
+
59
+ Come back anytime:
60
+
61
+ ```bash
62
+ tandem resume # most recent session in this directory
63
+ tandem resume a1b2c3d4e5f6 # a specific one (id from the exit hint)
64
+ ```
65
+
66
+ ## Why tandem?
67
+
68
+ ### 🖥️ One CLI, two harnesses, zero ceremony.
69
+
70
+ `tandem` is the terminal you live in. It fronts the real Claude Code or
71
+ Codex TUI — pixel-for-pixel native — and **Ctrl-]** flips to the other
72
+ one in a couple of seconds, same conversation, same files, same history.
73
+ A one-line tab bar on the bottom row shows which model you're facing;
74
+ everything above it is the untouched native UI.
75
+
76
+ ### 🆕 0.2 — GPT subagents inside Claude Code
77
+
78
+ Type "get the code reviewed by gpt" and tandem's plugin hands the task
79
+ to a local codex worker; the verdict lands back in your Claude session,
80
+ and your Claude quota stays on the main thread. Pin any model your
81
+ codex account offers ("ask sol to review it") or set a cheap default
82
+ once. Setup and routing:
83
+ [GPT subagents guide](https://github.com/Bhavya6187/tandem/blob/main/docs/subagents.md).
84
+
85
+ ![gpt subagent demo — ask for a GPT review in Claude Code, tandem dispatches a codex worker, the verdict comes back, fixes get committed](https://raw.githubusercontent.com/Bhavya6187/tandem/main/docs/gpt-subagent.gif)
86
+
87
+ ### ⏳ Hit a usage limit? Just keep going.
88
+
89
+ Claude runs out of its usage window mid-refactor? Press **Ctrl-]** and
90
+ Codex continues the **same conversation** a second later — same files,
91
+ same history, same plan. Two subscriptions become one long runway.
92
+
93
+ ### 🌩️ Immune to outages.
94
+
95
+ An Anthropic or OpenAI outage doesn't stop your work: **Ctrl-]**, keep
96
+ going in the other harness, and flip back whenever it clears.
97
+
98
+ Five more reasons — subscriptions not API bills, two model families on
99
+ one problem, native harnesses, instant switching, privacy:
100
+ [Why tandem?](https://github.com/Bhavya6187/tandem/blob/main/docs/why.md)
101
+
102
+ ## How it works
103
+
104
+ - Only one model runs per turn — the other side is never invoked to
105
+ "catch up".
106
+ - As you work, tandem translates the growing transcript into the other
107
+ CLI's native session format — pure local file I/O, no model calls —
108
+ so the other side is always resume-ready.
109
+ - Each CLI is the real thing running on a PTY: your keybindings, slash
110
+ commands, and MCP servers all work exactly as they do today — tandem
111
+ reserves exactly one key (Ctrl-]) and one terminal row (the tab bar).
112
+ - A flip waits for the turn boundary, exits the fronted CLI gracefully,
113
+ lets the sync settle, and resumes the other side — no stop in
114
+ between.
115
+ - Everything stays local: the CLIs' own session files plus a small
116
+ SQLite database in `~/.tandem`. No cloud sync, no telemetry.
117
+ - Uninstall tandem tomorrow and both sessions still resume natively
118
+ with `claude --resume` and `codex resume`.
119
+
120
+ Full mechanics — sync engine, crash safety, compatibility ranges:
121
+ [How tandem works](https://github.com/Bhavya6187/tandem/blob/main/docs/how-it-works.md).
122
+
123
+ ## Command cheat sheet
124
+
125
+ | Command | What it does |
126
+ | --- | --- |
127
+ | `tandem` | Start a fresh paired session (Claude active; `--active codex` to flip) |
128
+ | `Ctrl-]` | Flip to the other harness from inside a running session (rebindable in `[frame]`) |
129
+ | `tandem resume [id]` | Continue the most recent (or a specific) session |
130
+ | `tandem run --on codex "…"` | One-off prompt to the *other* agent, with full context |
131
+ | `tandem sub "…"` | Run one delegated task on a codex model (what GPT subagents use under the hood) |
132
+ | `tandem status` | Show pairing, roles, and sync position |
133
+ | `tandem plugin install` | Install the Claude Code plugin (also offered on first `tandem` launch) |
134
+
135
+ Three maintenance commands round it out: `tandem doctor` (health
136
+ check), `tandem sync` (manual catch-up translation), and `tandem
137
+ sync-mcp` (share MCP server configs between the tools).
138
+
139
+ ## Docs
140
+
141
+ - [GPT subagents](https://github.com/Bhavya6187/tandem/blob/main/docs/subagents.md) —
142
+ plugin install, worker model, routing modes, sandbox & trust boundary
143
+ - [Why tandem?](https://github.com/Bhavya6187/tandem/blob/main/docs/why.md) —
144
+ the full pitch, all eight reasons
145
+ - [Configuration](https://github.com/Bhavya6187/tandem/blob/main/docs/configuration.md) —
146
+ the optional `~/.tandem/config.toml`: subagent workers, per-harness
147
+ startup args, the flip key and tab bar
148
+ - [How tandem works](https://github.com/Bhavya6187/tandem/blob/main/docs/how-it-works.md) —
149
+ sync engine, PTY passthrough, compatibility, where your data lives
150
+ - [Developing tandem](https://github.com/Bhavya6187/tandem/blob/main/docs/development.md) —
151
+ dev setup and the converter adapter interface
152
+ - [Observed session formats](https://github.com/Bhavya6187/tandem/blob/main/docs/formats.md) —
153
+ the claude/codex transcript details tandem is pinned against
154
+
155
+ ## License
156
+
157
+ [MIT](https://github.com/Bhavya6187/tandem/blob/main/LICENSE)
@@ -0,0 +1,77 @@
1
+ # Configuration
2
+
3
+ tandem reads one optional file: `~/.tandem/config.toml`. No file is
4
+ required; every key has a working default. (Back to the
5
+ [README](../README.md).)
6
+
7
+ ## [subagents] — GPT subagent workers
8
+
9
+ Worker model, routing mode, and context handling for GPT subagent
10
+ dispatches. Key semantics and the full routing story live in the
11
+ [GPT subagents guide](subagents.md):
12
+
13
+ ```toml
14
+ [subagents]
15
+ model = "gpt-5.6-luna" # worker default; unset = your codex account's default
16
+ route = "manual" # manual | all | off
17
+ context = "match" # match | task | full
18
+ keep_forks = false # keep each worker's rollout for debugging
19
+ ```
20
+
21
+ ## [claude] / [codex] — per-harness startup args
22
+
23
+ Optional per-harness tables add flags to every interactive session tandem
24
+ opens (`tandem`, `tandem resume`) — one-off relays (`tandem run`),
25
+ subagent dispatch, and doctor probes are unaffected:
26
+
27
+ ```toml
28
+ [claude]
29
+ args = ["--dangerously-skip-permissions"]
30
+
31
+ [codex]
32
+ args = ["--dangerously-bypass-approvals-and-sandbox"]
33
+ ```
34
+
35
+ The flags shown disable the harnesses' own permission prompts for
36
+ sessions tandem launches — set them only if that is what you want.
37
+ The list is passed to the harness raw: a flag that expects a value can
38
+ swallow the settings tandem appends after it and break turn tracking.
39
+ Malformed values (a non-list, empty or non-string elements) are
40
+ silently ignored rather than failing the launch.
41
+
42
+ ## [frame] — the flip key, the tab bar, and warm flips
43
+
44
+ The frame is tandem's own surface inside a running session: one reserved
45
+ keybind that flips to the other harness, the one-line tab bar on the
46
+ bottom terminal row, and the pipelined boot behind the flip.
47
+
48
+ ```toml
49
+ [frame]
50
+ flip_key = "ctrl-]"
51
+ bar = true
52
+ warm = true # boot the incoming harness while the outgoing one shuts down
53
+ ```
54
+
55
+ | key | default | meaning |
56
+ | --- | --- | --- |
57
+ | `flip_key` | `"ctrl-]"` | The flip keybind, consumed by tandem (never forwarded). Accepts `ctrl-<char>` or a hex byte like `"0x1d"`; printable keys are rejected (they would swallow typing). The bar relabels itself to match (`ctrl-t` shows `^T flips`). |
58
+ | `bar` | `true` | The one-line tab bar on the bottom terminal row. `false` hides it; the flip still works. |
59
+ | `warm` | `true` | Overlap the two halves of a flip: the incoming harness starts booting the moment the flip fires (a mid-turn press waits for the turn boundary first), while the outgoing one is still shutting down. `false` gives fully serial flips — the boot only begins once the old harness is gone. |
60
+
61
+ An unparseable value falls back to the default rather than failing the
62
+ launch. If a terminal can't sustain the bar, tandem drops it for the rest
63
+ of that session — the flip is unaffected — and `tandem doctor` warns
64
+ about it until you delete the marker file it names; set `bar = false` if
65
+ you'd rather keep the bar off for good. Shrinking the window below the
66
+ bar's row floor also drops it for the session, but that is tandem's own
67
+ policy rather than a conflict, so nothing is recorded and `doctor` stays
68
+ quiet.
69
+
70
+ Warming is pipelining, not a background service: the moment the flip
71
+ fires — a mid-turn `Ctrl-]` waits for the turn boundary first — the
72
+ incoming harness starts and the outgoing one is torn down at the same
73
+ time, so the flip lands at about the incoming harness's own start-up
74
+ speed instead of that plus the shutdown. Nothing exists before you press
75
+ the key and nothing survives the flip — between flips a tandem session is
76
+ exactly one harness process. `warm = false` restores the fully serial
77
+ flip, which is slower but does the same thing.
@@ -0,0 +1,29 @@
1
+ # Developing tandem
2
+
3
+ Dev setup and the extension surface. (Back to the
4
+ [README](../README.md).)
5
+
6
+ ## Extending tandem
7
+
8
+ The sync engine talks to a small adapter interface
9
+ (`tandem.converter.TraceConverter`):
10
+
11
+ ```python
12
+ class TraceConverter(Protocol):
13
+ def translate_entry(entry, direction, ctx) -> list[TargetEntry] | TranslationError
14
+ ```
15
+
16
+ `ReferenceConverter` implements it via a normalized event model
17
+ (`tandem/events.py`) derived from the observed formats. Pass your own
18
+ converter to `SyncEngine(store, session, source, converter=...)`.
19
+
20
+ ## Development
21
+
22
+ ```bash
23
+ uv sync && uv run pytest
24
+ pipx install . # or: uv tool install .
25
+ ```
26
+
27
+ Dependencies are deliberately small: `click` (CLI), `pydantic` v2 (event
28
+ schema), `watchdog` (transcript tailing), `pexpect`/ptyprocess (PTY
29
+ passthrough); state is stdlib `sqlite3`.
@@ -0,0 +1,106 @@
1
+ # How tandem works
2
+
3
+ The mechanics behind the pairing: sync engine, PTY passthrough,
4
+ compatibility ranges, and where your data lives. (Back to the
5
+ [README](../README.md).)
6
+
7
+ - **One model per command — always.** Only the active harness's model is
8
+ ever invoked (or, for `run --on`, the target's). The shadow side is pure
9
+ local file I/O: tandem tails the active transcript, translates each
10
+ entry, and appends it to the shadow's session file. The shadow's model
11
+ is never called to "catch up".
12
+ - **Exit means exit.** Leaving the harness — as opposed to flipping out
13
+ of it — prints the resume hint and returns you to your OS shell; the
14
+ paired session is saved and `tandem resume` re-enters it.
15
+ `status` / `sync` / `doctor` / `run --on` / `sync-mcp` run one-shot
16
+ from your shell, targeting the directory's most recently used session.
17
+ - **The frame: flip without leaving.** Ctrl-] (configurable, consumed at
18
+ the PTY layer, ignored inside bracketed paste) flips the screen to the
19
+ other harness: pressed mid-turn it arms and fires at the turn boundary
20
+ (press again to cancel — the bar shows the armed state), then
21
+ tandem exits the fronted CLI gracefully (quit keystrokes, then SIGTERM,
22
+ then a bounded SIGKILL), lets the incremental sync settle, flips roles,
23
+ and resumes the other side — with no stop in between.
24
+ With claude fronted, the boundary comes from claude's own session
25
+ registry (`~/.claude/sessions/<pid>.json`, the data `claude agents
26
+ --json` prints): `busy` holds the flip, anything else fires it — so an
27
+ idle prompt flips instantly even though claude keeps appending
28
+ housekeeping to its transcript between turns.
29
+ (The registry appeared in claude 2.1.226; on older claudes the probe
30
+ finds nothing and every armed flip fires at once, mid-turn included —
31
+ the deliberate single-tier trade: no silent valve, breakage is loud.)
32
+ With codex fronted the
33
+ transcript-marker rules stand: where no marker could be wired (codex
34
+ with a `notify` handler of your own, which tandem won't clobber) ~2s of
35
+ transcript quiescence stands in; where one was wired, a 120s valve
36
+ covers a marker that never arrives — far above any plausible tool-call
37
+ silence, because firing early kills a live turn while firing late only
38
+ costs a wait (and Ctrl-] cancels). The
39
+ bottom terminal row is tandem's one drawn pixel: the child is told the
40
+ terminal is a row shorter, a scroll region keeps output above the bar,
41
+ and a targeted watcher reasserts it after child screen resets. If a
42
+ terminal can't sustain the bar it drops for the session (the flip keeps
43
+ working) and `tandem doctor` says so until you delete the marker file it
44
+ names; shrinking the window below the bar's row floor drops it just as
45
+ permanently, but silently — that one isn't a conflict to fix.
46
+ - **PTY passthrough.** tandem launches the real CLI on a pty (raw mode,
47
+ resize forwarding, signals through the line discipline) and never
48
+ scrapes terminal output — the transcript files are the source of truth.
49
+ Turn-complete hooks (`claude --settings` Stop hook, `codex -c
50
+ notify=[…]`) are wired per-invocation as wake-up signals, with
51
+ fs-watching as the data path and fallback. If your codex config already
52
+ sets `notify`, tandem leaves it alone.
53
+ - **Append-only, crash-safe sync.** Each transcript entry is translated as
54
+ it lands — no bulk re-export at switch time. Appends are whole-line +
55
+ fsync, and a write-ahead intent in the sync cursor makes translation
56
+ exactly-once across crashes; on restart, sync resumes from the last
57
+ confirmed entry.
58
+ - **Tool calls translate natively.** The harnesses speak different tool
59
+ vocabularies, so each completed call+result pair is re-expressed in the
60
+ shadow's own terms — `Bash` ↔ `exec_command`, `Edit`/`Write` ↔
61
+ `apply_patch`, `TodoWrite` ↔ `update_plan` — and lands as a real
62
+ tool-call record, so shadow history reads as the shadow's own work.
63
+ Anything that wouldn't map truthfully passes through verbatim; a call
64
+ whose result never arrived is closed with a `(tool result not recorded)`
65
+ placeholder at handoff, since both replay APIs reject dangling calls.
66
+ - **Attribution stays legible.** Every synced *text* message is tagged
67
+ `[via claude-code]` / `[via codex]` (tandem's own notes use `[tandem]`),
68
+ so interleaved histories make sense to you and to the models. Tool
69
+ activity is untagged — it's mirrored as native records, not prose.
70
+ - **Errors are contained.** An entry that fails translation becomes a
71
+ single per-turn placeholder in the shadow, with the raw entry
72
+ quarantined under `~/.tandem/quarantine/…` — and sync continues. The
73
+ shadow is never corrupted or truncated.
74
+ - **Memory files stay in step.** Fresh launches and every switch sync
75
+ CLAUDE.md ↔ AGENTS.md: shared content lives in a
76
+ `<!-- tandem:shared:begin/end -->` block (newer file wins),
77
+ tool-specific text outside the block is preserved, and a file without
78
+ markers is read from but never rewritten. Git state is never touched.
79
+
80
+ ## Compatibility
81
+
82
+ Session formats are internal to the CLIs and drift between releases.
83
+ tandem pins what it was built against (observed formats documented in
84
+ [docs/formats.md](https://github.com/Bhavya6187/tandem/blob/main/docs/formats.md)):
85
+
86
+ | CLI | Tested | Accepted range |
87
+ | --- | --- | --- |
88
+ | Claude Code | 2.1.220 | ≥ 2.0, < 3 |
89
+ | Codex CLI | 0.145.0 | ≥ 0.140, < 0.150 |
90
+
91
+ Outside the range, tandem warns and asks you to run `tandem doctor`.
92
+ Format knowledge is isolated per tool in
93
+ `src/tandem/harness/claude_code.py` and `src/tandem/harness/codex.py`.
94
+
95
+ ## Where your data lives
96
+
97
+ - `~/.tandem/state.db` — SQLite: session pairing + per-source sync cursors
98
+ (override the directory with `TANDEM_HOME`)
99
+ - `~/.tandem/quarantine/<session>/` — raw entries that failed translation
100
+ - `~/.claude/projects/<munged-cwd>/<session-id>.jsonl` — claude transcript
101
+ - `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<session-id>.jsonl` — codex
102
+ rollout (`CLAUDE_CONFIG_DIR` / `CODEX_HOME` honored)
103
+
104
+ Claude session ids are minted by tandem (`claude --session-id`); codex
105
+ mints its own on first run and tandem captures it from the new rollout
106
+ file.