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.
Files changed (123) hide show
  1. {overcode-0.3.4/src/overcode.egg-info → overcode-0.3.6}/PKG-INFO +1 -1
  2. {overcode-0.3.4 → overcode-0.3.6}/pyproject.toml +1 -1
  3. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/bundled_skills.py +5 -2
  4. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/agent.py +82 -0
  5. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/jobs.py +104 -1
  6. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/config.py +28 -0
  7. overcode-0.3.6/src/overcode/daemon_claude_skill.md +68 -0
  8. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/data_export.py +8 -1
  9. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/history_reader.py +21 -2
  10. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/hook_handler.py +47 -1
  11. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/hook_status_detector.py +2 -1
  12. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/launcher.py +41 -2
  13. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/monitor_daemon.py +85 -6
  14. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/monitor_daemon_core.py +2 -33
  15. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/monitor_daemon_state.py +2 -1
  16. overcode-0.3.6/src/overcode/pricing.py +106 -0
  17. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/session_manager.py +3 -0
  18. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/settings.py +2 -17
  19. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/sister_poller.py +5 -2
  20. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_history.py +26 -11
  21. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/summarizer_client.py +78 -6
  22. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/summarizer_component.py +17 -0
  23. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/summary_columns.py +24 -0
  24. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/supervisor_daemon.py +53 -2
  25. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/supervisor_daemon_core.py +14 -7
  26. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tmux_manager.py +7 -0
  27. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui.py +69 -10
  28. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui.tcss +2 -2
  29. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/daemon.py +23 -0
  30. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_logic.py +2 -2
  31. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_render.py +36 -1
  32. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/command_bar.py +36 -6
  33. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/daemon_status_bar.py +18 -15
  34. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/session_summary.py +5 -1
  35. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/status_timeline.py +1 -1
  36. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_api.py +5 -4
  37. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_control_api.py +6 -6
  38. {overcode-0.3.4 → overcode-0.3.6/src/overcode.egg-info}/PKG-INFO +1 -1
  39. {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/SOURCES.txt +1 -0
  40. overcode-0.3.4/src/overcode/daemon_claude_skill.md +0 -183
  41. {overcode-0.3.4 → overcode-0.3.6}/LICENSE +0 -0
  42. {overcode-0.3.4 → overcode-0.3.6}/MANIFEST.in +0 -0
  43. {overcode-0.3.4 → overcode-0.3.6}/README.md +0 -0
  44. {overcode-0.3.4 → overcode-0.3.6}/setup.cfg +0 -0
  45. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/__init__.py +0 -0
  46. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/agent_scanner.py +0 -0
  47. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/claude_config.py +0 -0
  48. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/claude_pid.py +0 -0
  49. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/__init__.py +0 -0
  50. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/__main__.py +0 -0
  51. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/_shared.py +0 -0
  52. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/budget.py +0 -0
  53. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/config.py +0 -0
  54. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/daemon.py +0 -0
  55. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/hooks.py +0 -0
  56. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/monitoring.py +0 -0
  57. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/perms.py +0 -0
  58. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/sister.py +0 -0
  59. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/skills.py +0 -0
  60. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/cli/split.py +0 -0
  61. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/daemon_logging.py +0 -0
  62. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/daemon_utils.py +0 -0
  63. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/dependency_check.py +0 -0
  64. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/duration.py +0 -0
  65. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/exceptions.py +0 -0
  66. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/follow_mode.py +0 -0
  67. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/implementations.py +0 -0
  68. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/interfaces.py +0 -0
  69. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/job_launcher.py +0 -0
  70. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/job_manager.py +0 -0
  71. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/logging_config.py +0 -0
  72. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/mocks.py +0 -0
  73. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/notifier.py +0 -0
  74. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/pid_utils.py +0 -0
  75. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/presence_logger.py +0 -0
  76. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/protocols.py +0 -0
  77. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/sister_controller.py +0 -0
  78. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/ssh_provisioner.py +0 -0
  79. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/standing_instructions.py +0 -0
  80. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_constants.py +0 -0
  81. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_detector.py +0 -0
  82. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_detector_factory.py +0 -0
  83. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/status_patterns.py +0 -0
  84. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/summary_groups.py +0 -0
  85. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/supervisor_layout.sh +0 -0
  86. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/__init__.py +0 -0
  87. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/renderer.py +0 -0
  88. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/tmux_driver.py +0 -0
  89. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/tui_eye.py +0 -0
  90. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/testing/tui_eye_skill.md +0 -0
  91. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/time_context.py +0 -0
  92. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tmux_utils.py +0 -0
  93. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/__init__.py +0 -0
  94. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/input.py +0 -0
  95. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/navigation.py +0 -0
  96. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/session.py +0 -0
  97. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_actions/view.py +0 -0
  98. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_helpers.py +0 -0
  99. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/__init__.py +0 -0
  100. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
  101. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  102. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  103. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/help_overlay.py +0 -0
  104. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
  105. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/job_summary.py +0 -0
  106. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  107. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/preview_pane.py +0 -0
  108. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
  109. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
  110. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/tui_widgets/tui_log_panel.py +0 -0
  111. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/usage_monitor.py +0 -0
  112. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web/__init__.py +0 -0
  113. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web/templates/analytics.html +0 -0
  114. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web/templates/dashboard.html +0 -0
  115. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_chartjs.py +0 -0
  116. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_server.py +0 -0
  117. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_server_runner.py +0 -0
  118. {overcode-0.3.4 → overcode-0.3.6}/src/overcode/web_templates.py +0 -0
  119. {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/dependency_links.txt +0 -0
  120. {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/entry_points.txt +0 -0
  121. {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/requires.txt +0 -0
  122. {overcode-0.3.4 → overcode-0.3.6}/src/overcode.egg-info/top_level.txt +0 -0
  123. {overcode-0.3.4 → overcode-0.3.6}/tests/test_e2e_multi_agent_jokes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: overcode
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: A supervisor for managing multiple Claude Code instances in tmux
5
5
  Author: Mike Bond
6
6
  Project-URL: Homepage, https://github.com/mkb23/overcode
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "overcode"
7
- version = "0.3.4"
7
+ version = "0.3.6"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -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 attach full-tests # Attach to see live output
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
- launcher.attach(job.name)
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 from the already-scoped interactions
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
- encoded = encode_project_path(session.start_directory)
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
- return
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": STATUS_WAITING_USER,
55
+ "PermissionRequest": STATUS_WAITING_APPROVAL,
55
56
  "SessionEnd": STATUS_TERMINATED,
56
57
  }
57
58