overcode 0.4.2__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.
- overcode-0.5.1/MANIFEST.in +3 -0
- {overcode-0.4.2/src/overcode.egg-info → overcode-0.5.1}/PKG-INFO +16 -4
- {overcode-0.4.2 → overcode-0.5.1}/README.md +15 -3
- {overcode-0.4.2 → overcode-0.5.1}/pyproject.toml +10 -2
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/__init__.py +1 -1
- overcode-0.5.1/src/overcode/backends/__init__.py +170 -0
- overcode-0.5.1/src/overcode/backends/base.py +189 -0
- overcode-0.5.1/src/overcode/backends/claude_code.py +270 -0
- overcode-0.5.1/src/overcode/backends/codex.py +590 -0
- overcode-0.5.1/src/overcode/backends/codex_stats.py +508 -0
- overcode-0.5.1/src/overcode/backends/grok.py +857 -0
- overcode-0.5.1/src/overcode/backends/grok_stats.py +400 -0
- overcode-0.5.1/src/overcode/backends/opencode.py +719 -0
- overcode-0.5.1/src/overcode/backends/opencode_stats.py +641 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/bundled_skills.py +4 -4
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/claude_pid.py +1 -1
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/__init__.py +2 -2
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/_shared.py +1 -1
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/agent.py +118 -51
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/daemon.py +1 -1
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/doctor.py +65 -12
- overcode-0.5.1/src/overcode/cli/hooks.py +174 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/parallelism.py +2 -2
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/split.py +44 -5
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/config.py +74 -1
- overcode-0.5.1/src/overcode/daemon_claude_skill.md +105 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/daemon_logging.py +46 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/dependency_check.py +61 -9
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/doctor.py +91 -40
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/exceptions.py +15 -7
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/history_reader.py +289 -40
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/hook_handler.py +296 -2
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/hook_status_detector.py +62 -22
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/launcher.py +279 -230
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/monitor_daemon.py +131 -150
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/monitor_daemon_state.py +12 -5
- overcode-0.5.1/src/overcode/opencode_plugin/overcode-telemetry.js +481 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/pricing.py +32 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/process_resources.py +2 -2
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/session_manager.py +167 -35
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/settings.py +32 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/sister_poller.py +24 -0
- overcode-0.5.1/src/overcode/stats_reader.py +334 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/status_constants.py +1 -1
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/status_detector.py +34 -44
- overcode-0.5.1/src/overcode/status_detector_factory.py +211 -0
- overcode-0.5.1/src/overcode/status_history.py +626 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/status_patterns.py +233 -54
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/summary_columns.py +82 -14
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/supervisor_daemon.py +1 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/supervisor_daemon_core.py +10 -1
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui.py +92 -31
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_actions/input.py +2 -2
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_actions/session.py +70 -16
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_logic.py +5 -5
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/agent_select_modal.py +5 -5
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/daemon_panel.py +11 -11
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/help_overlay.py +1 -1
- overcode-0.5.1/src/overcode/tui_widgets/new_agent_defaults_modal.py +142 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/new_agent_modal.py +30 -13
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/session_summary.py +15 -5
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/tui_log_panel.py +8 -8
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/web_api.py +13 -7
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/web_control_api.py +67 -28
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/wrapper.py +95 -27
- {overcode-0.4.2 → overcode-0.5.1/src/overcode.egg-info}/PKG-INFO +16 -4
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode.egg-info/SOURCES.txt +11 -0
- {overcode-0.4.2 → overcode-0.5.1}/tests/test_e2e_multi_agent_jokes.py +6 -6
- overcode-0.4.2/MANIFEST.in +0 -3
- overcode-0.4.2/src/overcode/cli/hooks.py +0 -101
- overcode-0.4.2/src/overcode/daemon_claude_skill.md +0 -68
- overcode-0.4.2/src/overcode/status_detector_factory.py +0 -133
- overcode-0.4.2/src/overcode/status_history.py +0 -323
- overcode-0.4.2/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -98
- {overcode-0.4.2 → overcode-0.5.1}/LICENSE +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/setup.cfg +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/agent_scanner.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/claude_config.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/budget.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/config.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/focal.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/jobs.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/monitoring.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/perms.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/sister.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/skills.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/tags.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/cli/wrappers.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/data_export.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/duration.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/follow_mode.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/implementations.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/interfaces.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/job_launcher.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/job_manager.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/logging_config.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/mocks.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/notifier.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/pid_utils.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/presence_logger.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/protocols.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/sandbox_detect.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/sister_controller.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/ssh_provisioner.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/summary_groups.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/time_context.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tmux_manager.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui.tcss +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_actions/daemon.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_actions/navigation.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_actions/view.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_render.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/__init__.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/command_bar.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/job_summary.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/jump_modal.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/modal_base.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/passthru_config_modal.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/status_timeline.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/tui_widgets/tmux_config_modal.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/web/__init__.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/web_server.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode/web_templates.py +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.4.2 → overcode-0.5.1}/src/overcode.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: overcode
|
|
3
|
-
Version: 0.
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# overcode
|
|
2
2
|
|
|
3
|
-
A TUI supervisor for managing multiple Claude Code
|
|
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
|
|
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
|
|
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`
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "overcode"
|
|
7
|
-
version = "0.
|
|
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 = [
|
|
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
|
|
@@ -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)
|