overcode 0.2.1__tar.gz → 0.2.2__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.2}/PKG-INFO +1 -1
- {overcode-0.2.1 → overcode-0.2.2}/pyproject.toml +1 -1
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/agent.py +137 -32
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/config.py +42 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/history_reader.py +69 -74
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/launcher.py +37 -1
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/monitor_daemon.py +10 -3
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/monitor_daemon_state.py +10 -168
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/session_manager.py +5 -1
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/settings.py +3 -3
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/status_patterns.py +55 -1
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/summary_columns.py +171 -28
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/summary_groups.py +1 -1
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui.py +213 -71
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui.tcss +16 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_actions/input.py +3 -3
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_actions/session.py +28 -3
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_actions/view.py +20 -4
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_logic.py +5 -4
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/__init__.py +2 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/command_bar.py +181 -24
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/daemon_status_bar.py +39 -13
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/help_overlay.py +4 -0
- overcode-0.2.2/src/overcode/tui_widgets/new_agent_defaults_modal.py +131 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/session_summary.py +26 -17
- {overcode-0.2.1 → overcode-0.2.2/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode.egg-info/SOURCES.txt +1 -0
- {overcode-0.2.1 → overcode-0.2.2}/LICENSE +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/MANIFEST.in +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/README.md +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/setup.cfg +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/bundled_skills.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/claude_config.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/_shared.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/budget.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/config.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/daemon.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/monitoring.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/perms.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/sister.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/cli/skills.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/data_export.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/dependency_check.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/exceptions.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/follow_mode.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/hook_handler.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/hook_status_detector.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/implementations.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/interfaces.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/logging_config.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/mocks.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/notifier.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/pid_utils.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/presence_logger.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/protocols.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/sister_controller.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/sister_poller.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/status_constants.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/status_detector.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/status_detector_factory.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/status_history.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/supervisor_daemon.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/supervisor_daemon_core.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/time_context.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tmux_manager.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_actions/daemon.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_actions/navigation.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_render.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/status_timeline.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/web/__init__.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/web_api.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/web_control_api.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/web_server.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode/web_templates.py +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.2.1 → overcode-0.2.2}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -59,11 +59,65 @@ 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
|
+
teams: Annotated[
|
|
63
|
+
bool,
|
|
64
|
+
typer.Option("--teams", help="Enable Claude Code agent teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)"),
|
|
65
|
+
] = False,
|
|
66
|
+
sister: Annotated[
|
|
67
|
+
Optional[str],
|
|
68
|
+
typer.Option("--sister", "-S", help="Launch on a remote sister machine (by name from config)"),
|
|
69
|
+
] = None,
|
|
62
70
|
session: SessionOption = "agents",
|
|
63
71
|
):
|
|
64
72
|
"""Launch a new Claude agent."""
|
|
65
73
|
import os
|
|
66
74
|
|
|
75
|
+
# Remote launch via sister
|
|
76
|
+
if sister:
|
|
77
|
+
from ..config import get_sister_by_name
|
|
78
|
+
from ..sister_controller import SisterController
|
|
79
|
+
|
|
80
|
+
sister_config = get_sister_by_name(sister)
|
|
81
|
+
if not sister_config:
|
|
82
|
+
from ..config import get_sisters_config
|
|
83
|
+
available = [s["name"] for s in get_sisters_config()]
|
|
84
|
+
if available:
|
|
85
|
+
rprint(f"[red]Error: Sister '{sister}' not found. Available: {', '.join(available)}[/red]")
|
|
86
|
+
else:
|
|
87
|
+
rprint(f"[red]Error: No sisters configured. Add sisters to ~/.overcode/config.yaml[/red]")
|
|
88
|
+
raise typer.Exit(code=1)
|
|
89
|
+
|
|
90
|
+
remote_dir = directory if directory else "."
|
|
91
|
+
if not directory:
|
|
92
|
+
rprint("[yellow]Warning:[/yellow] No --directory given; remote agent will use '.'")
|
|
93
|
+
|
|
94
|
+
# Map permission flags to permissions string
|
|
95
|
+
if bypass_permissions:
|
|
96
|
+
permissions = "bypass"
|
|
97
|
+
elif skip_permissions:
|
|
98
|
+
permissions = "skip"
|
|
99
|
+
else:
|
|
100
|
+
permissions = "normal"
|
|
101
|
+
|
|
102
|
+
controller = SisterController()
|
|
103
|
+
result = controller.launch_agent(
|
|
104
|
+
sister_url=sister_config["url"],
|
|
105
|
+
api_key=sister_config.get("api_key", ""),
|
|
106
|
+
directory=remote_dir,
|
|
107
|
+
name=name,
|
|
108
|
+
prompt=prompt,
|
|
109
|
+
permissions=permissions,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if result.ok:
|
|
113
|
+
rprint(f"\n[green]✓[/green] Remote agent '[bold]{name}[/bold]' launched on [bold]{sister}[/bold]")
|
|
114
|
+
if prompt:
|
|
115
|
+
rprint(" Initial prompt sent")
|
|
116
|
+
else:
|
|
117
|
+
rprint(f"[red]Error: Remote launch failed: {result.error}[/red]")
|
|
118
|
+
raise typer.Exit(code=1)
|
|
119
|
+
return
|
|
120
|
+
|
|
67
121
|
# Parse oversight policy
|
|
68
122
|
oversight_policy = "wait"
|
|
69
123
|
oversight_timeout_seconds = 0.0
|
|
@@ -104,6 +158,7 @@ def launch(
|
|
|
104
158
|
parent_name=parent,
|
|
105
159
|
allowed_tools=allowed_tools,
|
|
106
160
|
extra_claude_args=claude_args,
|
|
161
|
+
agent_teams=teams,
|
|
107
162
|
)
|
|
108
163
|
|
|
109
164
|
if result:
|
|
@@ -116,6 +171,8 @@ def launch(
|
|
|
116
171
|
rprint(f" Allowed tools: {allowed_tools}")
|
|
117
172
|
if claude_args:
|
|
118
173
|
rprint(f" Extra Claude args: {' '.join(claude_args)}")
|
|
174
|
+
if teams:
|
|
175
|
+
rprint(" Agent teams: enabled")
|
|
119
176
|
|
|
120
177
|
# Store oversight policy on session
|
|
121
178
|
if oversight_policy != "wait" or oversight_timeout_seconds > 0:
|
|
@@ -155,6 +212,15 @@ def list_agents(
|
|
|
155
212
|
sisters: Annotated[
|
|
156
213
|
bool, typer.Option("--sisters", help="Include sister (remote) agents")
|
|
157
214
|
] = False,
|
|
215
|
+
low: Annotated[
|
|
216
|
+
bool, typer.Option("--low", help="Low detail (identity only)")
|
|
217
|
+
] = False,
|
|
218
|
+
med: Annotated[
|
|
219
|
+
bool, typer.Option("--med", help="Medium detail")
|
|
220
|
+
] = False,
|
|
221
|
+
full: Annotated[
|
|
222
|
+
bool, typer.Option("--full", help="Full detail (default)")
|
|
223
|
+
] = False,
|
|
158
224
|
session: SessionOption = "agents",
|
|
159
225
|
):
|
|
160
226
|
"""List running agents with status.
|
|
@@ -170,19 +236,30 @@ def list_agents(
|
|
|
170
236
|
get_status_symbol, get_git_diff_stats,
|
|
171
237
|
)
|
|
172
238
|
from ..monitor_daemon_state import get_monitor_daemon_state
|
|
173
|
-
from ..summary_columns import build_cli_context,
|
|
239
|
+
from ..summary_columns import build_cli_context, render_summary_cells, align_summary_rows
|
|
174
240
|
from ..tui_logic import compute_tree_metadata, sort_sessions_by_tree
|
|
175
|
-
from rich.text import Text
|
|
176
241
|
from rich.console import Console
|
|
177
242
|
|
|
243
|
+
# Resolve detail level (mutually exclusive flags, default full)
|
|
244
|
+
if low:
|
|
245
|
+
detail = "low"
|
|
246
|
+
elif med:
|
|
247
|
+
detail = "med"
|
|
248
|
+
else:
|
|
249
|
+
detail = "full"
|
|
250
|
+
|
|
178
251
|
launcher = ClaudeLauncher(session)
|
|
179
252
|
sessions = launcher.list_sessions()
|
|
180
253
|
|
|
181
254
|
# Merge sister sessions if --sisters flag
|
|
255
|
+
has_sisters = False
|
|
256
|
+
local_hostname = ""
|
|
182
257
|
if sisters:
|
|
183
258
|
from ..sister_poller import SisterPoller
|
|
184
259
|
poller = SisterPoller()
|
|
185
|
-
|
|
260
|
+
has_sisters = poller.has_sisters
|
|
261
|
+
local_hostname = poller.local_hostname
|
|
262
|
+
if has_sisters:
|
|
186
263
|
remote_sessions = poller.poll_all()
|
|
187
264
|
sessions = sessions + remote_sessions
|
|
188
265
|
|
|
@@ -212,23 +289,38 @@ def list_agents(
|
|
|
212
289
|
sessions = sort_sessions_by_tree(sessions)
|
|
213
290
|
tree_meta = compute_tree_metadata(sessions)
|
|
214
291
|
|
|
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
|
|
292
|
+
# Pre-compute: any agent with budget, column alignment widths
|
|
224
293
|
any_has_budget = any(s.cost_budget_usd > 0 for s in sessions)
|
|
225
294
|
max_name_len = max(len(s.name) for s in sessions)
|
|
226
295
|
name_width = min(max(max_name_len, 10), 20)
|
|
296
|
+
max_repo_width = max((len(s.repo_name or "n/a") for s in sessions), default=5)
|
|
297
|
+
max_branch_width = max((len(s.branch or "n/a") for s in sessions), default=5)
|
|
298
|
+
all_names_match_repos = all(
|
|
299
|
+
s.name == s.repo_name for s in sessions if s.repo_name
|
|
300
|
+
)
|
|
227
301
|
|
|
228
302
|
# Prefer daemon state for status/activity (single source of truth)
|
|
229
303
|
daemon_state = get_monitor_daemon_state(session)
|
|
230
304
|
use_daemon = daemon_state is not None and not daemon_state.is_stale()
|
|
231
305
|
|
|
306
|
+
# Compute cross-session flags from daemon state
|
|
307
|
+
any_has_oversight_timeout = False
|
|
308
|
+
any_has_pr = False
|
|
309
|
+
if use_daemon:
|
|
310
|
+
any_has_oversight_timeout = any(
|
|
311
|
+
ds.oversight_timeout_seconds > 0
|
|
312
|
+
for ds in daemon_state.sessions
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
any_has_oversight_timeout = any(
|
|
316
|
+
getattr(s, 'oversight_timeout_seconds', 0) > 0
|
|
317
|
+
for s in sessions
|
|
318
|
+
)
|
|
319
|
+
any_has_pr = any(
|
|
320
|
+
getattr(s, 'pr_number', None) is not None
|
|
321
|
+
for s in sessions
|
|
322
|
+
)
|
|
323
|
+
|
|
232
324
|
# Only create detector as fallback when daemon isn't running
|
|
233
325
|
detector = None
|
|
234
326
|
if not use_daemon:
|
|
@@ -288,24 +380,43 @@ def list_agents(
|
|
|
288
380
|
# Compute cross-session flags
|
|
289
381
|
any_is_sleeping = any(st == "busy_sleeping" for _, st, _, _, _ in session_data)
|
|
290
382
|
|
|
291
|
-
# Second pass:
|
|
383
|
+
# Second pass: build contexts and collect cells for auto-alignment
|
|
384
|
+
all_cells = []
|
|
385
|
+
activities = []
|
|
292
386
|
for sess, status, activity, claude_stats, git_diff in session_data:
|
|
293
387
|
meta = tree_meta.get(sess.id)
|
|
294
388
|
child_count = meta.child_count if meta else 0
|
|
389
|
+
|
|
390
|
+
# Get per-session daemon fields
|
|
391
|
+
oversight_deadline = None
|
|
392
|
+
if use_daemon:
|
|
393
|
+
ds = daemon_state.get_session_by_name(sess.name)
|
|
394
|
+
if ds:
|
|
395
|
+
oversight_deadline = ds.oversight_deadline
|
|
396
|
+
|
|
397
|
+
_, status_color = get_status_symbol(status)
|
|
295
398
|
ctx = build_cli_context(
|
|
296
399
|
session=sess, stats=sess.stats,
|
|
297
400
|
claude_stats=claude_stats, git_diff_stats=git_diff,
|
|
298
401
|
status=status, bg_bash_count=0, live_sub_count=0,
|
|
299
402
|
any_has_budget=any_has_budget, child_count=child_count,
|
|
300
403
|
any_is_sleeping=any_is_sleeping,
|
|
404
|
+
any_has_oversight_timeout=any_has_oversight_timeout,
|
|
405
|
+
oversight_deadline=oversight_deadline,
|
|
406
|
+
pr_number=getattr(sess, 'pr_number', None),
|
|
407
|
+
any_has_pr=any_has_pr,
|
|
408
|
+
monochrome=False,
|
|
409
|
+
summary_detail=detail,
|
|
410
|
+
has_sisters=has_sisters,
|
|
411
|
+
local_hostname=local_hostname,
|
|
412
|
+
max_name_width=name_width,
|
|
413
|
+
max_repo_width=max_repo_width,
|
|
414
|
+
max_branch_width=max_branch_width,
|
|
415
|
+
all_names_match_repos=all_names_match_repos,
|
|
301
416
|
)
|
|
302
|
-
|
|
303
|
-
# Enable colors and set detail level for list view
|
|
304
|
-
ctx.monochrome = False
|
|
305
|
-
_, status_color = get_status_symbol(status)
|
|
306
417
|
ctx.status_color = f"bold {status_color}"
|
|
307
|
-
ctx.summary_detail = "med"
|
|
308
418
|
ctx.show_cost = cost
|
|
419
|
+
ctx.is_list_mode = True
|
|
309
420
|
|
|
310
421
|
# Handle tree indentation (#244) using compute_tree_metadata
|
|
311
422
|
depth = meta.depth if meta else 0
|
|
@@ -313,23 +424,15 @@ def list_agents(
|
|
|
313
424
|
available = name_width - len(indent)
|
|
314
425
|
ctx.display_name = (indent + sess.name[:available]).ljust(name_width)
|
|
315
426
|
|
|
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)
|
|
427
|
+
all_cells.append(render_summary_cells(ctx))
|
|
428
|
+
activities.append(activity)
|
|
429
|
+
|
|
430
|
+
# Auto-align columns across all rows, then append activity
|
|
431
|
+
aligned_lines = align_summary_rows(all_cells)
|
|
432
|
+
for line, activity in zip(aligned_lines, activities):
|
|
329
433
|
line.append(" │ ", style="dim")
|
|
330
434
|
line.append(activity)
|
|
331
435
|
line.truncate(console.width, pad=False)
|
|
332
|
-
|
|
333
436
|
console.print(line, no_wrap=True)
|
|
334
437
|
|
|
335
438
|
if terminated_count > 0:
|
|
@@ -756,6 +859,8 @@ def show(
|
|
|
756
859
|
print(f"{'Tools:':<{label_width + 1}} {sess.allowed_tools}")
|
|
757
860
|
if sess.extra_claude_args:
|
|
758
861
|
print(f"{'Claude args:':<{label_width + 1}} {' '.join(sess.extra_claude_args)}")
|
|
862
|
+
if sess.agent_teams:
|
|
863
|
+
print(f"{'Teams:':<{label_width + 1}} enabled")
|
|
759
864
|
|
|
760
865
|
print()
|
|
761
866
|
|
|
@@ -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"]
|
|
@@ -11,6 +11,7 @@ import time
|
|
|
11
11
|
import subprocess
|
|
12
12
|
import os
|
|
13
13
|
import shlex
|
|
14
|
+
from pathlib import Path
|
|
14
15
|
from typing import List, Optional
|
|
15
16
|
|
|
16
17
|
import re
|
|
@@ -77,6 +78,7 @@ class ClaudeLauncher:
|
|
|
77
78
|
parent_name: Optional[str] = None,
|
|
78
79
|
allowed_tools: Optional[str] = None,
|
|
79
80
|
extra_claude_args: Optional[List[str]] = None,
|
|
81
|
+
agent_teams: bool = False,
|
|
80
82
|
) -> Optional[Session]:
|
|
81
83
|
"""
|
|
82
84
|
Launch an interactive Claude Code session in a tmux window.
|
|
@@ -175,6 +177,10 @@ class ClaudeLauncher:
|
|
|
175
177
|
if parent_session:
|
|
176
178
|
env_prefix += f" OVERCODE_PARENT_SESSION_ID={parent_session.id} OVERCODE_PARENT_NAME={parent_session.name}"
|
|
177
179
|
|
|
180
|
+
# Enable Claude Code agent teams if requested
|
|
181
|
+
if agent_teams:
|
|
182
|
+
env_prefix += " CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1"
|
|
183
|
+
|
|
178
184
|
# If MOCK_SCENARIO is set, prepend it to the command for testing
|
|
179
185
|
mock_scenario = os.environ.get("MOCK_SCENARIO")
|
|
180
186
|
if mock_scenario:
|
|
@@ -198,16 +204,19 @@ class ClaudeLauncher:
|
|
|
198
204
|
|
|
199
205
|
# Register session with default standing instructions from config
|
|
200
206
|
default_instructions = get_default_standing_instructions()
|
|
207
|
+
# Resolve to absolute path so daemon/CLI don't disagree on CWD (#312)
|
|
208
|
+
resolved_directory = str(Path(start_directory).resolve()) if start_directory else None
|
|
201
209
|
session = self.sessions.create_session(
|
|
202
210
|
name=name,
|
|
203
211
|
tmux_session=self.tmux.session_name,
|
|
204
212
|
tmux_window=window_index,
|
|
205
213
|
command=claude_cmd,
|
|
206
|
-
start_directory=
|
|
214
|
+
start_directory=resolved_directory,
|
|
207
215
|
standing_instructions=default_instructions,
|
|
208
216
|
permissiveness_mode=perm_mode,
|
|
209
217
|
allowed_tools=allowed_tools,
|
|
210
218
|
extra_claude_args=extra_claude_args,
|
|
219
|
+
agent_teams=agent_teams,
|
|
211
220
|
)
|
|
212
221
|
|
|
213
222
|
# Set parent if launching as child agent (#244)
|
|
@@ -467,6 +476,33 @@ class ClaudeLauncher:
|
|
|
467
476
|
print(f"Session '{name}' not found")
|
|
468
477
|
return False
|
|
469
478
|
|
|
479
|
+
return self._send_to_resolved_session(session, text, enter)
|
|
480
|
+
|
|
481
|
+
def send_to_session_by_id(self, session_id: str, text: str, enter: bool = True) -> bool:
|
|
482
|
+
"""Send text/keys to a session by ID.
|
|
483
|
+
|
|
484
|
+
Preferred over send_to_session() when the session ID is known,
|
|
485
|
+
since IDs are unique even when local and remote agents share a name.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
session_id: Unique session ID
|
|
489
|
+
text: Text to send (or special key like "Enter", "Escape")
|
|
490
|
+
enter: Whether to press Enter after the text (default: True)
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
True if successful, False otherwise
|
|
494
|
+
"""
|
|
495
|
+
session = self.sessions.get_session(session_id)
|
|
496
|
+
if session is None:
|
|
497
|
+
return False
|
|
498
|
+
|
|
499
|
+
return self._send_to_resolved_session(session, text, enter)
|
|
500
|
+
|
|
501
|
+
def _send_to_resolved_session(self, session: Session, text: str, enter: bool = True) -> bool:
|
|
502
|
+
"""Send text/keys to an already-resolved session.
|
|
503
|
+
|
|
504
|
+
Internal helper shared by send_to_session() and send_to_session_by_id().
|
|
505
|
+
"""
|
|
470
506
|
# Handle special keys
|
|
471
507
|
special_keys = {
|
|
472
508
|
"enter": "", # Empty string + Enter = just press Enter
|
|
@@ -63,6 +63,7 @@ from .status_constants import (
|
|
|
63
63
|
is_green_status,
|
|
64
64
|
)
|
|
65
65
|
from .status_detector import StatusDetector
|
|
66
|
+
from .status_patterns import extract_pr_number
|
|
66
67
|
from .status_detector_factory import StatusDetectorDispatcher
|
|
67
68
|
from .status_history import log_agent_status
|
|
68
69
|
from .monitor_daemon_core import (
|
|
@@ -191,7 +192,7 @@ class PresenceComponent:
|
|
|
191
192
|
self._last_publish_time = now
|
|
192
193
|
return False
|
|
193
194
|
gap = (now - self._last_publish_time).total_seconds()
|
|
194
|
-
slept = gap >
|
|
195
|
+
slept = gap > 20 # Machine sleep produces gaps of 30s+; absolute threshold avoids false positives
|
|
195
196
|
self._last_publish_time = now
|
|
196
197
|
return slept
|
|
197
198
|
|
|
@@ -637,7 +638,7 @@ class MonitorDaemon:
|
|
|
637
638
|
|
|
638
639
|
def _interruptible_sleep(self, total_seconds: int) -> None:
|
|
639
640
|
"""Sleep with activity signal checking."""
|
|
640
|
-
chunk_size =
|
|
641
|
+
chunk_size = 1
|
|
641
642
|
elapsed = 0
|
|
642
643
|
|
|
643
644
|
while elapsed < total_seconds and not self._shutdown:
|
|
@@ -849,7 +850,13 @@ class MonitorDaemon:
|
|
|
849
850
|
status, activity = STATUS_DONE, "Completed"
|
|
850
851
|
else:
|
|
851
852
|
# Detect status - dispatches per-session via dispatcher (#5)
|
|
852
|
-
status, activity,
|
|
853
|
+
status, activity, pane_content = self.detector.detect_status(session)
|
|
854
|
+
|
|
855
|
+
# Extract PR number from pane content
|
|
856
|
+
if pane_content:
|
|
857
|
+
pr = extract_pr_number(pane_content)
|
|
858
|
+
if pr is not None and pr != session.pr_number:
|
|
859
|
+
self.session_manager.update_session(session.id, pr_number=pr)
|
|
853
860
|
|
|
854
861
|
# Clear heartbeat tracking when session stops running
|
|
855
862
|
if status != STATUS_RUNNING and session.id in self._sessions_running_from_heartbeat:
|