overcode 0.3.4__tar.gz → 0.3.5__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 (122) hide show
  1. {overcode-0.3.4/src/overcode.egg-info → overcode-0.3.5}/PKG-INFO +1 -1
  2. {overcode-0.3.4 → overcode-0.3.5}/pyproject.toml +1 -1
  3. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/bundled_skills.py +5 -2
  4. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/agent.py +82 -0
  5. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/jobs.py +104 -1
  6. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/config.py +20 -0
  7. overcode-0.3.5/src/overcode/daemon_claude_skill.md +68 -0
  8. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/history_reader.py +21 -2
  9. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/hook_handler.py +47 -1
  10. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/hook_status_detector.py +2 -1
  11. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/launcher.py +41 -2
  12. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/monitor_daemon.py +75 -5
  13. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/monitor_daemon_state.py +2 -1
  14. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/session_manager.py +3 -0
  15. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/sister_poller.py +2 -1
  16. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/summarizer_client.py +63 -6
  17. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/summary_columns.py +24 -0
  18. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/supervisor_daemon.py +53 -2
  19. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/supervisor_daemon_core.py +14 -7
  20. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tmux_manager.py +7 -0
  21. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui.py +17 -7
  22. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui.tcss +2 -2
  23. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/command_bar.py +36 -6
  24. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/session_summary.py +5 -1
  25. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/web_api.py +1 -0
  26. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/web_control_api.py +6 -6
  27. {overcode-0.3.4 → overcode-0.3.5/src/overcode.egg-info}/PKG-INFO +1 -1
  28. overcode-0.3.4/src/overcode/daemon_claude_skill.md +0 -183
  29. {overcode-0.3.4 → overcode-0.3.5}/LICENSE +0 -0
  30. {overcode-0.3.4 → overcode-0.3.5}/MANIFEST.in +0 -0
  31. {overcode-0.3.4 → overcode-0.3.5}/README.md +0 -0
  32. {overcode-0.3.4 → overcode-0.3.5}/setup.cfg +0 -0
  33. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/__init__.py +0 -0
  34. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/agent_scanner.py +0 -0
  35. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/claude_config.py +0 -0
  36. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/claude_pid.py +0 -0
  37. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/__init__.py +0 -0
  38. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/__main__.py +0 -0
  39. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/_shared.py +0 -0
  40. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/budget.py +0 -0
  41. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/config.py +0 -0
  42. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/daemon.py +0 -0
  43. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/hooks.py +0 -0
  44. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/monitoring.py +0 -0
  45. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/perms.py +0 -0
  46. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/sister.py +0 -0
  47. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/skills.py +0 -0
  48. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/cli/split.py +0 -0
  49. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/daemon_logging.py +0 -0
  50. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/daemon_utils.py +0 -0
  51. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/data_export.py +0 -0
  52. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/dependency_check.py +0 -0
  53. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/duration.py +0 -0
  54. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/exceptions.py +0 -0
  55. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/follow_mode.py +0 -0
  56. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/implementations.py +0 -0
  57. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/interfaces.py +0 -0
  58. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/job_launcher.py +0 -0
  59. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/job_manager.py +0 -0
  60. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/logging_config.py +0 -0
  61. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/mocks.py +0 -0
  62. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/monitor_daemon_core.py +0 -0
  63. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/notifier.py +0 -0
  64. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/pid_utils.py +0 -0
  65. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/presence_logger.py +0 -0
  66. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/protocols.py +0 -0
  67. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/settings.py +0 -0
  68. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/sister_controller.py +0 -0
  69. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/ssh_provisioner.py +0 -0
  70. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/standing_instructions.py +0 -0
  71. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/status_constants.py +0 -0
  72. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/status_detector.py +0 -0
  73. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/status_detector_factory.py +0 -0
  74. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/status_history.py +0 -0
  75. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/status_patterns.py +0 -0
  76. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/summarizer_component.py +0 -0
  77. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/summary_groups.py +0 -0
  78. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/supervisor_layout.sh +0 -0
  79. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/testing/__init__.py +0 -0
  80. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/testing/renderer.py +0 -0
  81. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/testing/tmux_driver.py +0 -0
  82. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/testing/tui_eye.py +0 -0
  83. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/testing/tui_eye_skill.md +0 -0
  84. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/time_context.py +0 -0
  85. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tmux_utils.py +0 -0
  86. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_actions/__init__.py +0 -0
  87. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_actions/daemon.py +0 -0
  88. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_actions/input.py +0 -0
  89. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_actions/navigation.py +0 -0
  90. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_actions/session.py +0 -0
  91. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_actions/view.py +0 -0
  92. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_helpers.py +0 -0
  93. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_logic.py +0 -0
  94. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_render.py +0 -0
  95. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/__init__.py +0 -0
  96. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
  97. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  98. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
  99. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  100. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/help_overlay.py +0 -0
  101. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
  102. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/job_summary.py +0 -0
  103. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  104. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/preview_pane.py +0 -0
  105. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
  106. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/status_timeline.py +0 -0
  107. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
  108. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/tui_widgets/tui_log_panel.py +0 -0
  109. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/usage_monitor.py +0 -0
  110. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/web/__init__.py +0 -0
  111. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/web/templates/analytics.html +0 -0
  112. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/web/templates/dashboard.html +0 -0
  113. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/web_chartjs.py +0 -0
  114. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/web_server.py +0 -0
  115. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/web_server_runner.py +0 -0
  116. {overcode-0.3.4 → overcode-0.3.5}/src/overcode/web_templates.py +0 -0
  117. {overcode-0.3.4 → overcode-0.3.5}/src/overcode.egg-info/SOURCES.txt +0 -0
  118. {overcode-0.3.4 → overcode-0.3.5}/src/overcode.egg-info/dependency_links.txt +0 -0
  119. {overcode-0.3.4 → overcode-0.3.5}/src/overcode.egg-info/entry_points.txt +0 -0
  120. {overcode-0.3.4 → overcode-0.3.5}/src/overcode.egg-info/requires.txt +0 -0
  121. {overcode-0.3.4 → overcode-0.3.5}/src/overcode.egg-info/top_level.txt +0 -0
  122. {overcode-0.3.4 → overcode-0.3.5}/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.5
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.5"
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
@@ -128,6 +130,7 @@ def get_summarizer_config() -> dict:
128
130
  api_key = os.environ.get(api_key_var)
129
131
 
130
132
  return {
133
+ "api_type": api_type,
131
134
  "api_url": api_url,
132
135
  "model": model,
133
136
  "api_key": api_key,
@@ -340,6 +343,7 @@ def get_new_agent_defaults() -> dict:
340
343
  return {
341
344
  "bypass_permissions": bool(defaults.get("bypass_permissions", False)),
342
345
  "agent_teams": bool(defaults.get("agent_teams", False)),
346
+ "provider": defaults.get("provider", "web"),
343
347
  }
344
348
 
345
349
 
@@ -354,6 +358,22 @@ def save_new_agent_defaults(defaults: dict) -> None:
354
358
  save_config(config)
355
359
 
356
360
 
361
+ def get_bedrock_config() -> dict:
362
+ """Get AWS Bedrock configuration.
363
+
364
+ Config format in ~/.overcode/config.yaml:
365
+ bedrock:
366
+ region: us-east-1
367
+
368
+ Returns:
369
+ Dict with region (str).
370
+ """
371
+ bedrock = _get_config_value("bedrock", {})
372
+ return {
373
+ "region": bedrock.get("region", "us-east-1"),
374
+ }
375
+
376
+
357
377
  def get_jobs_retention_hours() -> float:
358
378
  """Get job retention period in hours.
359
379
 
@@ -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
@@ -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
 
@@ -141,6 +141,7 @@ class ClaudeLauncher:
141
141
  agent_teams: bool = False,
142
142
  claude_agent: Optional[str] = None,
143
143
  model: Optional[str] = None,
144
+ provider: str = "web",
144
145
  ) -> dict:
145
146
  """Build the kwargs dict for SessionManager.create_session.
146
147
 
@@ -160,6 +161,7 @@ class ClaudeLauncher:
160
161
  agent_teams=agent_teams,
161
162
  claude_agent=claude_agent,
162
163
  model=model,
164
+ provider=provider,
163
165
  session_id=session_id,
164
166
  )
165
167
 
@@ -188,6 +190,13 @@ class ClaudeLauncher:
188
190
  if metadata.get('agent_teams'):
189
191
  env_prefix += " CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1"
190
192
 
193
+ if metadata.get('provider') == 'bedrock':
194
+ env_prefix += " CLAUDE_CODE_USE_BEDROCK=1"
195
+ # Use configured region or default to us-east-1
196
+ from .config import get_bedrock_config
197
+ bedrock_cfg = get_bedrock_config()
198
+ env_prefix += f" AWS_REGION={bedrock_cfg['region']}"
199
+
191
200
  mock_scenario = os.environ.get("MOCK_SCENARIO")
192
201
  if mock_scenario:
193
202
  cmd_str = f"MOCK_SCENARIO={mock_scenario} {env_prefix} python {shlex.join(claude_cmd)}"
@@ -221,6 +230,7 @@ class ClaudeLauncher:
221
230
  budget_usd: Optional[float] = None,
222
231
  claude_agent: Optional[str] = None,
223
232
  model: Optional[str] = None,
233
+ provider: str = "web",
224
234
  ) -> Optional[Session]:
225
235
  """
226
236
  Launch an interactive Claude Code session in a tmux window.
@@ -236,6 +246,7 @@ class ClaudeLauncher:
236
246
  If not set, auto-detects from OVERCODE_SESSION_NAME env var.
237
247
  allowed_tools: Comma-separated tool list for --allowedTools
238
248
  extra_claude_args: Extra Claude CLI flags (each a space-separated string)
249
+ provider: API provider — "web" (Claude.ai OAuth) or "bedrock" (AWS Bedrock)
239
250
 
240
251
  Returns:
241
252
  Session object if successful, None otherwise
@@ -329,7 +340,7 @@ class ClaudeLauncher:
329
340
  standing_instructions=default_instructions,
330
341
  permissiveness_mode=perm_mode, allowed_tools=allowed_tools,
331
342
  extra_claude_args=extra_claude_args, agent_teams=agent_teams,
332
- claude_agent=claude_agent, model=model,
343
+ claude_agent=claude_agent, model=model, provider=provider,
333
344
  )
334
345
 
335
346
  session = self._prepare_and_launch(
@@ -377,16 +388,42 @@ class ClaudeLauncher:
377
388
  ) -> bool:
378
389
  """Poll pane content until Claude's input prompt appears.
379
390
 
391
+ Automatically handles startup dialogs:
392
+ 1. Workspace trust dialog -- presses Enter to accept default
393
+ 2. Bypass permissions warning -- selects "Yes, I accept" (Down + Enter)
394
+
380
395
  Returns True if prompt detected, False on timeout.
381
396
  """
382
397
  from .status_patterns import strip_ansi
398
+ from .tmux_utils import _build_tmux_cmd
399
+
400
+ target = tmux_window_target(self.tmux.session_name, window_name)
401
+ tmux_cmd = _build_tmux_cmd()
402
+ trust_accepted = False
403
+ perms_accepted = False
383
404
 
384
405
  deadline = time.time() + timeout
385
406
  while time.time() < deadline:
386
407
  content = get_tmux_pane_content(
387
- self.tmux.session_name, window_name, lines=5
408
+ self.tmux.session_name, window_name, lines=20
388
409
  )
389
410
  if content:
411
+ # Trust prompt: default is accept, just Enter
412
+ if not trust_accepted and "I trust this folder" in content:
413
+ subprocess.run(tmux_cmd + ['send-keys', '-t', target, '', 'Enter'], timeout=5)
414
+ trust_accepted = True
415
+ time.sleep(1.5)
416
+ continue
417
+
418
+ # Permissions warning: navigate Down to "Yes, I accept", then Enter
419
+ if not perms_accepted and "Yes, I accept" in content:
420
+ subprocess.run(tmux_cmd + ['send-keys', '-t', target, 'Down'], timeout=5)
421
+ time.sleep(0.3)
422
+ subprocess.run(tmux_cmd + ['send-keys', '-t', target, '', 'Enter'], timeout=5)
423
+ perms_accepted = True
424
+ time.sleep(2.0)
425
+ continue
426
+
390
427
  for line in content.split('\n'):
391
428
  cleaned = strip_ansi(line).strip()
392
429
  if cleaned in self.PROMPT_READY_CHARS:
@@ -501,6 +538,7 @@ class ClaudeLauncher:
501
538
  extra_claude_args=source_session.extra_claude_args,
502
539
  resume_session_id=source_session.active_claude_session_id,
503
540
  fork=True,
541
+ model=source_session.model,
504
542
  )
505
543
 
506
544
  perm_mode = source_session.permissiveness_mode or "normal"
@@ -512,6 +550,7 @@ class ClaudeLauncher:
512
550
  permissiveness_mode=perm_mode, allowed_tools=source_session.allowed_tools,
513
551
  extra_claude_args=source_session.extra_claude_args,
514
552
  agent_teams=source_session.agent_teams, claude_agent=source_session.claude_agent,
553
+ model=source_session.model, provider=source_session.provider,
515
554
  )
516
555
 
517
556
  session = self._prepare_and_launch(