overcode 0.3.3__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.3/src/overcode.egg-info → overcode-0.3.5}/PKG-INFO +1 -1
  2. {overcode-0.3.3 → overcode-0.3.5}/pyproject.toml +1 -1
  3. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/bundled_skills.py +31 -2
  4. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/agent.py +82 -0
  5. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/config.py +2 -2
  6. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/jobs.py +104 -1
  7. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/monitoring.py +1 -1
  8. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/split.py +50 -8
  9. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/config.py +48 -7
  10. overcode-0.3.5/src/overcode/daemon_claude_skill.md +68 -0
  11. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/dependency_check.py +33 -1
  12. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/history_reader.py +21 -2
  13. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/hook_handler.py +51 -5
  14. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/hook_status_detector.py +20 -1
  15. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/launcher.py +41 -2
  16. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/monitor_daemon.py +101 -9
  17. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/monitor_daemon_state.py +3 -2
  18. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/session_manager.py +17 -2
  19. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/settings.py +17 -0
  20. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/sister_controller.py +22 -4
  21. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/sister_poller.py +28 -6
  22. overcode-0.3.5/src/overcode/ssh_provisioner.py +234 -0
  23. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_detector_factory.py +6 -0
  24. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/summarizer_client.py +63 -6
  25. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/summary_columns.py +98 -7
  26. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/summary_groups.py +1 -1
  27. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/supervisor_daemon.py +53 -2
  28. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/supervisor_daemon_core.py +14 -7
  29. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/time_context.py +21 -16
  30. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tmux_manager.py +90 -0
  31. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui.py +285 -22
  32. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui.tcss +13 -2
  33. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/daemon.py +9 -0
  34. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/navigation.py +3 -0
  35. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/session.py +13 -13
  36. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/view.py +28 -16
  37. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/__init__.py +2 -0
  38. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/command_bar.py +53 -6
  39. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/help_overlay.py +37 -22
  40. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/session_summary.py +12 -3
  41. overcode-0.3.5/src/overcode/tui_widgets/tui_log_panel.py +145 -0
  42. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_api.py +14 -1
  43. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_control_api.py +48 -17
  44. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_server.py +4 -1
  45. {overcode-0.3.3 → overcode-0.3.5/src/overcode.egg-info}/PKG-INFO +1 -1
  46. {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/SOURCES.txt +2 -0
  47. overcode-0.3.3/src/overcode/daemon_claude_skill.md +0 -183
  48. {overcode-0.3.3 → overcode-0.3.5}/LICENSE +0 -0
  49. {overcode-0.3.3 → overcode-0.3.5}/MANIFEST.in +0 -0
  50. {overcode-0.3.3 → overcode-0.3.5}/README.md +0 -0
  51. {overcode-0.3.3 → overcode-0.3.5}/setup.cfg +0 -0
  52. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/__init__.py +0 -0
  53. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/agent_scanner.py +0 -0
  54. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/claude_config.py +0 -0
  55. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/claude_pid.py +0 -0
  56. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/__init__.py +0 -0
  57. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/__main__.py +0 -0
  58. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/_shared.py +0 -0
  59. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/budget.py +0 -0
  60. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/daemon.py +0 -0
  61. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/hooks.py +0 -0
  62. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/perms.py +0 -0
  63. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/sister.py +0 -0
  64. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/skills.py +0 -0
  65. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/daemon_logging.py +0 -0
  66. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/daemon_utils.py +0 -0
  67. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/data_export.py +0 -0
  68. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/duration.py +0 -0
  69. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/exceptions.py +0 -0
  70. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/follow_mode.py +0 -0
  71. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/implementations.py +0 -0
  72. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/interfaces.py +0 -0
  73. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/job_launcher.py +0 -0
  74. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/job_manager.py +0 -0
  75. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/logging_config.py +0 -0
  76. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/mocks.py +0 -0
  77. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/monitor_daemon_core.py +0 -0
  78. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/notifier.py +0 -0
  79. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/pid_utils.py +0 -0
  80. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/presence_logger.py +0 -0
  81. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/protocols.py +0 -0
  82. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/standing_instructions.py +0 -0
  83. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_constants.py +0 -0
  84. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_detector.py +0 -0
  85. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_history.py +0 -0
  86. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_patterns.py +0 -0
  87. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/summarizer_component.py +0 -0
  88. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/supervisor_layout.sh +0 -0
  89. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/__init__.py +0 -0
  90. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/renderer.py +0 -0
  91. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/tmux_driver.py +0 -0
  92. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/tui_eye.py +0 -0
  93. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/tui_eye_skill.md +0 -0
  94. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tmux_utils.py +0 -0
  95. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/__init__.py +0 -0
  96. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/input.py +0 -0
  97. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_helpers.py +0 -0
  98. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_logic.py +0 -0
  99. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_render.py +0 -0
  100. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
  101. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  102. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
  103. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  104. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
  105. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/job_summary.py +0 -0
  106. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  107. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/preview_pane.py +0 -0
  108. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
  109. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/status_timeline.py +0 -0
  110. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
  111. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/usage_monitor.py +0 -0
  112. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web/__init__.py +0 -0
  113. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web/templates/analytics.html +0 -0
  114. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web/templates/dashboard.html +0 -0
  115. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_chartjs.py +0 -0
  116. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_server_runner.py +0 -0
  117. {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_templates.py +0 -0
  118. {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/dependency_links.txt +0 -0
  119. {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/entry_points.txt +0 -0
  120. {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/requires.txt +0 -0
  121. {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/top_level.txt +0 -0
  122. {overcode-0.3.3 → 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.3
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.3"
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
 
@@ -248,6 +251,32 @@ Jobs are visible in the TUI jobs view (press `J`) and auto-clean after 24h (conf
248
251
  }
249
252
 
250
253
 
254
+ def get_available_skills(project_dir: str | None = None) -> list[str]:
255
+ """Scan for installed skill directories (user-level + project-level).
256
+
257
+ Returns sorted list of skill names found in ~/.claude/skills/
258
+ and optionally .claude/skills/ relative to project_dir.
259
+ """
260
+ skills: set[str] = set()
261
+
262
+ # User-level skills
263
+ user_skills = Path.home() / ".claude" / "skills"
264
+ if user_skills.is_dir():
265
+ for d in user_skills.iterdir():
266
+ if d.is_dir() and (d / "SKILL.md").exists():
267
+ skills.add(d.name)
268
+
269
+ # Project-level skills
270
+ if project_dir:
271
+ proj_skills = Path(project_dir) / ".claude" / "skills"
272
+ if proj_skills.is_dir():
273
+ for d in proj_skills.iterdir():
274
+ if d.is_dir() and (d / "SKILL.md").exists():
275
+ skills.add(d.name)
276
+
277
+ return sorted(skills)
278
+
279
+
251
280
  def any_skills_stale() -> bool:
252
281
  """Check if any installed skills are outdated vs bundled versions."""
253
282
  base = Path.home() / ".claude" / "skills"
@@ -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
@@ -44,8 +44,8 @@ CONFIG_TEMPLATE = """\
44
44
  # start: "09:00"
45
45
  # end: "17:00"
46
46
 
47
- # Time context hook settings (for 'overcode time-context')
48
- # time_context:
47
+ # Enhanced context hook settings (agent identity, clock, presence, uptime)
48
+ # enhanced_context:
49
49
  # office_start: 9
50
50
  # office_end: 17
51
51
  # heartbeat_interval_minutes: 15 # omit to disable
@@ -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")],
@@ -18,7 +18,7 @@ def hook_handler_cmd():
18
18
 
19
19
  Called by Claude Code hooks, not by users directly.
20
20
  Reads event JSON from stdin, writes state for status detection,
21
- and outputs time-context for UserPromptSubmit events.
21
+ and outputs enhanced context for UserPromptSubmit events.
22
22
  """
23
23
  from ..hook_handler import handle_hook_event
24
24
 
@@ -263,20 +263,51 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
263
263
  # --- Scrollback for the nested tmux in the bottom pane ---
264
264
  # The bottom pane runs a nested tmux client. The outer tmux
265
265
  # intercepts the prefix key, so copy-mode can't be entered
266
- # normally. These bindings use `copy-mode -t` to directly enter
267
- # copy mode in the inner session via the tmux API.
266
+ # normally.
267
+ #
268
+ # For local agents: use `copy-mode -t` to directly enter copy mode
269
+ # in the inner session via the tmux API (the linked session shares
270
+ # the actual agent panes, so scrollback is real).
271
+ #
272
+ # For SSH proxy windows (name starts with "ssh:"): the linked
273
+ # session pane is an SSH+tmux-attach client whose local scrollback
274
+ # is just rendering frames. Instead, forward PageUp/scroll through
275
+ # to the remote tmux which has the actual agent scrollback.
268
276
  if linked_session:
269
277
  # PageUp: enter copy mode + scroll up, but only when in the
270
278
  # bottom pane (pane_index != base) of the split window.
271
279
  # Top pane (Textual TUI) and other windows get normal PageUp.
280
+ #
281
+ # For SSH proxy windows: forward PageUp through the SSH pane
282
+ # so the *remote* tmux enters copy mode (the local proxy pane
283
+ # has no real scrollback — just rendered inner-tmux frames).
284
+ # For local agents: enter copy mode directly on the linked
285
+ # session which shares the real agent pane.
286
+ #
287
+ # Detection uses the @is_ssh_proxy window option (set by
288
+ # create_ssh_proxy_window) via a shell-based if-shell — this
289
+ # avoids tmux format expansion issues in stored bindings.
290
+ _ssh_check = (
291
+ f"tmux show-window-option -t {linked_session} -v @is_ssh_proxy "
292
+ "2>/dev/null | grep -q on"
293
+ )
272
294
  _tmux(
273
295
  "bind-key", "-n", "PPage",
274
296
  "if-shell", "-F",
275
297
  f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},{_base}}}}}",
276
- f"copy-mode -t {linked_session} -u",
298
+ # SSH proxy: send prefix + PPage so the REMOTE tmux enters
299
+ # copy mode (the remote's root-table PPage is overridden by
300
+ # overcode, but prefix PPage still maps to copy-mode -u).
301
+ # Local: enter copy mode directly on the linked session.
302
+ f'if-shell "{_ssh_check}" '
303
+ f'"send-keys -t {linked_session} C-b ; '
304
+ f'send-keys -t {linked_session} PPage" '
305
+ f'"copy-mode -t {linked_session} -u"',
277
306
  "send-keys PPage",
278
307
  )
279
308
  # PageDown in the inner session's copy mode
309
+ # (send-keys works for both local and SSH proxy — for local it
310
+ # controls copy mode, for SSH proxy it forwards to remote tmux)
280
311
  _tmux(
281
312
  "bind-key", "-n", "NPage",
282
313
  "if-shell", "-F",
@@ -288,8 +319,9 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
288
319
  # Without this, scrolling enters copy mode in the outer pane
289
320
  # which has no scrollback (just rendered inner tmux frames).
290
321
  #
291
- # Strategy: enter copy mode in the inner session (no-op if
292
- # already active), then send scroll-up/down commands to it.
322
+ # For local agents: enter copy mode in the inner session then
323
+ # send scroll commands. For SSH proxies: forward mouse events
324
+ # to the remote tmux through the pane.
293
325
  _in_bottom = (
294
326
  f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},"
295
327
  f"#{{!=:#{{pane_index}},{_base}}}}}"
@@ -297,8 +329,14 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
297
329
  _tmux(
298
330
  "bind-key", "-n", "WheelUpPane",
299
331
  "if-shell", "-F", _in_bottom,
300
- f"copy-mode -t {linked_session} -e ; "
301
- f"send-keys -t {linked_session} -X -N 3 scroll-up",
332
+ # SSH proxy: send prefix+PPage to enter copy mode on remote
333
+ # (no-op if already in copy mode, then PPage scrolls up).
334
+ # Local: enter copy mode + scroll up directly.
335
+ f'if-shell "{_ssh_check}" '
336
+ f'"send-keys -t {linked_session} C-b ; '
337
+ f'send-keys -t {linked_session} PPage" '
338
+ f'"copy-mode -t {linked_session} -e ; '
339
+ f'send-keys -t {linked_session} -X -N 3 scroll-up"',
302
340
  # Default behaviour for other contexts
303
341
  "if-shell -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' "
304
342
  "'send-keys -M' 'copy-mode -e'",
@@ -306,7 +344,11 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
306
344
  _tmux(
307
345
  "bind-key", "-n", "WheelDownPane",
308
346
  "if-shell", "-F", _in_bottom,
309
- f"send-keys -t {linked_session} -X -N 3 scroll-down",
347
+ # SSH proxy: send NPage to remote (works in copy mode).
348
+ # Local: send scroll-down in local copy mode.
349
+ f'if-shell "{_ssh_check}" '
350
+ f'"send-keys -t {linked_session} NPage" '
351
+ f'"send-keys -t {linked_session} -X -N 3 scroll-down"',
310
352
  "send-keys -M",
311
353
  )
312
354
 
@@ -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,
@@ -197,23 +200,35 @@ def get_web_time_presets() -> list:
197
200
  ]
198
201
 
199
202
 
200
- def get_time_context_config() -> dict:
201
- """Get time context configuration for the time-context hook.
203
+ def get_enhanced_context_config() -> dict:
204
+ """Get enhanced context configuration for the enhanced-context hook.
202
205
 
203
206
  Config format in ~/.overcode/config.yaml:
204
- time_context:
207
+ enhanced_context:
205
208
  office_start: 9
206
209
  office_end: 17
207
210
  heartbeat_interval_minutes: 15 # omit to disable
208
211
 
212
+ Falls back to legacy 'time_context' key for backwards compatibility.
213
+
209
214
  Returns:
210
215
  Dict with office_start (int), office_end (int),
211
216
  heartbeat_interval_minutes (Optional[int])
212
217
  """
218
+ # Try new key first, fall back to legacy key
219
+ office_start = _get_config_value("enhanced_context.office_start")
220
+ if office_start is None:
221
+ office_start = _get_config_value("time_context.office_start", 9)
222
+ office_end = _get_config_value("enhanced_context.office_end")
223
+ if office_end is None:
224
+ office_end = _get_config_value("time_context.office_end", 17)
225
+ hb_interval = _get_config_value("enhanced_context.heartbeat_interval_minutes")
226
+ if hb_interval is None:
227
+ hb_interval = _get_config_value("time_context.heartbeat_interval_minutes")
213
228
  return {
214
- "office_start": _get_config_value("time_context.office_start", 9),
215
- "office_end": _get_config_value("time_context.office_end", 17),
216
- "heartbeat_interval_minutes": _get_config_value("time_context.heartbeat_interval_minutes"),
229
+ "office_start": office_start,
230
+ "office_end": office_end,
231
+ "heartbeat_interval_minutes": hb_interval,
217
232
  }
218
233
 
219
234
 
@@ -328,6 +343,7 @@ def get_new_agent_defaults() -> dict:
328
343
  return {
329
344
  "bypass_permissions": bool(defaults.get("bypass_permissions", False)),
330
345
  "agent_teams": bool(defaults.get("agent_teams", False)),
346
+ "provider": defaults.get("provider", "web"),
331
347
  }
332
348
 
333
349
 
@@ -342,6 +358,22 @@ def save_new_agent_defaults(defaults: dict) -> None:
342
358
  save_config(config)
343
359
 
344
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
+
345
377
  def get_jobs_retention_hours() -> float:
346
378
  """Get job retention period in hours.
347
379
 
@@ -365,9 +397,11 @@ def get_sisters_config() -> List[dict]:
365
397
  - name: "desktop"
366
398
  url: "http://localhost:25337"
367
399
  api_key: "secret"
400
+ ssh: "user@desktop" # optional: enables tmux attach + auto-provisioning
401
+ tmux_session: "agents" # optional: remote tmux session name (default: "agents")
368
402
 
369
403
  Returns:
370
- List of dicts with name, url, and optional api_key. Empty list if unconfigured.
404
+ List of dicts with name, url, and optional api_key/ssh/tmux_session. Empty list if unconfigured.
371
405
  """
372
406
  sisters = _get_config_value("sisters", [])
373
407
  if not isinstance(sisters, list):
@@ -385,6 +419,13 @@ def get_sisters_config() -> List[dict]:
385
419
  api_key = s.get("api_key")
386
420
  if api_key:
387
421
  entry["api_key"] = api_key
422
+ # SSH connectivity (optional)
423
+ ssh = s.get("ssh")
424
+ if ssh:
425
+ entry["ssh"] = ssh
426
+ tmux_session = s.get("tmux_session")
427
+ if tmux_session:
428
+ entry["tmux_session"] = tmux_session
388
429
  result.append(entry)
389
430
 
390
431
  return result
@@ -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