overcode 0.2.1__tar.gz → 0.2.3__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.2.1/src/overcode.egg-info → overcode-0.2.3}/PKG-INFO +1 -1
- {overcode-0.2.1 → overcode-0.2.3}/pyproject.toml +1 -1
- overcode-0.2.3/src/overcode/agent_scanner.py +38 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/bundled_skills.py +7 -2
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/agent.py +165 -32
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/daemon.py +1 -1
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/config.py +42 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/history_reader.py +69 -74
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/launcher.py +57 -1
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/monitor_daemon.py +65 -21
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/monitor_daemon_state.py +13 -170
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/presence_logger.py +4 -4
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/session_manager.py +10 -1
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/settings.py +18 -18
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/sister_poller.py +3 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_patterns.py +55 -1
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/summary_columns.py +320 -66
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/summary_groups.py +7 -39
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui.py +361 -105
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui.tcss +43 -1
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/input.py +3 -3
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/session.py +28 -3
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/view.py +36 -7
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_logic.py +5 -4
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/__init__.py +4 -0
- overcode-0.2.3/src/overcode/tui_widgets/agent_select_modal.py +126 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/command_bar.py +207 -24
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/daemon_status_bar.py +39 -13
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/help_overlay.py +4 -0
- overcode-0.2.3/src/overcode/tui_widgets/new_agent_defaults_modal.py +131 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/session_summary.py +34 -33
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/status_timeline.py +1 -5
- overcode-0.2.3/src/overcode/tui_widgets/summary_config_modal.py +292 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_api.py +6 -0
- {overcode-0.2.1 → overcode-0.2.3/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/SOURCES.txt +3 -0
- overcode-0.2.1/src/overcode/tui_widgets/summary_config_modal.py +0 -164
- {overcode-0.2.1 → overcode-0.2.3}/LICENSE +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/MANIFEST.in +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/README.md +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/setup.cfg +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/claude_config.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/_shared.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/budget.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/config.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/monitoring.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/perms.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/sister.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/skills.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/data_export.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/dependency_check.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/exceptions.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/follow_mode.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/hook_handler.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/hook_status_detector.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/implementations.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/interfaces.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/logging_config.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/mocks.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/notifier.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/pid_utils.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/protocols.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/sister_controller.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_constants.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_detector.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_detector_factory.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_history.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/supervisor_daemon.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/supervisor_daemon_core.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/time_context.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tmux_manager.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/daemon.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/navigation.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_render.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_control_api.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_server.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_templates.py +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.2.1 → overcode-0.2.3}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scan for Claude Code agent definitions (.md files in .claude/agents/).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def scan_agents(directory: str) -> List[str]:
|
|
10
|
+
"""Scan for available Claude agent definitions.
|
|
11
|
+
|
|
12
|
+
Checks both project-level (.claude/agents/) and user-level (~/.claude/agents/)
|
|
13
|
+
directories for .md files. Returns deduplicated, sorted list of agent names
|
|
14
|
+
(filenames without .md extension).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
directory: Project directory to scan for project-level agents.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Sorted list of agent names. Empty list if none found.
|
|
21
|
+
"""
|
|
22
|
+
names: set = set()
|
|
23
|
+
|
|
24
|
+
# Project-level agents
|
|
25
|
+
project_dir = Path(directory) / ".claude" / "agents"
|
|
26
|
+
if project_dir.is_dir():
|
|
27
|
+
for f in project_dir.iterdir():
|
|
28
|
+
if f.is_file() and f.suffix == ".md":
|
|
29
|
+
names.add(f.stem)
|
|
30
|
+
|
|
31
|
+
# User-level agents
|
|
32
|
+
user_dir = Path.home() / ".claude" / "agents"
|
|
33
|
+
if user_dir.is_dir():
|
|
34
|
+
for f in user_dir.iterdir():
|
|
35
|
+
if f.is_file() and f.suffix == ".md":
|
|
36
|
+
names.add(f.stem)
|
|
37
|
+
|
|
38
|
+
return sorted(names)
|
|
@@ -32,6 +32,7 @@ overcode launch -n <name> [-d <path>] [-p "<prompt>"] [--follow] [--bypass-permi
|
|
|
32
32
|
overcode launch -n <name> --follow --oversight-timeout 5m -p "... When done: overcode report --status success"
|
|
33
33
|
overcode launch -n <name> --allowed-tools "Read,Glob,Grep" --skip-permissions
|
|
34
34
|
overcode launch -n <name> --claude-arg "--model haiku" --claude-arg "--effort low"
|
|
35
|
+
overcode launch -n <name> --budget 2.00 -p "..." # Set cost budget (auto-deducted from parent)
|
|
35
36
|
|
|
36
37
|
# Monitor
|
|
37
38
|
overcode list [name] [--show-done]
|
|
@@ -127,8 +128,8 @@ overcode launch --name fix-auth-bug -d ~/project --follow --bypass-permissions \
|
|
|
127
128
|
## Parallel (Non-Blocking)
|
|
128
129
|
|
|
129
130
|
```bash
|
|
130
|
-
overcode launch -n refactor-api -d ~/project -p "Refactor REST API. When done: overcode report --status success" --bypass-permissions
|
|
131
|
-
overcode launch -n write-tests -d ~/project -p "Write auth tests. When done: overcode report --status success" --bypass-permissions
|
|
131
|
+
overcode launch -n refactor-api -d ~/project --budget 3.00 -p "Refactor REST API. When done: overcode report --status success" --bypass-permissions
|
|
132
|
+
overcode launch -n write-tests -d ~/project --budget 2.00 -p "Write auth tests. When done: overcode report --status success" --bypass-permissions
|
|
132
133
|
|
|
133
134
|
overcode list # Monitor progress
|
|
134
135
|
overcode show refactor-api -n 100 # Read output
|
|
@@ -163,6 +164,10 @@ Children must call `overcode report --status success|failure [--reason "..."]` w
|
|
|
163
164
|
## Budget Control
|
|
164
165
|
|
|
165
166
|
```bash
|
|
167
|
+
# Preferred: set budget at launch (auto-deducted from parent if parent has budget)
|
|
168
|
+
overcode launch -n child-agent -d ~/project --bypass-permissions --budget 2.00 -p "..."
|
|
169
|
+
|
|
170
|
+
# Manual budget management
|
|
166
171
|
overcode budget transfer my-agent child-agent 2.00 # Transfer from your budget
|
|
167
172
|
overcode budget set child-agent 3.00 # Set directly
|
|
168
173
|
```
|
|
@@ -59,11 +59,73 @@ def launch(
|
|
|
59
59
|
Optional[List[str]],
|
|
60
60
|
typer.Option("--claude-arg", help="Extra Claude CLI flag (repeatable, e.g. '--model haiku')"),
|
|
61
61
|
] = None,
|
|
62
|
+
budget: Annotated[
|
|
63
|
+
Optional[float],
|
|
64
|
+
typer.Option("--budget", "-b", help="Cost budget in USD (deducted from parent if parent has budget)"),
|
|
65
|
+
] = None,
|
|
66
|
+
agent: Annotated[
|
|
67
|
+
Optional[str],
|
|
68
|
+
typer.Option("--agent", "-a", help="Claude agent to run as (from .claude/agents/)"),
|
|
69
|
+
] = None,
|
|
70
|
+
teams: Annotated[
|
|
71
|
+
bool,
|
|
72
|
+
typer.Option("--teams", help="Enable Claude Code agent teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)"),
|
|
73
|
+
] = False,
|
|
74
|
+
sister: Annotated[
|
|
75
|
+
Optional[str],
|
|
76
|
+
typer.Option("--sister", "-S", help="Launch on a remote sister machine (by name from config)"),
|
|
77
|
+
] = None,
|
|
62
78
|
session: SessionOption = "agents",
|
|
63
79
|
):
|
|
64
80
|
"""Launch a new Claude agent."""
|
|
65
81
|
import os
|
|
66
82
|
|
|
83
|
+
# Remote launch via sister
|
|
84
|
+
if sister:
|
|
85
|
+
from ..config import get_sister_by_name
|
|
86
|
+
from ..sister_controller import SisterController
|
|
87
|
+
|
|
88
|
+
sister_config = get_sister_by_name(sister)
|
|
89
|
+
if not sister_config:
|
|
90
|
+
from ..config import get_sisters_config
|
|
91
|
+
available = [s["name"] for s in get_sisters_config()]
|
|
92
|
+
if available:
|
|
93
|
+
rprint(f"[red]Error: Sister '{sister}' not found. Available: {', '.join(available)}[/red]")
|
|
94
|
+
else:
|
|
95
|
+
rprint(f"[red]Error: No sisters configured. Add sisters to ~/.overcode/config.yaml[/red]")
|
|
96
|
+
raise typer.Exit(code=1)
|
|
97
|
+
|
|
98
|
+
remote_dir = directory if directory else "."
|
|
99
|
+
if not directory:
|
|
100
|
+
rprint("[yellow]Warning:[/yellow] No --directory given; remote agent will use '.'")
|
|
101
|
+
|
|
102
|
+
# Map permission flags to permissions string
|
|
103
|
+
if bypass_permissions:
|
|
104
|
+
permissions = "bypass"
|
|
105
|
+
elif skip_permissions:
|
|
106
|
+
permissions = "skip"
|
|
107
|
+
else:
|
|
108
|
+
permissions = "normal"
|
|
109
|
+
|
|
110
|
+
controller = SisterController()
|
|
111
|
+
result = controller.launch_agent(
|
|
112
|
+
sister_url=sister_config["url"],
|
|
113
|
+
api_key=sister_config.get("api_key", ""),
|
|
114
|
+
directory=remote_dir,
|
|
115
|
+
name=name,
|
|
116
|
+
prompt=prompt,
|
|
117
|
+
permissions=permissions,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if result.ok:
|
|
121
|
+
rprint(f"\n[green]✓[/green] Remote agent '[bold]{name}[/bold]' launched on [bold]{sister}[/bold]")
|
|
122
|
+
if prompt:
|
|
123
|
+
rprint(" Initial prompt sent")
|
|
124
|
+
else:
|
|
125
|
+
rprint(f"[red]Error: Remote launch failed: {result.error}[/red]")
|
|
126
|
+
raise typer.Exit(code=1)
|
|
127
|
+
return
|
|
128
|
+
|
|
67
129
|
# Parse oversight policy
|
|
68
130
|
oversight_policy = "wait"
|
|
69
131
|
oversight_timeout_seconds = 0.0
|
|
@@ -104,6 +166,9 @@ def launch(
|
|
|
104
166
|
parent_name=parent,
|
|
105
167
|
allowed_tools=allowed_tools,
|
|
106
168
|
extra_claude_args=claude_args,
|
|
169
|
+
agent_teams=teams,
|
|
170
|
+
budget_usd=budget,
|
|
171
|
+
claude_agent=agent,
|
|
107
172
|
)
|
|
108
173
|
|
|
109
174
|
if result:
|
|
@@ -116,6 +181,12 @@ def launch(
|
|
|
116
181
|
rprint(f" Allowed tools: {allowed_tools}")
|
|
117
182
|
if claude_args:
|
|
118
183
|
rprint(f" Extra Claude args: {' '.join(claude_args)}")
|
|
184
|
+
if agent:
|
|
185
|
+
rprint(f" Agent: {agent}")
|
|
186
|
+
if teams:
|
|
187
|
+
rprint(" Agent teams: enabled")
|
|
188
|
+
if budget is not None and budget > 0:
|
|
189
|
+
rprint(f" Budget: ${budget:.2f}")
|
|
119
190
|
|
|
120
191
|
# Store oversight policy on session
|
|
121
192
|
if oversight_policy != "wait" or oversight_timeout_seconds > 0:
|
|
@@ -155,6 +226,15 @@ def list_agents(
|
|
|
155
226
|
sisters: Annotated[
|
|
156
227
|
bool, typer.Option("--sisters", help="Include sister (remote) agents")
|
|
157
228
|
] = False,
|
|
229
|
+
low: Annotated[
|
|
230
|
+
bool, typer.Option("--low", help="Low detail (identity only)")
|
|
231
|
+
] = False,
|
|
232
|
+
med: Annotated[
|
|
233
|
+
bool, typer.Option("--med", help="Medium detail")
|
|
234
|
+
] = False,
|
|
235
|
+
full: Annotated[
|
|
236
|
+
bool, typer.Option("--full", help="Full detail (default)")
|
|
237
|
+
] = False,
|
|
158
238
|
session: SessionOption = "agents",
|
|
159
239
|
):
|
|
160
240
|
"""List running agents with status.
|
|
@@ -170,19 +250,30 @@ def list_agents(
|
|
|
170
250
|
get_status_symbol, get_git_diff_stats,
|
|
171
251
|
)
|
|
172
252
|
from ..monitor_daemon_state import get_monitor_daemon_state
|
|
173
|
-
from ..summary_columns import build_cli_context,
|
|
253
|
+
from ..summary_columns import build_cli_context, render_summary_cells, align_summary_rows
|
|
174
254
|
from ..tui_logic import compute_tree_metadata, sort_sessions_by_tree
|
|
175
|
-
from rich.text import Text
|
|
176
255
|
from rich.console import Console
|
|
177
256
|
|
|
257
|
+
# Resolve detail level (mutually exclusive flags, default full)
|
|
258
|
+
if low:
|
|
259
|
+
detail = "low"
|
|
260
|
+
elif med:
|
|
261
|
+
detail = "med"
|
|
262
|
+
else:
|
|
263
|
+
detail = "full"
|
|
264
|
+
|
|
178
265
|
launcher = ClaudeLauncher(session)
|
|
179
266
|
sessions = launcher.list_sessions()
|
|
180
267
|
|
|
181
268
|
# Merge sister sessions if --sisters flag
|
|
269
|
+
has_sisters = False
|
|
270
|
+
local_hostname = ""
|
|
182
271
|
if sisters:
|
|
183
272
|
from ..sister_poller import SisterPoller
|
|
184
273
|
poller = SisterPoller()
|
|
185
|
-
|
|
274
|
+
has_sisters = poller.has_sisters
|
|
275
|
+
local_hostname = poller.local_hostname
|
|
276
|
+
if has_sisters:
|
|
186
277
|
remote_sessions = poller.poll_all()
|
|
187
278
|
sessions = sessions + remote_sessions
|
|
188
279
|
|
|
@@ -212,23 +303,48 @@ def list_agents(
|
|
|
212
303
|
sessions = sort_sessions_by_tree(sessions)
|
|
213
304
|
tree_meta = compute_tree_metadata(sessions)
|
|
214
305
|
|
|
215
|
-
#
|
|
216
|
-
list_columns = {
|
|
217
|
-
"status_symbol", "time_in_state", "sleep_countdown", "agent_name",
|
|
218
|
-
"git_diff",
|
|
219
|
-
"uptime", "running_time", "stalled_time", "sleep_time",
|
|
220
|
-
"token_count", "cost", "budget", "context_usage",
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
# Pre-compute: any agent with budget, max name width
|
|
306
|
+
# Pre-compute: any agent with budget, column alignment widths
|
|
224
307
|
any_has_budget = any(s.cost_budget_usd > 0 for s in sessions)
|
|
225
308
|
max_name_len = max(len(s.name) for s in sessions)
|
|
226
309
|
name_width = min(max(max_name_len, 10), 20)
|
|
310
|
+
max_repo_width = max((len(s.repo_name or "n/a") for s in sessions), default=5)
|
|
311
|
+
max_branch_width = max((len(s.branch or "n/a") for s in sessions), default=5)
|
|
312
|
+
all_names_match_repos = all(
|
|
313
|
+
s.name == s.repo_name for s in sessions if s.repo_name
|
|
314
|
+
)
|
|
227
315
|
|
|
228
316
|
# Prefer daemon state for status/activity (single source of truth)
|
|
229
317
|
daemon_state = get_monitor_daemon_state(session)
|
|
230
318
|
use_daemon = daemon_state is not None and not daemon_state.is_stale()
|
|
231
319
|
|
|
320
|
+
# Compute cross-session flags from daemon state
|
|
321
|
+
any_has_oversight_timeout = False
|
|
322
|
+
any_has_pr = False
|
|
323
|
+
subtree_costs = {}
|
|
324
|
+
if use_daemon:
|
|
325
|
+
any_has_oversight_timeout = any(
|
|
326
|
+
ds.oversight_timeout_seconds > 0
|
|
327
|
+
for ds in daemon_state.sessions
|
|
328
|
+
)
|
|
329
|
+
for ds in daemon_state.sessions:
|
|
330
|
+
if ds.subtree_cost_usd > 0:
|
|
331
|
+
subtree_costs[ds.session_id] = ds.subtree_cost_usd
|
|
332
|
+
# Also extract from remote sessions via forwarded daemon_state
|
|
333
|
+
for s in sessions:
|
|
334
|
+
rds = getattr(s, 'remote_daemon_state', None)
|
|
335
|
+
if rds and rds.get('subtree_cost_usd', 0) > 0:
|
|
336
|
+
subtree_costs[s.id] = rds['subtree_cost_usd']
|
|
337
|
+
if not use_daemon:
|
|
338
|
+
any_has_oversight_timeout = any(
|
|
339
|
+
getattr(s, 'oversight_timeout_seconds', 0) > 0
|
|
340
|
+
for s in sessions
|
|
341
|
+
)
|
|
342
|
+
any_has_subtree_cost = bool(subtree_costs)
|
|
343
|
+
any_has_pr = any(
|
|
344
|
+
getattr(s, 'pr_number', None) is not None
|
|
345
|
+
for s in sessions
|
|
346
|
+
)
|
|
347
|
+
|
|
232
348
|
# Only create detector as fallback when daemon isn't running
|
|
233
349
|
detector = None
|
|
234
350
|
if not use_daemon:
|
|
@@ -288,24 +404,45 @@ def list_agents(
|
|
|
288
404
|
# Compute cross-session flags
|
|
289
405
|
any_is_sleeping = any(st == "busy_sleeping" for _, st, _, _, _ in session_data)
|
|
290
406
|
|
|
291
|
-
# Second pass:
|
|
407
|
+
# Second pass: build contexts and collect cells for auto-alignment
|
|
408
|
+
all_cells = []
|
|
409
|
+
activities = []
|
|
292
410
|
for sess, status, activity, claude_stats, git_diff in session_data:
|
|
293
411
|
meta = tree_meta.get(sess.id)
|
|
294
412
|
child_count = meta.child_count if meta else 0
|
|
413
|
+
|
|
414
|
+
# Get per-session daemon fields
|
|
415
|
+
oversight_deadline = None
|
|
416
|
+
if use_daemon:
|
|
417
|
+
ds = daemon_state.get_session_by_name(sess.name)
|
|
418
|
+
if ds:
|
|
419
|
+
oversight_deadline = ds.oversight_deadline
|
|
420
|
+
|
|
421
|
+
_, status_color = get_status_symbol(status)
|
|
295
422
|
ctx = build_cli_context(
|
|
296
423
|
session=sess, stats=sess.stats,
|
|
297
424
|
claude_stats=claude_stats, git_diff_stats=git_diff,
|
|
298
425
|
status=status, bg_bash_count=0, live_sub_count=0,
|
|
299
426
|
any_has_budget=any_has_budget, child_count=child_count,
|
|
300
427
|
any_is_sleeping=any_is_sleeping,
|
|
428
|
+
any_has_oversight_timeout=any_has_oversight_timeout,
|
|
429
|
+
oversight_deadline=oversight_deadline,
|
|
430
|
+
pr_number=getattr(sess, 'pr_number', None),
|
|
431
|
+
any_has_pr=any_has_pr,
|
|
432
|
+
monochrome=False,
|
|
433
|
+
summary_detail=detail,
|
|
434
|
+
has_sisters=has_sisters,
|
|
435
|
+
local_hostname=local_hostname,
|
|
436
|
+
max_name_width=name_width,
|
|
437
|
+
max_repo_width=max_repo_width,
|
|
438
|
+
max_branch_width=max_branch_width,
|
|
439
|
+
all_names_match_repos=all_names_match_repos,
|
|
440
|
+
subtree_cost_usd=subtree_costs.get(sess.id, 0.0),
|
|
441
|
+
any_has_subtree_cost=any_has_subtree_cost,
|
|
301
442
|
)
|
|
302
|
-
|
|
303
|
-
# Enable colors and set detail level for list view
|
|
304
|
-
ctx.monochrome = False
|
|
305
|
-
_, status_color = get_status_symbol(status)
|
|
306
443
|
ctx.status_color = f"bold {status_color}"
|
|
307
|
-
ctx.summary_detail = "med"
|
|
308
444
|
ctx.show_cost = cost
|
|
445
|
+
ctx.is_list_mode = True
|
|
309
446
|
|
|
310
447
|
# Handle tree indentation (#244) using compute_tree_metadata
|
|
311
448
|
depth = meta.depth if meta else 0
|
|
@@ -313,23 +450,15 @@ def list_agents(
|
|
|
313
450
|
available = name_width - len(indent)
|
|
314
451
|
ctx.display_name = (indent + sess.name[:available]).ljust(name_width)
|
|
315
452
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
continue
|
|
323
|
-
segments = col.render(ctx)
|
|
324
|
-
if segments:
|
|
325
|
-
for text, style in segments:
|
|
326
|
-
line.append(text, style=style)
|
|
327
|
-
|
|
328
|
-
# Append activity (truncate to fit terminal width)
|
|
453
|
+
all_cells.append(render_summary_cells(ctx))
|
|
454
|
+
activities.append(activity)
|
|
455
|
+
|
|
456
|
+
# Auto-align columns across all rows, then append activity
|
|
457
|
+
aligned_lines = align_summary_rows(all_cells)
|
|
458
|
+
for line, activity in zip(aligned_lines, activities):
|
|
329
459
|
line.append(" │ ", style="dim")
|
|
330
460
|
line.append(activity)
|
|
331
461
|
line.truncate(console.width, pad=False)
|
|
332
|
-
|
|
333
462
|
console.print(line, no_wrap=True)
|
|
334
463
|
|
|
335
464
|
if terminated_count > 0:
|
|
@@ -756,6 +885,10 @@ def show(
|
|
|
756
885
|
print(f"{'Tools:':<{label_width + 1}} {sess.allowed_tools}")
|
|
757
886
|
if sess.extra_claude_args:
|
|
758
887
|
print(f"{'Claude args:':<{label_width + 1}} {' '.join(sess.extra_claude_args)}")
|
|
888
|
+
if sess.claude_agent:
|
|
889
|
+
print(f"{'Agent:':<{label_width + 1}} {sess.claude_agent}")
|
|
890
|
+
if sess.agent_teams:
|
|
891
|
+
print(f"{'Teams:':<{label_width + 1}} enabled")
|
|
759
892
|
|
|
760
893
|
print()
|
|
761
894
|
|
|
@@ -35,7 +35,7 @@ def monitor_daemon_start(
|
|
|
35
35
|
- Status detection (running, waiting, etc.)
|
|
36
36
|
- Time accumulation (green_time, non_green_time)
|
|
37
37
|
- Claude Code stats (tokens, interactions)
|
|
38
|
-
- User presence state
|
|
38
|
+
- User presence state
|
|
39
39
|
"""
|
|
40
40
|
from ..monitor_daemon import MonitorDaemon, is_monitor_daemon_running, get_monitor_daemon_pid
|
|
41
41
|
|
|
@@ -264,6 +264,36 @@ def get_web_allow_control() -> bool:
|
|
|
264
264
|
return bool(web.get("allow_control", False))
|
|
265
265
|
|
|
266
266
|
|
|
267
|
+
def get_new_agent_defaults() -> dict:
|
|
268
|
+
"""Get new-agent default settings from config.
|
|
269
|
+
|
|
270
|
+
Config format in ~/.overcode/config.yaml:
|
|
271
|
+
new_agent_defaults:
|
|
272
|
+
bypass_permissions: false
|
|
273
|
+
agent_teams: false
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Dict with bypass_permissions (bool) and agent_teams (bool).
|
|
277
|
+
"""
|
|
278
|
+
config = load_config()
|
|
279
|
+
defaults = config.get("new_agent_defaults", {})
|
|
280
|
+
return {
|
|
281
|
+
"bypass_permissions": bool(defaults.get("bypass_permissions", False)),
|
|
282
|
+
"agent_teams": bool(defaults.get("agent_teams", False)),
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def save_new_agent_defaults(defaults: dict) -> None:
|
|
287
|
+
"""Save new-agent default settings to config.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
defaults: Dict with bypass_permissions and agent_teams booleans.
|
|
291
|
+
"""
|
|
292
|
+
config = load_config()
|
|
293
|
+
config["new_agent_defaults"] = defaults
|
|
294
|
+
save_config(config)
|
|
295
|
+
|
|
296
|
+
|
|
267
297
|
def get_sisters_config() -> List[dict]:
|
|
268
298
|
"""Get sister instance configuration for cross-machine monitoring.
|
|
269
299
|
|
|
@@ -298,3 +328,15 @@ def get_sisters_config() -> List[dict]:
|
|
|
298
328
|
result.append(entry)
|
|
299
329
|
|
|
300
330
|
return result
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def get_sister_by_name(name: str) -> Optional[dict]:
|
|
334
|
+
"""Look up a sister config by name.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Sister config dict (name, url, optional api_key) or None if not found.
|
|
338
|
+
"""
|
|
339
|
+
for sister in get_sisters_config():
|
|
340
|
+
if sister["name"] == name:
|
|
341
|
+
return sister
|
|
342
|
+
return None
|
|
@@ -404,31 +404,35 @@ def get_session_file_path(
|
|
|
404
404
|
return projects_path / encoded / f"{session_id}.jsonl"
|
|
405
405
|
|
|
406
406
|
|
|
407
|
-
def
|
|
407
|
+
def read_session_file_stats(
|
|
408
408
|
session_file: Path,
|
|
409
|
-
since: Optional[datetime] = None
|
|
410
|
-
) -> dict:
|
|
411
|
-
"""Read token usage from a
|
|
409
|
+
since: Optional[datetime] = None,
|
|
410
|
+
) -> Tuple[dict, List[float]]:
|
|
411
|
+
"""Read token usage and work times from a session file in a single pass.
|
|
412
|
+
|
|
413
|
+
Combines the work of read_token_usage_from_session_file and
|
|
414
|
+
read_work_times_from_session_file so the file is only read once.
|
|
412
415
|
|
|
413
416
|
Args:
|
|
414
417
|
session_file: Path to the session JSONL file
|
|
415
|
-
since: Only count
|
|
418
|
+
since: Only count data from messages after this time
|
|
416
419
|
|
|
417
420
|
Returns:
|
|
418
|
-
|
|
419
|
-
and current_context_tokens (most recent input_tokens value)
|
|
421
|
+
(token_usage_dict, work_times_list)
|
|
420
422
|
"""
|
|
421
423
|
totals = {
|
|
422
424
|
"input_tokens": 0,
|
|
423
425
|
"output_tokens": 0,
|
|
424
426
|
"cache_creation_tokens": 0,
|
|
425
427
|
"cache_read_tokens": 0,
|
|
426
|
-
"current_context_tokens": 0,
|
|
427
|
-
"model": None,
|
|
428
|
+
"current_context_tokens": 0,
|
|
429
|
+
"model": None,
|
|
428
430
|
}
|
|
429
431
|
|
|
430
432
|
if not session_file.exists():
|
|
431
|
-
return totals
|
|
433
|
+
return totals, []
|
|
434
|
+
|
|
435
|
+
user_prompt_times: List[datetime] = []
|
|
432
436
|
|
|
433
437
|
try:
|
|
434
438
|
with open(session_file, 'r') as f:
|
|
@@ -438,14 +442,14 @@ def read_token_usage_from_session_file(
|
|
|
438
442
|
continue
|
|
439
443
|
try:
|
|
440
444
|
data = json.loads(line)
|
|
441
|
-
|
|
442
|
-
|
|
445
|
+
msg_type = data.get("type")
|
|
446
|
+
|
|
447
|
+
if msg_type == "assistant":
|
|
443
448
|
# Check timestamp if filtering by time
|
|
444
449
|
if since:
|
|
445
450
|
ts_str = data.get("timestamp")
|
|
446
451
|
if ts_str:
|
|
447
452
|
try:
|
|
448
|
-
# Parse ISO timestamp (e.g., "2026-01-02T06:56:01.975Z")
|
|
449
453
|
msg_time = datetime.fromisoformat(
|
|
450
454
|
ts_str.replace("Z", "+00:00")
|
|
451
455
|
).replace(tzinfo=None)
|
|
@@ -468,15 +472,62 @@ def read_token_usage_from_session_file(
|
|
|
468
472
|
"cache_creation_input_tokens", 0
|
|
469
473
|
)
|
|
470
474
|
totals["cache_read_tokens"] += cache_read
|
|
471
|
-
# Track most recent context size (input + cached context)
|
|
472
475
|
context_size = input_tokens + cache_read
|
|
473
476
|
if context_size > 0:
|
|
474
477
|
totals["current_context_tokens"] = context_size
|
|
478
|
+
|
|
479
|
+
elif msg_type == "user":
|
|
480
|
+
# Check if this is an actual user prompt (not a tool result)
|
|
481
|
+
message = data.get("message", {})
|
|
482
|
+
content = message.get("content", "")
|
|
483
|
+
if isinstance(content, list):
|
|
484
|
+
if content and content[0].get("type") == "tool_result":
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
ts_str = data.get("timestamp")
|
|
488
|
+
if not ts_str:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
msg_time = datetime.fromisoformat(
|
|
493
|
+
ts_str.replace("Z", "+00:00")
|
|
494
|
+
).replace(tzinfo=None)
|
|
495
|
+
if since and msg_time < since:
|
|
496
|
+
continue
|
|
497
|
+
user_prompt_times.append(msg_time)
|
|
498
|
+
except (ValueError, TypeError):
|
|
499
|
+
continue
|
|
500
|
+
|
|
475
501
|
except (json.JSONDecodeError, KeyError, TypeError):
|
|
476
502
|
continue
|
|
477
503
|
except IOError:
|
|
478
|
-
|
|
504
|
+
return totals, []
|
|
505
|
+
|
|
506
|
+
# Calculate durations between consecutive prompts
|
|
507
|
+
work_times = []
|
|
508
|
+
for i in range(1, len(user_prompt_times)):
|
|
509
|
+
duration = (user_prompt_times[i] - user_prompt_times[i - 1]).total_seconds()
|
|
510
|
+
if duration > 0:
|
|
511
|
+
work_times.append(duration)
|
|
512
|
+
|
|
513
|
+
return totals, work_times
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def read_token_usage_from_session_file(
|
|
517
|
+
session_file: Path,
|
|
518
|
+
since: Optional[datetime] = None
|
|
519
|
+
) -> dict:
|
|
520
|
+
"""Read token usage from a Claude Code session JSONL file.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
session_file: Path to the session JSONL file
|
|
524
|
+
since: Only count tokens from messages after this time
|
|
479
525
|
|
|
526
|
+
Returns:
|
|
527
|
+
Dict with input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
|
|
528
|
+
and current_context_tokens (most recent input_tokens value)
|
|
529
|
+
"""
|
|
530
|
+
totals, _ = read_session_file_stats(session_file, since)
|
|
480
531
|
return totals
|
|
481
532
|
|
|
482
533
|
|
|
@@ -498,62 +549,7 @@ def read_work_times_from_session_file(
|
|
|
498
549
|
Returns:
|
|
499
550
|
List of work times in seconds
|
|
500
551
|
"""
|
|
501
|
-
|
|
502
|
-
return []
|
|
503
|
-
|
|
504
|
-
user_prompt_times: List[datetime] = []
|
|
505
|
-
|
|
506
|
-
try:
|
|
507
|
-
with open(session_file, 'r') as f:
|
|
508
|
-
for line in f:
|
|
509
|
-
line = line.strip()
|
|
510
|
-
if not line:
|
|
511
|
-
continue
|
|
512
|
-
try:
|
|
513
|
-
data = json.loads(line)
|
|
514
|
-
if data.get("type") != "user":
|
|
515
|
-
continue
|
|
516
|
-
|
|
517
|
-
# Check if this is an actual user prompt (not a tool result)
|
|
518
|
-
message = data.get("message", {})
|
|
519
|
-
content = message.get("content", "")
|
|
520
|
-
|
|
521
|
-
# Tool results have content as a list with tool_result type
|
|
522
|
-
if isinstance(content, list):
|
|
523
|
-
# Check if it's a tool result
|
|
524
|
-
if content and content[0].get("type") == "tool_result":
|
|
525
|
-
continue
|
|
526
|
-
|
|
527
|
-
# Parse timestamp
|
|
528
|
-
ts_str = data.get("timestamp")
|
|
529
|
-
if not ts_str:
|
|
530
|
-
continue
|
|
531
|
-
|
|
532
|
-
try:
|
|
533
|
-
msg_time = datetime.fromisoformat(
|
|
534
|
-
ts_str.replace("Z", "+00:00")
|
|
535
|
-
).replace(tzinfo=None)
|
|
536
|
-
|
|
537
|
-
# Filter by since time
|
|
538
|
-
if since and msg_time < since:
|
|
539
|
-
continue
|
|
540
|
-
|
|
541
|
-
user_prompt_times.append(msg_time)
|
|
542
|
-
except (ValueError, TypeError):
|
|
543
|
-
continue
|
|
544
|
-
|
|
545
|
-
except (json.JSONDecodeError, KeyError, TypeError):
|
|
546
|
-
continue
|
|
547
|
-
except IOError:
|
|
548
|
-
return []
|
|
549
|
-
|
|
550
|
-
# Calculate durations between consecutive prompts
|
|
551
|
-
work_times = []
|
|
552
|
-
for i in range(1, len(user_prompt_times)):
|
|
553
|
-
duration = (user_prompt_times[i] - user_prompt_times[i - 1]).total_seconds()
|
|
554
|
-
if duration > 0:
|
|
555
|
-
work_times.append(duration)
|
|
556
|
-
|
|
552
|
+
_, work_times = read_session_file_stats(session_file, since)
|
|
557
553
|
return work_times
|
|
558
554
|
|
|
559
555
|
|
|
@@ -625,7 +621,7 @@ def get_session_stats(
|
|
|
625
621
|
session_file = get_session_file_path(
|
|
626
622
|
session.start_directory, sid, projects_path
|
|
627
623
|
)
|
|
628
|
-
usage =
|
|
624
|
+
usage, work_times = read_session_file_stats(session_file, since=session_start)
|
|
629
625
|
total_input += usage["input_tokens"]
|
|
630
626
|
total_output += usage["output_tokens"]
|
|
631
627
|
total_cache_creation += usage["cache_creation_tokens"]
|
|
@@ -644,7 +640,6 @@ def get_session_stats(
|
|
|
644
640
|
detected_model = usage["model"]
|
|
645
641
|
|
|
646
642
|
# Collect work times from this session file
|
|
647
|
-
work_times = read_work_times_from_session_file(session_file, since=session_start)
|
|
648
643
|
all_work_times.extend(work_times)
|
|
649
644
|
|
|
650
645
|
# Check for subagent files in {sessionId}/subagents/
|
|
@@ -655,7 +650,7 @@ def get_session_stats(
|
|
|
655
650
|
subagent_count += 1
|
|
656
651
|
if now - subagent_file.stat().st_mtime < 30:
|
|
657
652
|
live_subagent_count += 1
|
|
658
|
-
sub_usage =
|
|
653
|
+
sub_usage, _ = read_session_file_stats(
|
|
659
654
|
subagent_file, since=session_start
|
|
660
655
|
)
|
|
661
656
|
total_input += sub_usage["input_tokens"]
|