overcode 0.3.4__tar.gz → 0.3.6__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.3.4/src/overcode.egg-info → overcode-0.3.6}/PKG-INFO +1 -1
- {overcode-0.3.4 → overcode-0.3.6}/pyproject.toml +1 -1
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/bundled_skills.py +5 -2
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/agent.py +82 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/jobs.py +104 -1
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/config.py +28 -0
- overcode-0.3.6/src/overcode/daemon_claude_skill.md +68 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/data_export.py +8 -1
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/history_reader.py +21 -2
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/hook_handler.py +47 -1
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/hook_status_detector.py +2 -1
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/launcher.py +41 -2
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/monitor_daemon.py +85 -6
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/monitor_daemon_core.py +2 -33
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/monitor_daemon_state.py +2 -1
- overcode-0.3.6/src/overcode/pricing.py +106 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/session_manager.py +3 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/settings.py +2 -17
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/sister_poller.py +5 -2
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_history.py +26 -11
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/summarizer_client.py +78 -6
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/summarizer_component.py +17 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/summary_columns.py +24 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/supervisor_daemon.py +53 -2
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/supervisor_daemon_core.py +14 -7
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tmux_manager.py +7 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui.py +69 -10
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui.tcss +2 -2
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/daemon.py +23 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_logic.py +2 -2
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_render.py +36 -1
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/command_bar.py +36 -6
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/daemon_status_bar.py +18 -15
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/session_summary.py +5 -1
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/status_timeline.py +1 -1
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_api.py +5 -4
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_control_api.py +6 -6
- {overcode-0.3.4 → overcode-0.3.6/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/SOURCES.txt +1 -0
- overcode-0.3.4/src/overcode/daemon_claude_skill.md +0 -183
- {overcode-0.3.4 → overcode-0.3.6}/LICENSE +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/MANIFEST.in +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/README.md +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/setup.cfg +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/__init__.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/agent_scanner.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/claude_config.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/claude_pid.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/__init__.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/_shared.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/budget.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/config.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/daemon.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/monitoring.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/perms.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/sister.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/skills.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/split.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/dependency_check.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/duration.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/exceptions.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/follow_mode.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/implementations.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/interfaces.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/job_launcher.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/job_manager.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/logging_config.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/mocks.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/notifier.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/pid_utils.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/presence_logger.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/protocols.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/sister_controller.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/ssh_provisioner.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_constants.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_detector.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_detector_factory.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_patterns.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/summary_groups.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/time_context.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/input.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/navigation.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/session.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/view.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/__init__.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/help_overlay.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/job_summary.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/tui_log_panel.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web/__init__.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_server.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_templates.py +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.3.4 → overcode-0.3.6}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -94,8 +94,10 @@ overcode bash "make deploy-staging" --agent my-agent # Link to an agent
|
|
|
94
94
|
|
|
95
95
|
# Manage jobs
|
|
96
96
|
overcode jobs list [--all] # List running (or all) jobs
|
|
97
|
+
overcode jobs tail <name> # Stream output (works without TTY)
|
|
98
|
+
overcode jobs tail <name> -n 50 # Last 50 lines and exit
|
|
97
99
|
overcode jobs kill <name> # Kill a running job
|
|
98
|
-
overcode jobs attach <name> # Attach to job's tmux window
|
|
100
|
+
overcode jobs attach <name> # Attach to job's tmux window (needs TTY)
|
|
99
101
|
overcode jobs clear # Remove completed/failed/killed jobs
|
|
100
102
|
|
|
101
103
|
# TUI: press J to toggle jobs view, j/k to navigate, x to kill, c to clear
|
|
@@ -227,7 +229,8 @@ overcode bash "npm run build" --name frontend-build --agent my-agent
|
|
|
227
229
|
|
|
228
230
|
# Check on it later
|
|
229
231
|
overcode jobs list
|
|
230
|
-
overcode jobs
|
|
232
|
+
overcode jobs tail full-tests # Stream output (no TTY needed)
|
|
233
|
+
overcode jobs tail full-tests -n 50 # Last 50 lines snapshot
|
|
231
234
|
overcode jobs kill full-tests # Kill if needed
|
|
232
235
|
```
|
|
233
236
|
|
|
@@ -212,6 +212,10 @@ def launch(
|
|
|
212
212
|
bool,
|
|
213
213
|
typer.Option("--teams", help="Enable Claude Code agent teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)"),
|
|
214
214
|
] = False,
|
|
215
|
+
provider: Annotated[
|
|
216
|
+
Optional[str],
|
|
217
|
+
typer.Option("--provider", "-P", help="API provider: 'web' (Claude.ai OAuth) or 'bedrock' (AWS Bedrock)"),
|
|
218
|
+
] = None,
|
|
215
219
|
sister: Annotated[
|
|
216
220
|
Optional[str],
|
|
217
221
|
typer.Option("--sister", "-S", help="Launch on a remote sister machine (by name from config)"),
|
|
@@ -270,6 +274,15 @@ def launch(
|
|
|
270
274
|
# Parse oversight policy
|
|
271
275
|
oversight_policy, oversight_timeout_seconds = _parse_oversight_policy(on_stuck, oversight_timeout)
|
|
272
276
|
|
|
277
|
+
# Resolve provider: CLI flag > config default > "web"
|
|
278
|
+
resolved_provider = provider
|
|
279
|
+
if resolved_provider is None:
|
|
280
|
+
from ..config import get_new_agent_defaults
|
|
281
|
+
resolved_provider = get_new_agent_defaults().get("provider", "web")
|
|
282
|
+
if resolved_provider not in ("web", "bedrock"):
|
|
283
|
+
rprint(f"[red]Error: Invalid provider '{resolved_provider}'. Use: web, bedrock[/red]")
|
|
284
|
+
raise typer.Exit(code=1)
|
|
285
|
+
|
|
273
286
|
# Default to current directory if not specified
|
|
274
287
|
working_dir = directory if directory else os.getcwd()
|
|
275
288
|
|
|
@@ -288,6 +301,7 @@ def launch(
|
|
|
288
301
|
budget_usd=budget,
|
|
289
302
|
claude_agent=agent,
|
|
290
303
|
model=model,
|
|
304
|
+
provider=resolved_provider,
|
|
291
305
|
)
|
|
292
306
|
|
|
293
307
|
if result:
|
|
@@ -304,6 +318,8 @@ def launch(
|
|
|
304
318
|
rprint(f" Agent: {agent}")
|
|
305
319
|
if teams:
|
|
306
320
|
rprint(" Agent teams: enabled")
|
|
321
|
+
if resolved_provider != "web":
|
|
322
|
+
rprint(f" Provider: {resolved_provider}")
|
|
307
323
|
if budget is not None and budget > 0:
|
|
308
324
|
rprint(f" Budget: ${budget:.2f}")
|
|
309
325
|
|
|
@@ -479,6 +495,8 @@ def list_agents(
|
|
|
479
495
|
|
|
480
496
|
# Pre-compute: any agent with budget, column alignment widths
|
|
481
497
|
any_has_budget = any(s.cost_budget_usd > 0 for s in sessions)
|
|
498
|
+
any_has_provider = any(getattr(s, 'provider', 'web') not in ('web', None, '') for s in sessions)
|
|
499
|
+
any_has_model = any(getattr(s, 'model', None) for s in sessions)
|
|
482
500
|
max_name_len = max((len(s.name) for s in sessions), default=10)
|
|
483
501
|
name_width = min(max(max_name_len, 10), 20)
|
|
484
502
|
max_repo_width = max((len(s.repo_name or "n/a") for s in sessions), default=5)
|
|
@@ -585,6 +603,8 @@ def list_agents(
|
|
|
585
603
|
oversight_deadline=oversight_deadline,
|
|
586
604
|
pr_number=getattr(sess, 'pr_number', None),
|
|
587
605
|
any_has_pr=any_has_pr,
|
|
606
|
+
any_has_model=any_has_model,
|
|
607
|
+
any_has_provider=any_has_provider,
|
|
588
608
|
monochrome=False,
|
|
589
609
|
summary_detail=detail,
|
|
590
610
|
has_sisters=has_sisters,
|
|
@@ -673,6 +693,59 @@ def kill(
|
|
|
673
693
|
launcher.kill_session(name, cascade=not no_cascade)
|
|
674
694
|
|
|
675
695
|
|
|
696
|
+
@app.command()
|
|
697
|
+
def restart(
|
|
698
|
+
name: Annotated[str, typer.Argument(help="Name of agent to restart")],
|
|
699
|
+
session: SessionOption = "agents",
|
|
700
|
+
):
|
|
701
|
+
"""Restart a running agent with the same configuration.
|
|
702
|
+
|
|
703
|
+
Gracefully exits Claude (Ctrl-C + /exit), then relaunches with the
|
|
704
|
+
same permissions mode. Useful when MCP server configs change.
|
|
705
|
+
"""
|
|
706
|
+
import os
|
|
707
|
+
import time
|
|
708
|
+
from ..session_manager import SessionManager
|
|
709
|
+
from ..tmux_manager import TmuxManager
|
|
710
|
+
from ..tmux_utils import tmux_window_target, _build_tmux_cmd
|
|
711
|
+
|
|
712
|
+
sm = SessionManager()
|
|
713
|
+
sess = sm.get_session_by_name(name)
|
|
714
|
+
if not sess:
|
|
715
|
+
rprint(f"[red]Error: Agent '{name}' not found[/red]")
|
|
716
|
+
raise typer.Exit(code=1)
|
|
717
|
+
|
|
718
|
+
tmux = TmuxManager(session)
|
|
719
|
+
if not tmux.window_exists(sess.tmux_window):
|
|
720
|
+
rprint(f"[red]Error: Tmux window for '{name}' no longer exists[/red]")
|
|
721
|
+
raise typer.Exit(code=1)
|
|
722
|
+
|
|
723
|
+
# Build the claude command based on permissiveness mode
|
|
724
|
+
claude_command = os.environ.get("CLAUDE_COMMAND", "claude")
|
|
725
|
+
cmd_parts = [claude_command]
|
|
726
|
+
if sess.permissiveness_mode == "bypass":
|
|
727
|
+
cmd_parts.append("--dangerously-skip-permissions")
|
|
728
|
+
elif sess.permissiveness_mode == "permissive":
|
|
729
|
+
cmd_parts.extend(["--permission-mode", "dontAsk"])
|
|
730
|
+
cmd_str = " ".join(cmd_parts)
|
|
731
|
+
|
|
732
|
+
# Gracefully exit Claude: Ctrl-C + /exit
|
|
733
|
+
rprint(f"[dim]Stopping '{name}'...[/dim]")
|
|
734
|
+
tmux.send_keys(sess.tmux_window, "C-c", enter=False)
|
|
735
|
+
time.sleep(0.5)
|
|
736
|
+
tmux.send_keys(sess.tmux_window, "/exit", enter=True)
|
|
737
|
+
time.sleep(3.0)
|
|
738
|
+
|
|
739
|
+
# Relaunch
|
|
740
|
+
if tmux.send_keys(sess.tmux_window, cmd_str, enter=True):
|
|
741
|
+
sm.update_stats(sess.id, current_task="Restarting...")
|
|
742
|
+
sm.update_session(sess.id, claude_session_ids=[])
|
|
743
|
+
rprint(f"[green]Restarted agent: {name}[/green]")
|
|
744
|
+
else:
|
|
745
|
+
rprint(f"[red]Failed to restart agent: {name}[/red]")
|
|
746
|
+
raise typer.Exit(code=1)
|
|
747
|
+
|
|
748
|
+
|
|
676
749
|
@app.command()
|
|
677
750
|
def follow(
|
|
678
751
|
name: Annotated[str, typer.Argument(help="Name of agent to follow")],
|
|
@@ -903,6 +976,15 @@ def send(
|
|
|
903
976
|
overcode send my-agent escape # Press Escape (reject)
|
|
904
977
|
overcode send my-agent --no-enter "y" # Send "y" without Enter
|
|
905
978
|
"""
|
|
979
|
+
from ..session_manager import SessionManager
|
|
980
|
+
sm = SessionManager()
|
|
981
|
+
|
|
982
|
+
# Auto-wake sleeping agent (#168)
|
|
983
|
+
agent_session = sm.get_session_by_name(name)
|
|
984
|
+
if agent_session and agent_session.is_asleep:
|
|
985
|
+
sm.update_session(agent_session.id, is_asleep=False)
|
|
986
|
+
rprint(f"[dim]Woke agent '{name}' to send command[/dim]")
|
|
987
|
+
|
|
906
988
|
launcher = ClaudeLauncher(session)
|
|
907
989
|
|
|
908
990
|
# Join all text parts if multiple were given
|
|
@@ -55,7 +55,10 @@ def bash(
|
|
|
55
55
|
rprint(f" Linked to agent: {agent_name}")
|
|
56
56
|
|
|
57
57
|
if follow:
|
|
58
|
-
|
|
58
|
+
if os.isatty(0):
|
|
59
|
+
launcher.attach(job.name)
|
|
60
|
+
else:
|
|
61
|
+
rprint(f" [dim]No TTY — use 'overcode jobs tail {job.name}' to stream output[/dim]")
|
|
59
62
|
|
|
60
63
|
|
|
61
64
|
@jobs_app.command("list")
|
|
@@ -144,6 +147,106 @@ def clear_completed():
|
|
|
144
147
|
rprint("[green]✓[/green] Cleared completed jobs")
|
|
145
148
|
|
|
146
149
|
|
|
150
|
+
@jobs_app.command("tail")
|
|
151
|
+
def tail_job(
|
|
152
|
+
name: Annotated[str, typer.Argument(help="Job name to tail")],
|
|
153
|
+
lines: Annotated[Optional[int], typer.Option("--lines", "-n", help="Show last N lines and exit")] = None,
|
|
154
|
+
follow: Annotated[bool, typer.Option("--follow/--no-follow", "-f", help="Stream output until job completes")] = True,
|
|
155
|
+
poll_interval: Annotated[float, typer.Option("--poll", hidden=True)] = 0.5,
|
|
156
|
+
):
|
|
157
|
+
"""Stream a job's output (like tail -f). Works without a TTY.
|
|
158
|
+
|
|
159
|
+
Default: streams output until the job completes or Ctrl-C.
|
|
160
|
+
With --lines N: shows last N lines and exits.
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
overcode jobs tail my-job
|
|
164
|
+
overcode jobs tail my-job --lines 50
|
|
165
|
+
overcode jobs tail my-job --no-follow
|
|
166
|
+
"""
|
|
167
|
+
import signal
|
|
168
|
+
import sys
|
|
169
|
+
import time
|
|
170
|
+
from collections import deque
|
|
171
|
+
from ..follow_mode import _capture_pane, _find_dedup_start
|
|
172
|
+
from ..job_manager import JobManager
|
|
173
|
+
from ..status_patterns import strip_ansi
|
|
174
|
+
|
|
175
|
+
manager = JobManager()
|
|
176
|
+
job = manager.get_job_by_name(name)
|
|
177
|
+
if not job:
|
|
178
|
+
rprint(f"[red]Error: Job '{name}' not found[/red]")
|
|
179
|
+
raise typer.Exit(1)
|
|
180
|
+
|
|
181
|
+
tmux_session = job.tmux_session
|
|
182
|
+
window_name = job.tmux_window
|
|
183
|
+
|
|
184
|
+
# One-shot: capture and print last N lines
|
|
185
|
+
if lines is not None:
|
|
186
|
+
raw = _capture_pane(tmux_session, window_name, lines=lines)
|
|
187
|
+
if raw:
|
|
188
|
+
for line in raw.rstrip().split('\n'):
|
|
189
|
+
cleaned = strip_ansi(line).strip()
|
|
190
|
+
if cleaned:
|
|
191
|
+
print(cleaned)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Streaming mode
|
|
195
|
+
interrupted = False
|
|
196
|
+
|
|
197
|
+
def _sigint(sig, frame):
|
|
198
|
+
nonlocal interrupted
|
|
199
|
+
interrupted = True
|
|
200
|
+
|
|
201
|
+
old_handler = signal.signal(signal.SIGINT, _sigint)
|
|
202
|
+
|
|
203
|
+
recent_lines: deque = deque(maxlen=50)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
while not interrupted:
|
|
207
|
+
raw = _capture_pane(tmux_session, window_name)
|
|
208
|
+
if raw:
|
|
209
|
+
new_lines = []
|
|
210
|
+
for line in raw.rstrip().split('\n'):
|
|
211
|
+
cleaned = strip_ansi(line).strip()
|
|
212
|
+
new_lines.append(cleaned)
|
|
213
|
+
|
|
214
|
+
output_start = _find_dedup_start(new_lines, recent_lines)
|
|
215
|
+
if output_start < len(new_lines):
|
|
216
|
+
for line in new_lines[output_start:]:
|
|
217
|
+
if line:
|
|
218
|
+
print(line)
|
|
219
|
+
recent_lines.append(line)
|
|
220
|
+
|
|
221
|
+
# Check if job is done
|
|
222
|
+
if follow:
|
|
223
|
+
job = manager.get_job_by_name(name)
|
|
224
|
+
if job and job.status in ("completed", "failed", "killed"):
|
|
225
|
+
# Final capture
|
|
226
|
+
time.sleep(poll_interval)
|
|
227
|
+
raw = _capture_pane(tmux_session, window_name)
|
|
228
|
+
if raw:
|
|
229
|
+
new_lines = [strip_ansi(l).strip() for l in raw.rstrip().split('\n')]
|
|
230
|
+
output_start = _find_dedup_start(new_lines, recent_lines)
|
|
231
|
+
for line in new_lines[output_start:]:
|
|
232
|
+
if line:
|
|
233
|
+
print(line)
|
|
234
|
+
recent_lines.append(line)
|
|
235
|
+
status_msg = f"completed (exit {job.exit_code})" if job.exit_code is not None else job.status
|
|
236
|
+
print(f"\n[tail] Job '{name}' {status_msg}", file=sys.stderr)
|
|
237
|
+
raise typer.Exit(0 if job.status == "completed" else 1)
|
|
238
|
+
else:
|
|
239
|
+
# --no-follow: one capture cycle then exit
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
time.sleep(poll_interval)
|
|
243
|
+
finally:
|
|
244
|
+
signal.signal(signal.SIGINT, old_handler)
|
|
245
|
+
|
|
246
|
+
if interrupted:
|
|
247
|
+
raise typer.Exit(130)
|
|
248
|
+
|
|
249
|
+
|
|
147
250
|
@jobs_app.command("_complete", hidden=True)
|
|
148
251
|
def mark_complete(
|
|
149
252
|
job_id: Annotated[str, typer.Argument(help="Job ID")],
|
|
@@ -118,8 +118,10 @@ def get_summarizer_config() -> dict:
|
|
|
118
118
|
default_api_url = "https://api.openai.com/v1/chat/completions"
|
|
119
119
|
default_model = "gpt-4o-mini"
|
|
120
120
|
default_api_key_var = "OPENAI_API_KEY"
|
|
121
|
+
default_api_type = "openai"
|
|
121
122
|
|
|
122
123
|
# Config file takes precedence, env vars are fallback
|
|
124
|
+
api_type = _get_config_value("summarizer.api_type") or os.environ.get("OVERCODE_SUMMARIZER_API_TYPE") or default_api_type
|
|
123
125
|
api_url = _get_config_value("summarizer.api_url") or os.environ.get("OVERCODE_SUMMARIZER_API_URL") or default_api_url
|
|
124
126
|
model = _get_config_value("summarizer.model") or os.environ.get("OVERCODE_SUMMARIZER_MODEL") or default_model
|
|
125
127
|
api_key_var = _get_config_value("summarizer.api_key_var") or os.environ.get("OVERCODE_SUMMARIZER_API_KEY_VAR") or default_api_key_var
|
|
@@ -127,11 +129,20 @@ def get_summarizer_config() -> dict:
|
|
|
127
129
|
# Resolve the actual API key from the configured env var
|
|
128
130
|
api_key = os.environ.get(api_key_var)
|
|
129
131
|
|
|
132
|
+
# Cost cap: default $100, configurable
|
|
133
|
+
cost_cap = _get_config_value("summarizer.cost_cap")
|
|
134
|
+
if cost_cap is None:
|
|
135
|
+
cost_cap = 100.0
|
|
136
|
+
else:
|
|
137
|
+
cost_cap = float(cost_cap)
|
|
138
|
+
|
|
130
139
|
return {
|
|
140
|
+
"api_type": api_type,
|
|
131
141
|
"api_url": api_url,
|
|
132
142
|
"model": model,
|
|
133
143
|
"api_key": api_key,
|
|
134
144
|
"api_key_var": api_key_var,
|
|
145
|
+
"cost_cap": cost_cap,
|
|
135
146
|
}
|
|
136
147
|
|
|
137
148
|
|
|
@@ -340,6 +351,7 @@ def get_new_agent_defaults() -> dict:
|
|
|
340
351
|
return {
|
|
341
352
|
"bypass_permissions": bool(defaults.get("bypass_permissions", False)),
|
|
342
353
|
"agent_teams": bool(defaults.get("agent_teams", False)),
|
|
354
|
+
"provider": defaults.get("provider", "web"),
|
|
343
355
|
}
|
|
344
356
|
|
|
345
357
|
|
|
@@ -354,6 +366,22 @@ def save_new_agent_defaults(defaults: dict) -> None:
|
|
|
354
366
|
save_config(config)
|
|
355
367
|
|
|
356
368
|
|
|
369
|
+
def get_bedrock_config() -> dict:
|
|
370
|
+
"""Get AWS Bedrock configuration.
|
|
371
|
+
|
|
372
|
+
Config format in ~/.overcode/config.yaml:
|
|
373
|
+
bedrock:
|
|
374
|
+
region: us-east-1
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Dict with region (str).
|
|
378
|
+
"""
|
|
379
|
+
bedrock = _get_config_value("bedrock", {})
|
|
380
|
+
return {
|
|
381
|
+
"region": bedrock.get("region", "us-east-1"),
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
357
385
|
def get_jobs_retention_hours() -> float:
|
|
358
386
|
"""Get job retention period in hours.
|
|
359
387
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Overcode Supervisor Skill
|
|
2
|
+
|
|
3
|
+
You are the Overcode supervisor agent. Your mission: **Unblock each non-green session once, then exit**.
|
|
4
|
+
|
|
5
|
+
## Status Guide
|
|
6
|
+
|
|
7
|
+
- ORANGE (`waiting_approval`) -- Agent blocked on a permission prompt. This is your PRIMARY target. Approve or reject based on standing instructions and approval rules.
|
|
8
|
+
- RED (`waiting_user`) -- Agent waiting for human input at the prompt. If it has standing instructions, send guidance. If not, skip it.
|
|
9
|
+
- YELLOW (`busy_sleeping`) -- Agent is sleeping. Usually skip.
|
|
10
|
+
- PURPLE (`error`) -- API error. Usually skip.
|
|
11
|
+
|
|
12
|
+
## Critical: Act Fast, Don't Investigate
|
|
13
|
+
|
|
14
|
+
You have LIMITED TIME. Do NOT waste it on `overcode list` or reading sessions.json -- the context below already tells you which sessions need help and their standing instructions.
|
|
15
|
+
|
|
16
|
+
**For each non-green session in order:**
|
|
17
|
+
|
|
18
|
+
1. Run `overcode show <name>` to see what it's stuck on
|
|
19
|
+
2. Immediately act: `overcode send <name> enter` (approve) or `overcode send <name> escape` (reject)
|
|
20
|
+
3. Move to the next session -- do NOT check if it worked
|
|
21
|
+
|
|
22
|
+
## How to Unblock
|
|
23
|
+
|
|
24
|
+
# Approve a permission request (ORANGE sessions)
|
|
25
|
+
overcode send my-agent enter
|
|
26
|
+
|
|
27
|
+
# Reject a permission request
|
|
28
|
+
overcode send my-agent escape
|
|
29
|
+
|
|
30
|
+
# Send text response (RED sessions with instructions)
|
|
31
|
+
overcode send my-agent "your guidance here"
|
|
32
|
+
|
|
33
|
+
## Approval Rules
|
|
34
|
+
|
|
35
|
+
Follow the session's **standing instructions** first. Then apply these defaults:
|
|
36
|
+
|
|
37
|
+
### Auto-Approve
|
|
38
|
+
- File reads/writes/edits, Grep, Glob
|
|
39
|
+
- Shell commands: ls, cat, head, tail, find, grep, mkdir, touch, wc, sort, diff
|
|
40
|
+
- git add, git commit, git status, git diff, git log, git branch
|
|
41
|
+
- Running tests, linters, builds
|
|
42
|
+
- WebFetch, web searches
|
|
43
|
+
- pip/npm/uv install
|
|
44
|
+
|
|
45
|
+
### Use Judgment
|
|
46
|
+
- git push (only if tests pass)
|
|
47
|
+
- Operations outside the project directory
|
|
48
|
+
- Destructive operations (rm, git reset)
|
|
49
|
+
|
|
50
|
+
### Reject
|
|
51
|
+
- rm -rf on large directories
|
|
52
|
+
- Operations on system files
|
|
53
|
+
- Network writes to external services (unless in standing instructions)
|
|
54
|
+
|
|
55
|
+
## Your Process
|
|
56
|
+
|
|
57
|
+
For EACH non-green session listed in the context below:
|
|
58
|
+
1. `overcode show <name>` -- see what it needs
|
|
59
|
+
2. Decide and act immediately
|
|
60
|
+
3. Move on
|
|
61
|
+
|
|
62
|
+
After attempting ALL sessions once, run `exit 0`. The daemon will call you again if needed.
|
|
63
|
+
|
|
64
|
+
**Do NOT:**
|
|
65
|
+
- Run `overcode list` (you already have the list)
|
|
66
|
+
- Read sessions.json (you already have the context)
|
|
67
|
+
- Loop back to check results
|
|
68
|
+
- Make multiple attempts on the same session
|
|
@@ -8,6 +8,7 @@ from datetime import datetime
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Dict, Any
|
|
10
10
|
|
|
11
|
+
from .config import get_hostname
|
|
11
12
|
from .session_manager import SessionManager
|
|
12
13
|
from .status_history import read_agent_status_history
|
|
13
14
|
from .presence_logger import read_presence_history
|
|
@@ -118,6 +119,7 @@ def _session_to_record(session, is_archived: bool) -> Dict[str, Any]:
|
|
|
118
119
|
return {
|
|
119
120
|
"id": session.id,
|
|
120
121
|
"name": session.name,
|
|
122
|
+
"hostname": getattr(session, 'source_host', '') or get_hostname(),
|
|
121
123
|
"tmux_session": session.tmux_session,
|
|
122
124
|
"tmux_window": session.tmux_window,
|
|
123
125
|
"start_directory": session.start_directory,
|
|
@@ -184,6 +186,7 @@ def _get_sessions_schema():
|
|
|
184
186
|
return pa.schema([
|
|
185
187
|
("id", pa.string()),
|
|
186
188
|
("name", pa.string()),
|
|
189
|
+
("hostname", pa.string()),
|
|
187
190
|
("start_time", pa.string()),
|
|
188
191
|
("end_time", pa.string()),
|
|
189
192
|
("is_archived", pa.bool_()),
|
|
@@ -201,6 +204,8 @@ def _get_timeline_schema():
|
|
|
201
204
|
("timestamp", pa.string()),
|
|
202
205
|
("agent", pa.string()),
|
|
203
206
|
("status", pa.string()),
|
|
207
|
+
("session_id", pa.string()),
|
|
208
|
+
("hostname", pa.string()),
|
|
204
209
|
])
|
|
205
210
|
|
|
206
211
|
|
|
@@ -223,11 +228,13 @@ def _build_timeline_records():
|
|
|
223
228
|
records = []
|
|
224
229
|
history = read_agent_status_history(hours=24.0)
|
|
225
230
|
|
|
226
|
-
for ts, agent_name, status, activity in history:
|
|
231
|
+
for ts, agent_name, status, activity, session_id, hostname in history:
|
|
227
232
|
records.append({
|
|
228
233
|
"timestamp": ts.isoformat() if isinstance(ts, datetime) else str(ts),
|
|
229
234
|
"agent": agent_name,
|
|
230
235
|
"status": status,
|
|
236
|
+
"session_id": session_id,
|
|
237
|
+
"hostname": hostname,
|
|
231
238
|
})
|
|
232
239
|
|
|
233
240
|
return records
|
|
@@ -650,8 +650,15 @@ def get_session_stats(
|
|
|
650
650
|
interactions = hf.get_interactions_for_session(session)
|
|
651
651
|
interaction_count = len(interactions)
|
|
652
652
|
|
|
653
|
-
# Derive Claude sessionIds
|
|
653
|
+
# Derive Claude sessionIds and their project paths from interactions.
|
|
654
|
+
# Claude Code may store session files under a different project path
|
|
655
|
+
# than start_directory (e.g., when the directory doesn't exist or Claude
|
|
656
|
+
# chooses a different project root).
|
|
654
657
|
session_ids = {e.session_id for e in interactions if e.session_id}
|
|
658
|
+
sid_to_project: Dict[str, str] = {}
|
|
659
|
+
for e in interactions:
|
|
660
|
+
if e.session_id and e.project:
|
|
661
|
+
sid_to_project[e.session_id] = e.project
|
|
655
662
|
|
|
656
663
|
# Active session ID for context window after /clear (#116)
|
|
657
664
|
active_session_id = getattr(session, 'active_claude_session_id', None)
|
|
@@ -673,6 +680,16 @@ def get_session_stats(
|
|
|
673
680
|
session_file = get_session_file_path(
|
|
674
681
|
session.start_directory, sid, projects_path
|
|
675
682
|
)
|
|
683
|
+
# Fall back to the project path from history entries if the session
|
|
684
|
+
# file doesn't exist at the expected start_directory path. Claude
|
|
685
|
+
# Code may use a different project root (e.g. home dir) when the
|
|
686
|
+
# launch directory no longer exists.
|
|
687
|
+
if not session_file.exists():
|
|
688
|
+
alt_project = sid_to_project.get(sid)
|
|
689
|
+
if alt_project:
|
|
690
|
+
session_file = get_session_file_path(
|
|
691
|
+
alt_project, sid, projects_path
|
|
692
|
+
)
|
|
676
693
|
usage, work_times = read_session_file_stats(session_file, since=session_start)
|
|
677
694
|
total_input += usage["input_tokens"]
|
|
678
695
|
total_output += usage["output_tokens"]
|
|
@@ -695,7 +712,9 @@ def get_session_stats(
|
|
|
695
712
|
all_work_times.extend(work_times)
|
|
696
713
|
|
|
697
714
|
# Check for subagent files in {sessionId}/subagents/
|
|
698
|
-
|
|
715
|
+
# Use the actual project path where the session file was found.
|
|
716
|
+
actual_project = sid_to_project.get(sid, session.start_directory)
|
|
717
|
+
encoded = encode_project_path(actual_project)
|
|
699
718
|
subagents_dir = projects_path / encoded / sid / "subagents"
|
|
700
719
|
if subagents_dir.exists():
|
|
701
720
|
for subagent_file in subagents_dir.glob("agent-*.jsonl"):
|
|
@@ -15,6 +15,7 @@ Hook registrations (all use the same command):
|
|
|
15
15
|
import json
|
|
16
16
|
import logging
|
|
17
17
|
import os
|
|
18
|
+
import subprocess
|
|
18
19
|
import sys
|
|
19
20
|
import time
|
|
20
21
|
from pathlib import Path
|
|
@@ -35,6 +36,47 @@ OVERCODE_HOOKS: list[tuple[str, str]] = [
|
|
|
35
36
|
]
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
def _detect_from_tmux_pane() -> tuple[str | None, str | None]:
|
|
40
|
+
"""Detect agent name and tmux session from the current tmux pane.
|
|
41
|
+
|
|
42
|
+
Fallback for when OVERCODE_SESSION_NAME / OVERCODE_TMUX_SESSION env vars
|
|
43
|
+
are missing (e.g. after a manual session restart with --session-id).
|
|
44
|
+
|
|
45
|
+
Returns (session_name, tmux_session) or (None, None) if detection fails.
|
|
46
|
+
"""
|
|
47
|
+
pane_id = os.environ.get("TMUX_PANE")
|
|
48
|
+
if not pane_id:
|
|
49
|
+
return None, None
|
|
50
|
+
try:
|
|
51
|
+
window_name = subprocess.run(
|
|
52
|
+
["tmux", "display-message", "-p", "-t", pane_id, "#{window_name}"],
|
|
53
|
+
capture_output=True, text=True, timeout=2,
|
|
54
|
+
).stdout.strip()
|
|
55
|
+
tmux_session = subprocess.run(
|
|
56
|
+
["tmux", "display-message", "-p", "-t", pane_id, "#{session_name}"],
|
|
57
|
+
capture_output=True, text=True, timeout=2,
|
|
58
|
+
).stdout.strip()
|
|
59
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
60
|
+
return None, None
|
|
61
|
+
|
|
62
|
+
if not window_name or not tmux_session:
|
|
63
|
+
return None, None
|
|
64
|
+
|
|
65
|
+
# Strip oc-view- prefix from split-view session names
|
|
66
|
+
if tmux_session.startswith("oc-view-"):
|
|
67
|
+
tmux_session = tmux_session[len("oc-view-"):]
|
|
68
|
+
|
|
69
|
+
# Window names are "agentname-XXXX" where XXXX is a UUID prefix
|
|
70
|
+
# Strip the last "-XXXX" suffix to get the agent name
|
|
71
|
+
dash_idx = window_name.rfind("-")
|
|
72
|
+
if dash_idx > 0:
|
|
73
|
+
session_name = window_name[:dash_idx]
|
|
74
|
+
else:
|
|
75
|
+
session_name = window_name
|
|
76
|
+
|
|
77
|
+
return session_name, tmux_session
|
|
78
|
+
|
|
79
|
+
|
|
38
80
|
def _get_hook_state_path(tmux_session: str, session_name: str) -> Path:
|
|
39
81
|
"""Get the path for a hook state file.
|
|
40
82
|
|
|
@@ -88,7 +130,11 @@ def handle_hook_event() -> None:
|
|
|
88
130
|
tmux_session = os.environ.get("OVERCODE_TMUX_SESSION")
|
|
89
131
|
|
|
90
132
|
if not session_name or not tmux_session:
|
|
91
|
-
|
|
133
|
+
# Fallback: detect from tmux pane when env vars are missing
|
|
134
|
+
# (e.g. after manual session restart with --session-id)
|
|
135
|
+
session_name, tmux_session = _detect_from_tmux_pane()
|
|
136
|
+
if not session_name or not tmux_session:
|
|
137
|
+
return
|
|
92
138
|
|
|
93
139
|
# Read stdin JSON
|
|
94
140
|
try:
|
|
@@ -24,6 +24,7 @@ from .status_constants import (
|
|
|
24
24
|
STATUS_CAPTURE_LINES,
|
|
25
25
|
STATUS_RUNNING,
|
|
26
26
|
STATUS_BUSY_SLEEPING,
|
|
27
|
+
STATUS_WAITING_APPROVAL,
|
|
27
28
|
STATUS_WAITING_USER,
|
|
28
29
|
STATUS_WAITING_OVERSIGHT,
|
|
29
30
|
STATUS_TERMINATED,
|
|
@@ -51,7 +52,7 @@ _HOOK_STATUS_MAP = {
|
|
|
51
52
|
"PostToolUseFailure": STATUS_RUNNING, # Tool failed but agent is still working
|
|
52
53
|
"Stop": STATUS_WAITING_USER,
|
|
53
54
|
"StopFailure": STATUS_ERROR, # API error ended the turn (purple indicator)
|
|
54
|
-
"PermissionRequest":
|
|
55
|
+
"PermissionRequest": STATUS_WAITING_APPROVAL,
|
|
55
56
|
"SessionEnd": STATUS_TERMINATED,
|
|
56
57
|
}
|
|
57
58
|
|