overcode 0.4.1__tar.gz → 0.5.1__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 (156) hide show
  1. overcode-0.5.1/MANIFEST.in +3 -0
  2. {overcode-0.4.1/src/overcode.egg-info → overcode-0.5.1}/PKG-INFO +31 -4
  3. {overcode-0.4.1 → overcode-0.5.1}/README.md +30 -3
  4. {overcode-0.4.1 → overcode-0.5.1}/pyproject.toml +10 -2
  5. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/__init__.py +1 -1
  6. overcode-0.5.1/src/overcode/backends/__init__.py +170 -0
  7. overcode-0.5.1/src/overcode/backends/base.py +189 -0
  8. overcode-0.5.1/src/overcode/backends/claude_code.py +270 -0
  9. overcode-0.5.1/src/overcode/backends/codex.py +590 -0
  10. overcode-0.5.1/src/overcode/backends/codex_stats.py +508 -0
  11. overcode-0.5.1/src/overcode/backends/grok.py +857 -0
  12. overcode-0.5.1/src/overcode/backends/grok_stats.py +400 -0
  13. overcode-0.5.1/src/overcode/backends/opencode.py +719 -0
  14. overcode-0.5.1/src/overcode/backends/opencode_stats.py +641 -0
  15. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/bundled_skills.py +4 -4
  16. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/claude_pid.py +1 -1
  17. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/__init__.py +5 -2
  18. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/_shared.py +1 -1
  19. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/agent.py +207 -79
  20. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/budget.py +41 -0
  21. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/daemon.py +14 -5
  22. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/doctor.py +65 -12
  23. overcode-0.5.1/src/overcode/cli/focal.py +76 -0
  24. overcode-0.5.1/src/overcode/cli/hooks.py +174 -0
  25. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/jobs.py +1 -1
  26. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/monitoring.py +0 -1
  27. overcode-0.5.1/src/overcode/cli/parallelism.py +115 -0
  28. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/split.py +69 -10
  29. overcode-0.5.1/src/overcode/cli/tags.py +121 -0
  30. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/config.py +163 -1
  31. overcode-0.5.1/src/overcode/daemon_claude_skill.md +105 -0
  32. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/daemon_logging.py +46 -0
  33. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/dependency_check.py +61 -9
  34. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/doctor.py +91 -40
  35. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/exceptions.py +15 -7
  36. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/follow_mode.py +12 -2
  37. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/history_reader.py +463 -50
  38. overcode-0.5.1/src/overcode/hook_handler.py +795 -0
  39. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/hook_status_detector.py +378 -23
  40. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/implementations.py +38 -2
  41. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/launcher.py +338 -234
  42. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/mocks.py +8 -0
  43. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/monitor_daemon.py +254 -148
  44. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/monitor_daemon_state.py +31 -4
  45. overcode-0.5.1/src/overcode/opencode_plugin/overcode-telemetry.js +481 -0
  46. overcode-0.5.1/src/overcode/pricing.py +157 -0
  47. overcode-0.5.1/src/overcode/process_resources.py +114 -0
  48. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/protocols.py +13 -0
  49. overcode-0.5.1/src/overcode/sandbox_detect.py +98 -0
  50. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/session_manager.py +378 -35
  51. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/settings.py +210 -6
  52. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/sister_poller.py +35 -0
  53. overcode-0.5.1/src/overcode/stats_reader.py +334 -0
  54. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/status_constants.py +177 -4
  55. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/status_detector.py +34 -44
  56. overcode-0.5.1/src/overcode/status_detector_factory.py +211 -0
  57. overcode-0.5.1/src/overcode/status_history.py +626 -0
  58. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/status_patterns.py +279 -56
  59. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/summary_columns.py +447 -35
  60. overcode-0.5.1/src/overcode/summary_groups.py +64 -0
  61. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/supervisor_daemon.py +25 -3
  62. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/supervisor_daemon_core.py +10 -1
  63. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tmux_manager.py +10 -2
  64. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tmux_utils.py +34 -0
  65. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui.py +544 -58
  66. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui.tcss +34 -0
  67. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_actions/input.py +58 -18
  68. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_actions/session.py +97 -28
  69. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_helpers.py +46 -0
  70. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_logic.py +135 -0
  71. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/__init__.py +5 -0
  72. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/agent_select_modal.py +5 -5
  73. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/daemon_panel.py +11 -11
  74. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/daemon_status_bar.py +135 -12
  75. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/help_overlay.py +10 -7
  76. overcode-0.5.1/src/overcode/tui_widgets/jump_modal.py +163 -0
  77. overcode-0.5.1/src/overcode/tui_widgets/new_agent_defaults_modal.py +142 -0
  78. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/new_agent_modal.py +30 -13
  79. overcode-0.5.1/src/overcode/tui_widgets/passthru_config_modal.py +138 -0
  80. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/preview_pane.py +15 -2
  81. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/session_summary.py +90 -10
  82. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/summary_config_modal.py +5 -1
  83. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/tui_log_panel.py +8 -8
  84. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/usage_monitor.py +22 -5
  85. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/web_api.py +29 -8
  86. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/web_control_api.py +68 -29
  87. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/web_server.py +1 -1
  88. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/wrapper.py +95 -27
  89. {overcode-0.4.1 → overcode-0.5.1/src/overcode.egg-info}/PKG-INFO +31 -4
  90. {overcode-0.4.1 → overcode-0.5.1}/src/overcode.egg-info/SOURCES.txt +18 -0
  91. {overcode-0.4.1 → overcode-0.5.1}/tests/test_e2e_multi_agent_jokes.py +6 -6
  92. overcode-0.4.1/MANIFEST.in +0 -3
  93. overcode-0.4.1/src/overcode/cli/hooks.py +0 -101
  94. overcode-0.4.1/src/overcode/daemon_claude_skill.md +0 -68
  95. overcode-0.4.1/src/overcode/hook_handler.py +0 -277
  96. overcode-0.4.1/src/overcode/pricing.py +0 -106
  97. overcode-0.4.1/src/overcode/status_detector_factory.py +0 -103
  98. overcode-0.4.1/src/overcode/status_history.py +0 -323
  99. overcode-0.4.1/src/overcode/summary_groups.py +0 -109
  100. overcode-0.4.1/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -98
  101. {overcode-0.4.1 → overcode-0.5.1}/LICENSE +0 -0
  102. {overcode-0.4.1 → overcode-0.5.1}/setup.cfg +0 -0
  103. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/agent_scanner.py +0 -0
  104. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/claude_config.py +0 -0
  105. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/__main__.py +0 -0
  106. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/config.py +0 -0
  107. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/perms.py +0 -0
  108. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/sister.py +0 -0
  109. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/skills.py +0 -0
  110. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/cli/wrappers.py +0 -0
  111. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/daemon_utils.py +0 -0
  112. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/data_export.py +0 -0
  113. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/duration.py +0 -0
  114. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/interfaces.py +0 -0
  115. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/job_launcher.py +0 -0
  116. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/job_manager.py +0 -0
  117. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/logging_config.py +0 -0
  118. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/monitor_daemon_core.py +0 -0
  119. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/notifier.py +0 -0
  120. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/pid_utils.py +0 -0
  121. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/presence_logger.py +0 -0
  122. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/sister_controller.py +0 -0
  123. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/ssh_provisioner.py +0 -0
  124. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/standing_instructions.py +0 -0
  125. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/summarizer_client.py +0 -0
  126. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/summarizer_component.py +0 -0
  127. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/supervisor_layout.sh +0 -0
  128. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/testing/__init__.py +0 -0
  129. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/testing/renderer.py +0 -0
  130. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/testing/tmux_driver.py +0 -0
  131. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/testing/tui_eye.py +0 -0
  132. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/testing/tui_eye_skill.md +0 -0
  133. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/time_context.py +0 -0
  134. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_actions/__init__.py +0 -0
  135. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_actions/daemon.py +0 -0
  136. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_actions/navigation.py +0 -0
  137. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_actions/view.py +0 -0
  138. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_render.py +0 -0
  139. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/command_bar.py +0 -0
  140. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  141. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
  142. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/job_summary.py +0 -0
  143. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/modal_base.py +0 -0
  144. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
  145. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/status_timeline.py +0 -0
  146. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/tui_widgets/tmux_config_modal.py +0 -0
  147. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/web/__init__.py +0 -0
  148. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/web/templates/analytics.html +0 -0
  149. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/web/templates/dashboard.html +0 -0
  150. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/web_chartjs.py +0 -0
  151. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/web_server_runner.py +0 -0
  152. {overcode-0.4.1 → overcode-0.5.1}/src/overcode/web_templates.py +0 -0
  153. {overcode-0.4.1 → overcode-0.5.1}/src/overcode.egg-info/dependency_links.txt +0 -0
  154. {overcode-0.4.1 → overcode-0.5.1}/src/overcode.egg-info/entry_points.txt +0 -0
  155. {overcode-0.4.1 → overcode-0.5.1}/src/overcode.egg-info/requires.txt +0 -0
  156. {overcode-0.4.1 → overcode-0.5.1}/src/overcode.egg-info/top_level.txt +0 -0
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include src/overcode *.md *.sh *.js
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: overcode
3
- Version: 0.4.1
3
+ Version: 0.5.1
4
4
  Summary: A supervisor for managing multiple Claude Code instances in tmux
5
5
  Author: Mike Bond
6
6
  Project-URL: Homepage, https://github.com/mkb23/overcode
@@ -42,13 +42,13 @@ Dynamic: license-file
42
42
 
43
43
  # overcode
44
44
 
45
- A TUI supervisor for managing multiple Claude Code agents in tmux.
45
+ A TUI supervisor for managing multiple coding agents (Claude Code, opencode, Codex, Grok) in tmux.
46
46
 
47
47
  Launch autonomous coding agents, monitor their progress in real-time, track costs and activity, and coordinate work across your projects—all from a single dashboard.
48
48
 
49
49
  ## Why overcode?
50
50
 
51
- Running multiple Claude Code agents is powerful, but managing them gets chaotic fast. Overcode solves this by giving you:
51
+ Running multiple coding agents is powerful, but managing them gets chaotic fast. Overcode solves this by giving you:
52
52
 
53
53
  - **Unified visibility** - See all agents at a glance: what they're working on, whether they need input, and how much they're costing you
54
54
  - **Native tmux integration** - A split layout with your dashboard on top and the focused agent's live terminal below—full speed, full color, full scrollback
@@ -81,7 +81,7 @@ overcode tmux
81
81
 
82
82
  This creates a split layout: the overcode dashboard on top, the focused agent's live terminal on the bottom. Navigate agents with `j/k` — the bottom pane follows automatically. Press `Tab` to toggle focus between panes.
83
83
 
84
- **Requirements:** Python 3.12+, tmux, [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
84
+ **Requirements:** Python 3.12+, tmux, and an agent CLI — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [opencode](https://opencode.ai), [Codex CLI](https://github.com/openai/codex), or [Grok Build](https://x.ai)
85
85
 
86
86
  See the [Getting Started Guide](docs/getting-started.md) for a complete walkthrough.
87
87
 
@@ -128,6 +128,18 @@ Run agents in custom environments — containers, VMs, or any setup your project
128
128
 
129
129
  See the [Wrappers Guide](docs/wrappers.md) for setup and customisation.
130
130
 
131
+ ### Agent Backends
132
+ Overcode is not Claude-Code-only. It supports four agent CLIs today: Claude Code, opencode,
133
+ Codex, and Grok. Launch a non-default one with `overcode launch -n my-agent --backend
134
+ opencode` (or `codex` / `grok`) and it appears in the same dashboard with live hook-grade
135
+ status, previews, AI summaries, send-instruction, restart, kill, resume, fork, and
136
+ token/cost/context columns. Backends declare their capabilities, so the Claude-only
137
+ subsystems — skills, the sandbox badge, the subscription-usage widget, agent teams — are
138
+ hidden rather than shown as misleading zeros. A backend badge column appears only when your
139
+ fleet actually mixes backends, so a Claude-only setup looks exactly as it did. See the
140
+ [Backends Guide](docs/backends.md) for the full support matrix, flag mapping, and current
141
+ limitations.
142
+
131
143
  ### Sister Integration
132
144
  Aggregate agents from multiple machines into one dashboard:
133
145
  - Configure sister machines in `~/.overcode/config.yaml`
@@ -165,6 +177,21 @@ See the [TUI Guide](docs/tui-guide.md) for all keyboard shortcuts.
165
177
  - [Wrappers](docs/wrappers.md) - Run agents in containers and custom environments
166
178
  - [Advanced Features](docs/advanced-features.md) - Sleep mode, handover, remote monitoring
167
179
 
180
+ ## Testing
181
+
182
+ Unit tests run on the host (`make test-unit`). End-to-end tests run inside a
183
+ disposable Docker container so daemons, tmux servers and (mock or real) Claude
184
+ processes can never leak onto your machine:
185
+
186
+ ```bash
187
+ make e2e # workflow + visual tiers (mock Claude, no credentials)
188
+ make e2e-real # real-LLM smoke tier (needs CLAUDE_CODE_OAUTH_TOKEN)
189
+ make e2e-shell # debug shell inside the test container
190
+ ```
191
+
192
+ See [docs/design/e2e-devcontainer-testing.md](docs/design/e2e-devcontainer-testing.md)
193
+ for the architecture, coverage matrix, and CI setup.
194
+
168
195
  ## License
169
196
 
170
197
  MIT
@@ -1,12 +1,12 @@
1
1
  # overcode
2
2
 
3
- A TUI supervisor for managing multiple Claude Code agents in tmux.
3
+ A TUI supervisor for managing multiple coding agents (Claude Code, opencode, Codex, Grok) in tmux.
4
4
 
5
5
  Launch autonomous coding agents, monitor their progress in real-time, track costs and activity, and coordinate work across your projects—all from a single dashboard.
6
6
 
7
7
  ## Why overcode?
8
8
 
9
- Running multiple Claude Code agents is powerful, but managing them gets chaotic fast. Overcode solves this by giving you:
9
+ Running multiple coding agents is powerful, but managing them gets chaotic fast. Overcode solves this by giving you:
10
10
 
11
11
  - **Unified visibility** - See all agents at a glance: what they're working on, whether they need input, and how much they're costing you
12
12
  - **Native tmux integration** - A split layout with your dashboard on top and the focused agent's live terminal below—full speed, full color, full scrollback
@@ -39,7 +39,7 @@ overcode tmux
39
39
 
40
40
  This creates a split layout: the overcode dashboard on top, the focused agent's live terminal on the bottom. Navigate agents with `j/k` — the bottom pane follows automatically. Press `Tab` to toggle focus between panes.
41
41
 
42
- **Requirements:** Python 3.12+, tmux, [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
42
+ **Requirements:** Python 3.12+, tmux, and an agent CLI — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [opencode](https://opencode.ai), [Codex CLI](https://github.com/openai/codex), or [Grok Build](https://x.ai)
43
43
 
44
44
  See the [Getting Started Guide](docs/getting-started.md) for a complete walkthrough.
45
45
 
@@ -86,6 +86,18 @@ Run agents in custom environments — containers, VMs, or any setup your project
86
86
 
87
87
  See the [Wrappers Guide](docs/wrappers.md) for setup and customisation.
88
88
 
89
+ ### Agent Backends
90
+ Overcode is not Claude-Code-only. It supports four agent CLIs today: Claude Code, opencode,
91
+ Codex, and Grok. Launch a non-default one with `overcode launch -n my-agent --backend
92
+ opencode` (or `codex` / `grok`) and it appears in the same dashboard with live hook-grade
93
+ status, previews, AI summaries, send-instruction, restart, kill, resume, fork, and
94
+ token/cost/context columns. Backends declare their capabilities, so the Claude-only
95
+ subsystems — skills, the sandbox badge, the subscription-usage widget, agent teams — are
96
+ hidden rather than shown as misleading zeros. A backend badge column appears only when your
97
+ fleet actually mixes backends, so a Claude-only setup looks exactly as it did. See the
98
+ [Backends Guide](docs/backends.md) for the full support matrix, flag mapping, and current
99
+ limitations.
100
+
89
101
  ### Sister Integration
90
102
  Aggregate agents from multiple machines into one dashboard:
91
103
  - Configure sister machines in `~/.overcode/config.yaml`
@@ -123,6 +135,21 @@ See the [TUI Guide](docs/tui-guide.md) for all keyboard shortcuts.
123
135
  - [Wrappers](docs/wrappers.md) - Run agents in containers and custom environments
124
136
  - [Advanced Features](docs/advanced-features.md) - Sleep mode, handover, remote monitoring
125
137
 
138
+ ## Testing
139
+
140
+ Unit tests run on the host (`make test-unit`). End-to-end tests run inside a
141
+ disposable Docker container so daemons, tmux servers and (mock or real) Claude
142
+ processes can never leak onto your machine:
143
+
144
+ ```bash
145
+ make e2e # workflow + visual tiers (mock Claude, no credentials)
146
+ make e2e-real # real-LLM smoke tier (needs CLAUDE_CODE_OAUTH_TOKEN)
147
+ make e2e-shell # debug shell inside the test container
148
+ ```
149
+
150
+ See [docs/design/e2e-devcontainer-testing.md](docs/design/e2e-devcontainer-testing.md)
151
+ for the architecture, coverage matrix, and CI setup.
152
+
126
153
  ## License
127
154
 
128
155
  MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "overcode"
7
- version = "0.4.1"
7
+ version = "0.5.1"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -68,7 +68,15 @@ tui-eye = "overcode.testing.tui_eye:main"
68
68
  where = ["src"]
69
69
 
70
70
  [tool.setuptools.package-data]
71
- overcode = ["*.md", "*.sh", "*.tcss", "web/templates/*.html"]
71
+ overcode = [
72
+ "*.md",
73
+ "*.sh",
74
+ "*.tcss",
75
+ "web/templates/*.html",
76
+ # Bundled opencode telemetry plugin — copied into a project's
77
+ # .opencode/plugins/ at launch, so it has to ship with the wheel.
78
+ "opencode_plugin/*.js",
79
+ ]
72
80
 
73
81
  [tool.black]
74
82
  line-length = 100
@@ -1,5 +1,5 @@
1
1
  """
2
- Overcode - A supervisor for managing multiple Claude Code instances.
2
+ Overcode - A supervisor for managing multiple coding agents (Claude Code, opencode).
3
3
  """
4
4
 
5
5
  from pathlib import Path
@@ -0,0 +1,170 @@
1
+ """Agent backend registry.
2
+
3
+ ``get_backend(name)`` resolves a ``Session.backend`` discriminator to the
4
+ adapter that owns that CLI's argv grammar and gestures.
5
+ """
6
+
7
+ from typing import Any, Dict, List
8
+
9
+ from .base import (
10
+ AgentBackend,
11
+ BackendCapability,
12
+ DialogRule,
13
+ KeyPress,
14
+ LaunchSpec,
15
+ supports,
16
+ )
17
+ from .claude_code import ClaudeCodeBackend
18
+ from .codex import CodexBackend
19
+ from .grok import GrokBackend
20
+ from .opencode import OpencodeBackend
21
+
22
+ DEFAULT_BACKEND = "claude-code"
23
+
24
+ _BACKENDS: Dict[str, AgentBackend] = {
25
+ ClaudeCodeBackend.name: ClaudeCodeBackend(),
26
+ OpencodeBackend.name: OpencodeBackend(),
27
+ CodexBackend.name: CodexBackend(),
28
+ GrokBackend.name: GrokBackend(),
29
+ }
30
+
31
+
32
+ class UnknownBackendError(ValueError):
33
+ """Raised when a Session names a backend overcode doesn't have."""
34
+
35
+
36
+ def get_backend(name: str = DEFAULT_BACKEND) -> AgentBackend:
37
+ """Resolve a backend by name.
38
+
39
+ Empty/None resolves to the default so pre-backend sessions and
40
+ partially-populated dicts keep working.
41
+ """
42
+ key = name or DEFAULT_BACKEND
43
+ try:
44
+ return _BACKENDS[key]
45
+ except KeyError:
46
+ known = ", ".join(sorted(_BACKENDS))
47
+ raise UnknownBackendError(
48
+ f"Unknown agent backend '{key}'. Known backends: {known}"
49
+ ) from None
50
+
51
+
52
+ def list_backends() -> List[str]:
53
+ """Names of every registered backend."""
54
+ return sorted(_BACKENDS)
55
+
56
+
57
+ def register_backend(backend: AgentBackend) -> None:
58
+ """Register (or replace) a backend. Used by tests to install doubles."""
59
+ _BACKENDS[backend.name] = backend
60
+ _invalidate_derived_caches()
61
+
62
+
63
+ def unregister_backend(name: str) -> None:
64
+ """Remove a backend. Silent when it isn't registered."""
65
+ _BACKENDS.pop(name, None)
66
+ _invalidate_derived_caches()
67
+
68
+
69
+ def _invalidate_derived_caches() -> None:
70
+ """Drop per-backend objects other modules cache by backend name."""
71
+ from ..stats_reader import clear_reader_cache
72
+ from ..status_patterns import clear_patterns_cache
73
+ clear_reader_cache()
74
+ clear_patterns_cache()
75
+
76
+
77
+ def session_backend_name(session: Any) -> str:
78
+ """Backend name recorded on a session, defaulting for legacy sessions."""
79
+ name = getattr(session, "backend", None)
80
+ if isinstance(name, str) and name:
81
+ return name
82
+ return DEFAULT_BACKEND
83
+
84
+
85
+ def capability_names(capabilities: BackendCapability) -> List[str]:
86
+ """Serialize a capability flag set to sorted member names.
87
+
88
+ This is the wire form: ``SessionDaemonState.backend_capabilities`` and
89
+ therefore the sister protocol carry these strings, so a newer sister can
90
+ tell an older TUI what its backends can do.
91
+ """
92
+ return sorted(
93
+ member.name
94
+ for member in BackendCapability
95
+ if member.name and member.value and (capabilities & member)
96
+ )
97
+
98
+
99
+ def capabilities_from_names(names: Any) -> BackendCapability:
100
+ """Parse serialized capability names back into a flag set.
101
+
102
+ Unknown names are ignored: a sister running a newer overcode may report
103
+ capabilities this build has never heard of.
104
+ """
105
+ result = BackendCapability.NONE
106
+ if not isinstance(names, (list, tuple, set)):
107
+ return result
108
+ for name in names:
109
+ member = getattr(BackendCapability, name, None) if isinstance(name, str) else None
110
+ if isinstance(member, BackendCapability):
111
+ result |= member
112
+ return result
113
+
114
+
115
+ def session_capabilities(session: Any) -> BackendCapability:
116
+ """Capabilities of the backend behind a session.
117
+
118
+ Remote (sister) agents are answered from the capability list their own
119
+ daemon published, so a sister can run a backend this host doesn't have.
120
+ Sisters predating Phase 6 report nothing; they are assumed to be
121
+ claude-code with the full capability set (design §3, consequence 5).
122
+ """
123
+ # Strict bool check: callers hand us duck-typed stand-ins whose every
124
+ # attribute is truthy, and misreading one as remote would silently swap
125
+ # in another host's capability answer.
126
+ is_remote = getattr(session, "is_remote", False)
127
+ if isinstance(is_remote, bool) and is_remote:
128
+ remote_state = getattr(session, "remote_daemon_state", None)
129
+ if isinstance(remote_state, dict) and remote_state.get("backend_capabilities"):
130
+ return capabilities_from_names(remote_state["backend_capabilities"])
131
+ return get_backend(DEFAULT_BACKEND).capabilities
132
+ try:
133
+ backend = get_backend(session_backend_name(session))
134
+ except UnknownBackendError:
135
+ return BackendCapability.NONE
136
+ return backend.capabilities
137
+
138
+
139
+ def session_supports(session: Any, capability: BackendCapability) -> bool:
140
+ """True when the session's backend declares ``capability``.
141
+
142
+ An unknown backend name answers False — an adapter overcode doesn't
143
+ have can't be assumed to support anything.
144
+ """
145
+ return bool(session_capabilities(session) & capability)
146
+
147
+
148
+ __all__ = [
149
+ "AgentBackend",
150
+ "BackendCapability",
151
+ "ClaudeCodeBackend",
152
+ "CodexBackend",
153
+ "GrokBackend",
154
+ "OpencodeBackend",
155
+ "DEFAULT_BACKEND",
156
+ "DialogRule",
157
+ "KeyPress",
158
+ "LaunchSpec",
159
+ "UnknownBackendError",
160
+ "capabilities_from_names",
161
+ "capability_names",
162
+ "get_backend",
163
+ "list_backends",
164
+ "register_backend",
165
+ "session_backend_name",
166
+ "session_capabilities",
167
+ "session_supports",
168
+ "unregister_backend",
169
+ "supports",
170
+ ]
@@ -0,0 +1,189 @@
1
+ """Backend seam: the protocol every agent CLI adapter implements.
2
+
3
+ An ``AgentBackend`` owns everything that is specific to one agent CLI —
4
+ argv grammar, env vars, startup-dialog handshake, key gestures — so the
5
+ rest of overcode can stay backend-neutral. See
6
+ ``docs/design/agent-agnostic-backends-opencode.md`` §2.1.
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Flag, auto
11
+ from typing import (
12
+ TYPE_CHECKING,
13
+ Dict,
14
+ List,
15
+ Optional,
16
+ Protocol,
17
+ Sequence,
18
+ Set,
19
+ Tuple,
20
+ Type,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from ..stats_reader import StatsReader
25
+ from ..status_patterns import StatusPatterns
26
+
27
+
28
+ class BackendCapability(Flag):
29
+ """What an agent CLI can do. Used to gate UI actions and telemetry."""
30
+
31
+ NONE = 0
32
+ RESUME = auto() # relaunch continuing a prior conversation
33
+ FORK = auto() # branch a conversation into a new agent
34
+ SESSION_ID_PRESCRIPTION = auto() # overcode chooses the session id up front
35
+ HOOK_EVENTS = auto() # push telemetry (hook-state files) available
36
+ TRANSCRIPT_STATS = auto() # tokens/cost/context readable from disk
37
+ PERMISSION_INJECTION = auto() # per-launch permission allowlist
38
+ SKILLS = auto() # skills/persona file discovery
39
+ SANDBOX_PROBE = auto()
40
+ SUBSCRIPTION_USAGE = auto()
41
+ AGENT_TEAMS = auto()
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class KeyPress:
46
+ """One tmux send-keys gesture, with the pause that must follow it."""
47
+
48
+ keys: str
49
+ enter: bool = False
50
+ delay_after: float = 0.0
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class DialogRule:
55
+ """A startup dialog to dismiss while waiting for the input prompt.
56
+
57
+ ``marker`` is matched against captured pane content; ``presses`` are
58
+ sent in order, then the poller sleeps ``settle_seconds``.
59
+ """
60
+
61
+ marker: str
62
+ presses: Sequence[KeyPress]
63
+ settle_seconds: float = 1.0
64
+
65
+
66
+ @dataclass
67
+ class LaunchSpec:
68
+ """Everything a backend needs to render one launch invocation.
69
+
70
+ Carries the launch-time knobs recorded on a ``Session`` plus the
71
+ backend-neutral OVERCODE_* identity fields, so a backend can inject
72
+ them into its own env/config if it needs to.
73
+ """
74
+
75
+ # Session identity (exported into the child process env)
76
+ name: str = ""
77
+ session_id: str = ""
78
+ tmux_session: str = ""
79
+ parent_session_id: Optional[str] = None
80
+ parent_name: Optional[str] = None
81
+
82
+ # Conversation selection
83
+ resume_session_id: Optional[str] = None
84
+ fork: bool = False
85
+ prescribed_session_id: Optional[str] = None
86
+
87
+ # Permissions
88
+ permissiveness_mode: Optional[str] = None
89
+ skip_permissions: bool = False
90
+ dangerously_skip_permissions: bool = False
91
+ include_punchy_perms: bool = False
92
+
93
+ # Agent configuration
94
+ model: Optional[str] = None
95
+ agent: Optional[str] = None
96
+ allowed_tools: Optional[str] = None
97
+ extra_args: List[str] = field(default_factory=list)
98
+ agent_teams: bool = False
99
+ provider: str = "web"
100
+
101
+ # Execution environment
102
+ start_directory: Optional[str] = None
103
+ wrapper: Optional[str] = None
104
+ mock_scenario: Optional[str] = None
105
+
106
+
107
+ class AgentBackend(Protocol):
108
+ """Adapter for one agent CLI."""
109
+
110
+ name: str # "claude-code" | "opencode" | "codex" | "grok"
111
+ display_name: str
112
+ binary: str # for dependency_check + doctor process matching
113
+ version_args: Sequence[str]
114
+ install_hint: str
115
+ process_basenames: Sequence[str]
116
+ not_found_error: Type[Exception]
117
+ capabilities: BackendCapability
118
+
119
+ # True when the backend's fork grammar takes an explicit new session id
120
+ # (--session-id alongside --fork-session) AND the prescribed id is
121
+ # authoritative for the forked session; False when the CLI mints its own
122
+ # fork id and discovery must fill it in. Only meaningful alongside
123
+ # SESSION_ID_PRESCRIPTION — launcher.py's fork branch mints a fresh uuid
124
+ # to bind eagerly only when both are true. Both grok (Phase 3) and Claude
125
+ # Code (#466, 2026-08-28) declare this True: Claude Code was originally
126
+ # assumed to mint its own, different fork id, but a live check of
127
+ # `claude --resume <id> --fork-session --session-id <new>` found the
128
+ # prescribed id IS honored — the CLI writes the fork's transcript under
129
+ # the prescribed uuid. That wrong assumption was #466's root cause:
130
+ # without eager binding, a fork's id was discovered via directory+time
131
+ # matching, which is ambiguous whenever another agent shares the same
132
+ # start_directory (the fork's record could bind to a sibling's id
133
+ # instead of its own). (The default backends don't literally inherit
134
+ # this Protocol, so launcher.py reads it via
135
+ # ``getattr(backend, "fork_prescribes_new_session_id", False)`` — this
136
+ # default documents the fallback, it isn't inherited automatically.)
137
+ fork_prescribes_new_session_id: bool = False
138
+
139
+ def build_command(self, spec: LaunchSpec) -> List[str]: ...
140
+
141
+ def prepare_launch(self, spec: LaunchSpec) -> None:
142
+ """Side effects the CLI needs in place before the process starts.
143
+
144
+ Called once per launch/restart/revive/fork, after the binary check and
145
+ before the shell line is sent. Claude Code needs nothing here — its
146
+ telemetry rides on ``--settings`` — but opencode installs its
147
+ telemetry plugin into the project directory. Must be idempotent and
148
+ must never raise: a failure costs telemetry, not the launch.
149
+ """
150
+ ...
151
+
152
+ def env_prefix(self, spec: LaunchSpec) -> Dict[str, str]: ...
153
+
154
+ def resume_args(self, session_id: str, fork: bool) -> List[str]: ...
155
+
156
+ def graceful_exit_keys(self) -> List[KeyPress]: ...
157
+
158
+ def clear_conversation_keys(self) -> List[KeyPress]: ...
159
+
160
+ def approve_keys(self) -> List[KeyPress]: ...
161
+
162
+ def reject_keys(self) -> List[KeyPress]: ...
163
+
164
+ def startup_dialog_rules(self) -> List[DialogRule]: ...
165
+
166
+ def prompt_ready_chars(self) -> Set[str]: ...
167
+
168
+ def status_patterns(self) -> "StatusPatterns": ...
169
+
170
+ def make_stats_reader(self) -> "StatsReader": ...
171
+
172
+ def health_verdict(self, argv: str) -> Optional[Tuple[str, str]]:
173
+ """Doctor's "is observability wired up?" answer for a live process.
174
+
175
+ Returns ``(verdict, details)`` from the ``doctor.VERDICT_*``
176
+ vocabulary, given the agent process's full argv.
177
+
178
+ A backend whose telemetry leaves no trace on the command line may
179
+ also define an *optional* ``refine_health_verdict(session, verdict,
180
+ details) -> (verdict, details)``. ``doctor`` calls it via ``getattr``
181
+ with the session in hand — opencode uses it to check for its
182
+ telemetry plugin in the project directory.
183
+ """
184
+ ...
185
+
186
+
187
+ def supports(backend: AgentBackend, capability: BackendCapability) -> bool:
188
+ """True when ``backend`` declares ``capability``."""
189
+ return bool(backend.capabilities & capability)