overcode 0.2.1__tar.gz → 0.2.3__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 (111) hide show
  1. {overcode-0.2.1/src/overcode.egg-info → overcode-0.2.3}/PKG-INFO +1 -1
  2. {overcode-0.2.1 → overcode-0.2.3}/pyproject.toml +1 -1
  3. overcode-0.2.3/src/overcode/agent_scanner.py +38 -0
  4. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/bundled_skills.py +7 -2
  5. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/agent.py +165 -32
  6. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/daemon.py +1 -1
  7. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/config.py +42 -0
  8. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/history_reader.py +69 -74
  9. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/launcher.py +57 -1
  10. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/monitor_daemon.py +65 -21
  11. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/monitor_daemon_state.py +13 -170
  12. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/presence_logger.py +4 -4
  13. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/session_manager.py +10 -1
  14. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/settings.py +18 -18
  15. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/sister_poller.py +3 -0
  16. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_patterns.py +55 -1
  17. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/summary_columns.py +320 -66
  18. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/summary_groups.py +7 -39
  19. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui.py +361 -105
  20. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui.tcss +43 -1
  21. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/input.py +3 -3
  22. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/session.py +28 -3
  23. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/view.py +36 -7
  24. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_logic.py +5 -4
  25. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/__init__.py +4 -0
  26. overcode-0.2.3/src/overcode/tui_widgets/agent_select_modal.py +126 -0
  27. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/command_bar.py +207 -24
  28. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/daemon_status_bar.py +39 -13
  29. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/help_overlay.py +4 -0
  30. overcode-0.2.3/src/overcode/tui_widgets/new_agent_defaults_modal.py +131 -0
  31. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/session_summary.py +34 -33
  32. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/status_timeline.py +1 -5
  33. overcode-0.2.3/src/overcode/tui_widgets/summary_config_modal.py +292 -0
  34. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_api.py +6 -0
  35. {overcode-0.2.1 → overcode-0.2.3/src/overcode.egg-info}/PKG-INFO +1 -1
  36. {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/SOURCES.txt +3 -0
  37. overcode-0.2.1/src/overcode/tui_widgets/summary_config_modal.py +0 -164
  38. {overcode-0.2.1 → overcode-0.2.3}/LICENSE +0 -0
  39. {overcode-0.2.1 → overcode-0.2.3}/MANIFEST.in +0 -0
  40. {overcode-0.2.1 → overcode-0.2.3}/README.md +0 -0
  41. {overcode-0.2.1 → overcode-0.2.3}/setup.cfg +0 -0
  42. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/__init__.py +0 -0
  43. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/claude_config.py +0 -0
  44. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/__init__.py +0 -0
  45. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/__main__.py +0 -0
  46. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/_shared.py +0 -0
  47. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/budget.py +0 -0
  48. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/config.py +0 -0
  49. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/hooks.py +0 -0
  50. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/monitoring.py +0 -0
  51. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/perms.py +0 -0
  52. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/sister.py +0 -0
  53. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/cli/skills.py +0 -0
  54. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/daemon_claude_skill.md +0 -0
  55. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/daemon_logging.py +0 -0
  56. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/daemon_utils.py +0 -0
  57. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/data_export.py +0 -0
  58. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/dependency_check.py +0 -0
  59. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/exceptions.py +0 -0
  60. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/follow_mode.py +0 -0
  61. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/hook_handler.py +0 -0
  62. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/hook_status_detector.py +0 -0
  63. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/implementations.py +0 -0
  64. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/interfaces.py +0 -0
  65. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/logging_config.py +0 -0
  66. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/mocks.py +0 -0
  67. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/monitor_daemon_core.py +0 -0
  68. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/notifier.py +0 -0
  69. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/pid_utils.py +0 -0
  70. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/protocols.py +0 -0
  71. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/sister_controller.py +0 -0
  72. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/standing_instructions.py +0 -0
  73. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_constants.py +0 -0
  74. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_detector.py +0 -0
  75. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_detector_factory.py +0 -0
  76. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/status_history.py +0 -0
  77. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/summarizer_client.py +0 -0
  78. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/summarizer_component.py +0 -0
  79. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/supervisor_daemon.py +0 -0
  80. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/supervisor_daemon_core.py +0 -0
  81. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/supervisor_layout.sh +0 -0
  82. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/__init__.py +0 -0
  83. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/renderer.py +0 -0
  84. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/tmux_driver.py +0 -0
  85. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/tui_eye.py +0 -0
  86. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/testing/tui_eye_skill.md +0 -0
  87. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/time_context.py +0 -0
  88. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tmux_manager.py +0 -0
  89. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tmux_utils.py +0 -0
  90. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/__init__.py +0 -0
  91. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/daemon.py +0 -0
  92. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_actions/navigation.py +0 -0
  93. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_helpers.py +0 -0
  94. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_render.py +0 -0
  95. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  96. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  97. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/tui_widgets/preview_pane.py +0 -0
  98. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/usage_monitor.py +0 -0
  99. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web/__init__.py +0 -0
  100. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web/templates/analytics.html +0 -0
  101. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web/templates/dashboard.html +0 -0
  102. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_chartjs.py +0 -0
  103. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_control_api.py +0 -0
  104. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_server.py +0 -0
  105. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_server_runner.py +0 -0
  106. {overcode-0.2.1 → overcode-0.2.3}/src/overcode/web_templates.py +0 -0
  107. {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/dependency_links.txt +0 -0
  108. {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/entry_points.txt +0 -0
  109. {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/requires.txt +0 -0
  110. {overcode-0.2.1 → overcode-0.2.3}/src/overcode.egg-info/top_level.txt +0 -0
  111. {overcode-0.2.1 → overcode-0.2.3}/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.2.1
3
+ Version: 0.2.3
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.2.1"
7
+ version = "0.2.3"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -0,0 +1,38 @@
1
+ """
2
+ Scan for Claude Code agent definitions (.md files in .claude/agents/).
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+
9
+ def scan_agents(directory: str) -> List[str]:
10
+ """Scan for available Claude agent definitions.
11
+
12
+ Checks both project-level (.claude/agents/) and user-level (~/.claude/agents/)
13
+ directories for .md files. Returns deduplicated, sorted list of agent names
14
+ (filenames without .md extension).
15
+
16
+ Args:
17
+ directory: Project directory to scan for project-level agents.
18
+
19
+ Returns:
20
+ Sorted list of agent names. Empty list if none found.
21
+ """
22
+ names: set = set()
23
+
24
+ # Project-level agents
25
+ project_dir = Path(directory) / ".claude" / "agents"
26
+ if project_dir.is_dir():
27
+ for f in project_dir.iterdir():
28
+ if f.is_file() and f.suffix == ".md":
29
+ names.add(f.stem)
30
+
31
+ # User-level agents
32
+ user_dir = Path.home() / ".claude" / "agents"
33
+ if user_dir.is_dir():
34
+ for f in user_dir.iterdir():
35
+ if f.is_file() and f.suffix == ".md":
36
+ names.add(f.stem)
37
+
38
+ return sorted(names)
@@ -32,6 +32,7 @@ overcode launch -n <name> [-d <path>] [-p "<prompt>"] [--follow] [--bypass-permi
32
32
  overcode launch -n <name> --follow --oversight-timeout 5m -p "... When done: overcode report --status success"
33
33
  overcode launch -n <name> --allowed-tools "Read,Glob,Grep" --skip-permissions
34
34
  overcode launch -n <name> --claude-arg "--model haiku" --claude-arg "--effort low"
35
+ overcode launch -n <name> --budget 2.00 -p "..." # Set cost budget (auto-deducted from parent)
35
36
 
36
37
  # Monitor
37
38
  overcode list [name] [--show-done]
@@ -127,8 +128,8 @@ overcode launch --name fix-auth-bug -d ~/project --follow --bypass-permissions \
127
128
  ## Parallel (Non-Blocking)
128
129
 
129
130
  ```bash
130
- overcode launch -n refactor-api -d ~/project -p "Refactor REST API. When done: overcode report --status success" --bypass-permissions
131
- overcode launch -n write-tests -d ~/project -p "Write auth tests. When done: overcode report --status success" --bypass-permissions
131
+ overcode launch -n refactor-api -d ~/project --budget 3.00 -p "Refactor REST API. When done: overcode report --status success" --bypass-permissions
132
+ overcode launch -n write-tests -d ~/project --budget 2.00 -p "Write auth tests. When done: overcode report --status success" --bypass-permissions
132
133
 
133
134
  overcode list # Monitor progress
134
135
  overcode show refactor-api -n 100 # Read output
@@ -163,6 +164,10 @@ Children must call `overcode report --status success|failure [--reason "..."]` w
163
164
  ## Budget Control
164
165
 
165
166
  ```bash
167
+ # Preferred: set budget at launch (auto-deducted from parent if parent has budget)
168
+ overcode launch -n child-agent -d ~/project --bypass-permissions --budget 2.00 -p "..."
169
+
170
+ # Manual budget management
166
171
  overcode budget transfer my-agent child-agent 2.00 # Transfer from your budget
167
172
  overcode budget set child-agent 3.00 # Set directly
168
173
  ```
@@ -59,11 +59,73 @@ def launch(
59
59
  Optional[List[str]],
60
60
  typer.Option("--claude-arg", help="Extra Claude CLI flag (repeatable, e.g. '--model haiku')"),
61
61
  ] = None,
62
+ budget: Annotated[
63
+ Optional[float],
64
+ typer.Option("--budget", "-b", help="Cost budget in USD (deducted from parent if parent has budget)"),
65
+ ] = None,
66
+ agent: Annotated[
67
+ Optional[str],
68
+ typer.Option("--agent", "-a", help="Claude agent to run as (from .claude/agents/)"),
69
+ ] = None,
70
+ teams: Annotated[
71
+ bool,
72
+ typer.Option("--teams", help="Enable Claude Code agent teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)"),
73
+ ] = False,
74
+ sister: Annotated[
75
+ Optional[str],
76
+ typer.Option("--sister", "-S", help="Launch on a remote sister machine (by name from config)"),
77
+ ] = None,
62
78
  session: SessionOption = "agents",
63
79
  ):
64
80
  """Launch a new Claude agent."""
65
81
  import os
66
82
 
83
+ # Remote launch via sister
84
+ if sister:
85
+ from ..config import get_sister_by_name
86
+ from ..sister_controller import SisterController
87
+
88
+ sister_config = get_sister_by_name(sister)
89
+ if not sister_config:
90
+ from ..config import get_sisters_config
91
+ available = [s["name"] for s in get_sisters_config()]
92
+ if available:
93
+ rprint(f"[red]Error: Sister '{sister}' not found. Available: {', '.join(available)}[/red]")
94
+ else:
95
+ rprint(f"[red]Error: No sisters configured. Add sisters to ~/.overcode/config.yaml[/red]")
96
+ raise typer.Exit(code=1)
97
+
98
+ remote_dir = directory if directory else "."
99
+ if not directory:
100
+ rprint("[yellow]Warning:[/yellow] No --directory given; remote agent will use '.'")
101
+
102
+ # Map permission flags to permissions string
103
+ if bypass_permissions:
104
+ permissions = "bypass"
105
+ elif skip_permissions:
106
+ permissions = "skip"
107
+ else:
108
+ permissions = "normal"
109
+
110
+ controller = SisterController()
111
+ result = controller.launch_agent(
112
+ sister_url=sister_config["url"],
113
+ api_key=sister_config.get("api_key", ""),
114
+ directory=remote_dir,
115
+ name=name,
116
+ prompt=prompt,
117
+ permissions=permissions,
118
+ )
119
+
120
+ if result.ok:
121
+ rprint(f"\n[green]✓[/green] Remote agent '[bold]{name}[/bold]' launched on [bold]{sister}[/bold]")
122
+ if prompt:
123
+ rprint(" Initial prompt sent")
124
+ else:
125
+ rprint(f"[red]Error: Remote launch failed: {result.error}[/red]")
126
+ raise typer.Exit(code=1)
127
+ return
128
+
67
129
  # Parse oversight policy
68
130
  oversight_policy = "wait"
69
131
  oversight_timeout_seconds = 0.0
@@ -104,6 +166,9 @@ def launch(
104
166
  parent_name=parent,
105
167
  allowed_tools=allowed_tools,
106
168
  extra_claude_args=claude_args,
169
+ agent_teams=teams,
170
+ budget_usd=budget,
171
+ claude_agent=agent,
107
172
  )
108
173
 
109
174
  if result:
@@ -116,6 +181,12 @@ def launch(
116
181
  rprint(f" Allowed tools: {allowed_tools}")
117
182
  if claude_args:
118
183
  rprint(f" Extra Claude args: {' '.join(claude_args)}")
184
+ if agent:
185
+ rprint(f" Agent: {agent}")
186
+ if teams:
187
+ rprint(" Agent teams: enabled")
188
+ if budget is not None and budget > 0:
189
+ rprint(f" Budget: ${budget:.2f}")
119
190
 
120
191
  # Store oversight policy on session
121
192
  if oversight_policy != "wait" or oversight_timeout_seconds > 0:
@@ -155,6 +226,15 @@ def list_agents(
155
226
  sisters: Annotated[
156
227
  bool, typer.Option("--sisters", help="Include sister (remote) agents")
157
228
  ] = False,
229
+ low: Annotated[
230
+ bool, typer.Option("--low", help="Low detail (identity only)")
231
+ ] = False,
232
+ med: Annotated[
233
+ bool, typer.Option("--med", help="Medium detail")
234
+ ] = False,
235
+ full: Annotated[
236
+ bool, typer.Option("--full", help="Full detail (default)")
237
+ ] = False,
158
238
  session: SessionOption = "agents",
159
239
  ):
160
240
  """List running agents with status.
@@ -170,19 +250,30 @@ def list_agents(
170
250
  get_status_symbol, get_git_diff_stats,
171
251
  )
172
252
  from ..monitor_daemon_state import get_monitor_daemon_state
173
- from ..summary_columns import build_cli_context, SUMMARY_COLUMNS
253
+ from ..summary_columns import build_cli_context, render_summary_cells, align_summary_rows
174
254
  from ..tui_logic import compute_tree_metadata, sort_sessions_by_tree
175
- from rich.text import Text
176
255
  from rich.console import Console
177
256
 
257
+ # Resolve detail level (mutually exclusive flags, default full)
258
+ if low:
259
+ detail = "low"
260
+ elif med:
261
+ detail = "med"
262
+ else:
263
+ detail = "full"
264
+
178
265
  launcher = ClaudeLauncher(session)
179
266
  sessions = launcher.list_sessions()
180
267
 
181
268
  # Merge sister sessions if --sisters flag
269
+ has_sisters = False
270
+ local_hostname = ""
182
271
  if sisters:
183
272
  from ..sister_poller import SisterPoller
184
273
  poller = SisterPoller()
185
- if poller.has_sisters:
274
+ has_sisters = poller.has_sisters
275
+ local_hostname = poller.local_hostname
276
+ if has_sisters:
186
277
  remote_sessions = poller.poll_all()
187
278
  sessions = sessions + remote_sessions
188
279
 
@@ -212,23 +303,48 @@ def list_agents(
212
303
  sessions = sort_sessions_by_tree(sessions)
213
304
  tree_meta = compute_tree_metadata(sessions)
214
305
 
215
- # Columns to render in list mode (subset of TUI columns)
216
- list_columns = {
217
- "status_symbol", "time_in_state", "sleep_countdown", "agent_name",
218
- "git_diff",
219
- "uptime", "running_time", "stalled_time", "sleep_time",
220
- "token_count", "cost", "budget", "context_usage",
221
- }
222
-
223
- # Pre-compute: any agent with budget, max name width
306
+ # Pre-compute: any agent with budget, column alignment widths
224
307
  any_has_budget = any(s.cost_budget_usd > 0 for s in sessions)
225
308
  max_name_len = max(len(s.name) for s in sessions)
226
309
  name_width = min(max(max_name_len, 10), 20)
310
+ max_repo_width = max((len(s.repo_name or "n/a") for s in sessions), default=5)
311
+ max_branch_width = max((len(s.branch or "n/a") for s in sessions), default=5)
312
+ all_names_match_repos = all(
313
+ s.name == s.repo_name for s in sessions if s.repo_name
314
+ )
227
315
 
228
316
  # Prefer daemon state for status/activity (single source of truth)
229
317
  daemon_state = get_monitor_daemon_state(session)
230
318
  use_daemon = daemon_state is not None and not daemon_state.is_stale()
231
319
 
320
+ # Compute cross-session flags from daemon state
321
+ any_has_oversight_timeout = False
322
+ any_has_pr = False
323
+ subtree_costs = {}
324
+ if use_daemon:
325
+ any_has_oversight_timeout = any(
326
+ ds.oversight_timeout_seconds > 0
327
+ for ds in daemon_state.sessions
328
+ )
329
+ for ds in daemon_state.sessions:
330
+ if ds.subtree_cost_usd > 0:
331
+ subtree_costs[ds.session_id] = ds.subtree_cost_usd
332
+ # Also extract from remote sessions via forwarded daemon_state
333
+ for s in sessions:
334
+ rds = getattr(s, 'remote_daemon_state', None)
335
+ if rds and rds.get('subtree_cost_usd', 0) > 0:
336
+ subtree_costs[s.id] = rds['subtree_cost_usd']
337
+ if not use_daemon:
338
+ any_has_oversight_timeout = any(
339
+ getattr(s, 'oversight_timeout_seconds', 0) > 0
340
+ for s in sessions
341
+ )
342
+ any_has_subtree_cost = bool(subtree_costs)
343
+ any_has_pr = any(
344
+ getattr(s, 'pr_number', None) is not None
345
+ for s in sessions
346
+ )
347
+
232
348
  # Only create detector as fallback when daemon isn't running
233
349
  detector = None
234
350
  if not use_daemon:
@@ -288,24 +404,45 @@ def list_agents(
288
404
  # Compute cross-session flags
289
405
  any_is_sleeping = any(st == "busy_sleeping" for _, st, _, _, _ in session_data)
290
406
 
291
- # Second pass: render
407
+ # Second pass: build contexts and collect cells for auto-alignment
408
+ all_cells = []
409
+ activities = []
292
410
  for sess, status, activity, claude_stats, git_diff in session_data:
293
411
  meta = tree_meta.get(sess.id)
294
412
  child_count = meta.child_count if meta else 0
413
+
414
+ # Get per-session daemon fields
415
+ oversight_deadline = None
416
+ if use_daemon:
417
+ ds = daemon_state.get_session_by_name(sess.name)
418
+ if ds:
419
+ oversight_deadline = ds.oversight_deadline
420
+
421
+ _, status_color = get_status_symbol(status)
295
422
  ctx = build_cli_context(
296
423
  session=sess, stats=sess.stats,
297
424
  claude_stats=claude_stats, git_diff_stats=git_diff,
298
425
  status=status, bg_bash_count=0, live_sub_count=0,
299
426
  any_has_budget=any_has_budget, child_count=child_count,
300
427
  any_is_sleeping=any_is_sleeping,
428
+ any_has_oversight_timeout=any_has_oversight_timeout,
429
+ oversight_deadline=oversight_deadline,
430
+ pr_number=getattr(sess, 'pr_number', None),
431
+ any_has_pr=any_has_pr,
432
+ monochrome=False,
433
+ summary_detail=detail,
434
+ has_sisters=has_sisters,
435
+ local_hostname=local_hostname,
436
+ max_name_width=name_width,
437
+ max_repo_width=max_repo_width,
438
+ max_branch_width=max_branch_width,
439
+ all_names_match_repos=all_names_match_repos,
440
+ subtree_cost_usd=subtree_costs.get(sess.id, 0.0),
441
+ any_has_subtree_cost=any_has_subtree_cost,
301
442
  )
302
-
303
- # Enable colors and set detail level for list view
304
- ctx.monochrome = False
305
- _, status_color = get_status_symbol(status)
306
443
  ctx.status_color = f"bold {status_color}"
307
- ctx.summary_detail = "med"
308
444
  ctx.show_cost = cost
445
+ ctx.is_list_mode = True
309
446
 
310
447
  # Handle tree indentation (#244) using compute_tree_metadata
311
448
  depth = meta.depth if meta else 0
@@ -313,23 +450,15 @@ def list_agents(
313
450
  available = name_width - len(indent)
314
451
  ctx.display_name = (indent + sess.name[:available]).ljust(name_width)
315
452
 
316
- # Render line using column system
317
- line = Text()
318
- for col in SUMMARY_COLUMNS:
319
- if col.id not in list_columns:
320
- continue
321
- if ctx.summary_detail not in col.detail_levels:
322
- continue
323
- segments = col.render(ctx)
324
- if segments:
325
- for text, style in segments:
326
- line.append(text, style=style)
327
-
328
- # Append activity (truncate to fit terminal width)
453
+ all_cells.append(render_summary_cells(ctx))
454
+ activities.append(activity)
455
+
456
+ # Auto-align columns across all rows, then append activity
457
+ aligned_lines = align_summary_rows(all_cells)
458
+ for line, activity in zip(aligned_lines, activities):
329
459
  line.append(" │ ", style="dim")
330
460
  line.append(activity)
331
461
  line.truncate(console.width, pad=False)
332
-
333
462
  console.print(line, no_wrap=True)
334
463
 
335
464
  if terminated_count > 0:
@@ -756,6 +885,10 @@ def show(
756
885
  print(f"{'Tools:':<{label_width + 1}} {sess.allowed_tools}")
757
886
  if sess.extra_claude_args:
758
887
  print(f"{'Claude args:':<{label_width + 1}} {' '.join(sess.extra_claude_args)}")
888
+ if sess.claude_agent:
889
+ print(f"{'Agent:':<{label_width + 1}} {sess.claude_agent}")
890
+ if sess.agent_teams:
891
+ print(f"{'Teams:':<{label_width + 1}} enabled")
759
892
 
760
893
  print()
761
894
 
@@ -35,7 +35,7 @@ def monitor_daemon_start(
35
35
  - Status detection (running, waiting, etc.)
36
36
  - Time accumulation (green_time, non_green_time)
37
37
  - Claude Code stats (tokens, interactions)
38
- - User presence state (macOS only)
38
+ - User presence state
39
39
  """
40
40
  from ..monitor_daemon import MonitorDaemon, is_monitor_daemon_running, get_monitor_daemon_pid
41
41
 
@@ -264,6 +264,36 @@ def get_web_allow_control() -> bool:
264
264
  return bool(web.get("allow_control", False))
265
265
 
266
266
 
267
+ def get_new_agent_defaults() -> dict:
268
+ """Get new-agent default settings from config.
269
+
270
+ Config format in ~/.overcode/config.yaml:
271
+ new_agent_defaults:
272
+ bypass_permissions: false
273
+ agent_teams: false
274
+
275
+ Returns:
276
+ Dict with bypass_permissions (bool) and agent_teams (bool).
277
+ """
278
+ config = load_config()
279
+ defaults = config.get("new_agent_defaults", {})
280
+ return {
281
+ "bypass_permissions": bool(defaults.get("bypass_permissions", False)),
282
+ "agent_teams": bool(defaults.get("agent_teams", False)),
283
+ }
284
+
285
+
286
+ def save_new_agent_defaults(defaults: dict) -> None:
287
+ """Save new-agent default settings to config.
288
+
289
+ Args:
290
+ defaults: Dict with bypass_permissions and agent_teams booleans.
291
+ """
292
+ config = load_config()
293
+ config["new_agent_defaults"] = defaults
294
+ save_config(config)
295
+
296
+
267
297
  def get_sisters_config() -> List[dict]:
268
298
  """Get sister instance configuration for cross-machine monitoring.
269
299
 
@@ -298,3 +328,15 @@ def get_sisters_config() -> List[dict]:
298
328
  result.append(entry)
299
329
 
300
330
  return result
331
+
332
+
333
+ def get_sister_by_name(name: str) -> Optional[dict]:
334
+ """Look up a sister config by name.
335
+
336
+ Returns:
337
+ Sister config dict (name, url, optional api_key) or None if not found.
338
+ """
339
+ for sister in get_sisters_config():
340
+ if sister["name"] == name:
341
+ return sister
342
+ return None
@@ -404,31 +404,35 @@ def get_session_file_path(
404
404
  return projects_path / encoded / f"{session_id}.jsonl"
405
405
 
406
406
 
407
- def read_token_usage_from_session_file(
407
+ def read_session_file_stats(
408
408
  session_file: Path,
409
- since: Optional[datetime] = None
410
- ) -> dict:
411
- """Read token usage from a Claude Code session JSONL file.
409
+ since: Optional[datetime] = None,
410
+ ) -> Tuple[dict, List[float]]:
411
+ """Read token usage and work times from a session file in a single pass.
412
+
413
+ Combines the work of read_token_usage_from_session_file and
414
+ read_work_times_from_session_file so the file is only read once.
412
415
 
413
416
  Args:
414
417
  session_file: Path to the session JSONL file
415
- since: Only count tokens from messages after this time
418
+ since: Only count data from messages after this time
416
419
 
417
420
  Returns:
418
- Dict with input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
419
- and current_context_tokens (most recent input_tokens value)
421
+ (token_usage_dict, work_times_list)
420
422
  """
421
423
  totals = {
422
424
  "input_tokens": 0,
423
425
  "output_tokens": 0,
424
426
  "cache_creation_tokens": 0,
425
427
  "cache_read_tokens": 0,
426
- "current_context_tokens": 0, # Most recent input_tokens
427
- "model": None, # Most recently seen model name (#272)
428
+ "current_context_tokens": 0,
429
+ "model": None,
428
430
  }
429
431
 
430
432
  if not session_file.exists():
431
- return totals
433
+ return totals, []
434
+
435
+ user_prompt_times: List[datetime] = []
432
436
 
433
437
  try:
434
438
  with open(session_file, 'r') as f:
@@ -438,14 +442,14 @@ def read_token_usage_from_session_file(
438
442
  continue
439
443
  try:
440
444
  data = json.loads(line)
441
- # Only assistant messages have usage data
442
- if data.get("type") == "assistant":
445
+ msg_type = data.get("type")
446
+
447
+ if msg_type == "assistant":
443
448
  # Check timestamp if filtering by time
444
449
  if since:
445
450
  ts_str = data.get("timestamp")
446
451
  if ts_str:
447
452
  try:
448
- # Parse ISO timestamp (e.g., "2026-01-02T06:56:01.975Z")
449
453
  msg_time = datetime.fromisoformat(
450
454
  ts_str.replace("Z", "+00:00")
451
455
  ).replace(tzinfo=None)
@@ -468,15 +472,62 @@ def read_token_usage_from_session_file(
468
472
  "cache_creation_input_tokens", 0
469
473
  )
470
474
  totals["cache_read_tokens"] += cache_read
471
- # Track most recent context size (input + cached context)
472
475
  context_size = input_tokens + cache_read
473
476
  if context_size > 0:
474
477
  totals["current_context_tokens"] = context_size
478
+
479
+ elif msg_type == "user":
480
+ # Check if this is an actual user prompt (not a tool result)
481
+ message = data.get("message", {})
482
+ content = message.get("content", "")
483
+ if isinstance(content, list):
484
+ if content and content[0].get("type") == "tool_result":
485
+ continue
486
+
487
+ ts_str = data.get("timestamp")
488
+ if not ts_str:
489
+ continue
490
+
491
+ try:
492
+ msg_time = datetime.fromisoformat(
493
+ ts_str.replace("Z", "+00:00")
494
+ ).replace(tzinfo=None)
495
+ if since and msg_time < since:
496
+ continue
497
+ user_prompt_times.append(msg_time)
498
+ except (ValueError, TypeError):
499
+ continue
500
+
475
501
  except (json.JSONDecodeError, KeyError, TypeError):
476
502
  continue
477
503
  except IOError:
478
- pass
504
+ return totals, []
505
+
506
+ # Calculate durations between consecutive prompts
507
+ work_times = []
508
+ for i in range(1, len(user_prompt_times)):
509
+ duration = (user_prompt_times[i] - user_prompt_times[i - 1]).total_seconds()
510
+ if duration > 0:
511
+ work_times.append(duration)
512
+
513
+ return totals, work_times
514
+
515
+
516
+ def read_token_usage_from_session_file(
517
+ session_file: Path,
518
+ since: Optional[datetime] = None
519
+ ) -> dict:
520
+ """Read token usage from a Claude Code session JSONL file.
521
+
522
+ Args:
523
+ session_file: Path to the session JSONL file
524
+ since: Only count tokens from messages after this time
479
525
 
526
+ Returns:
527
+ Dict with input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
528
+ and current_context_tokens (most recent input_tokens value)
529
+ """
530
+ totals, _ = read_session_file_stats(session_file, since)
480
531
  return totals
481
532
 
482
533
 
@@ -498,62 +549,7 @@ def read_work_times_from_session_file(
498
549
  Returns:
499
550
  List of work times in seconds
500
551
  """
501
- if not session_file.exists():
502
- return []
503
-
504
- user_prompt_times: List[datetime] = []
505
-
506
- try:
507
- with open(session_file, 'r') as f:
508
- for line in f:
509
- line = line.strip()
510
- if not line:
511
- continue
512
- try:
513
- data = json.loads(line)
514
- if data.get("type") != "user":
515
- continue
516
-
517
- # Check if this is an actual user prompt (not a tool result)
518
- message = data.get("message", {})
519
- content = message.get("content", "")
520
-
521
- # Tool results have content as a list with tool_result type
522
- if isinstance(content, list):
523
- # Check if it's a tool result
524
- if content and content[0].get("type") == "tool_result":
525
- continue
526
-
527
- # Parse timestamp
528
- ts_str = data.get("timestamp")
529
- if not ts_str:
530
- continue
531
-
532
- try:
533
- msg_time = datetime.fromisoformat(
534
- ts_str.replace("Z", "+00:00")
535
- ).replace(tzinfo=None)
536
-
537
- # Filter by since time
538
- if since and msg_time < since:
539
- continue
540
-
541
- user_prompt_times.append(msg_time)
542
- except (ValueError, TypeError):
543
- continue
544
-
545
- except (json.JSONDecodeError, KeyError, TypeError):
546
- continue
547
- except IOError:
548
- return []
549
-
550
- # Calculate durations between consecutive prompts
551
- work_times = []
552
- for i in range(1, len(user_prompt_times)):
553
- duration = (user_prompt_times[i] - user_prompt_times[i - 1]).total_seconds()
554
- if duration > 0:
555
- work_times.append(duration)
556
-
552
+ _, work_times = read_session_file_stats(session_file, since)
557
553
  return work_times
558
554
 
559
555
 
@@ -625,7 +621,7 @@ def get_session_stats(
625
621
  session_file = get_session_file_path(
626
622
  session.start_directory, sid, projects_path
627
623
  )
628
- usage = read_token_usage_from_session_file(session_file, since=session_start)
624
+ usage, work_times = read_session_file_stats(session_file, since=session_start)
629
625
  total_input += usage["input_tokens"]
630
626
  total_output += usage["output_tokens"]
631
627
  total_cache_creation += usage["cache_creation_tokens"]
@@ -644,7 +640,6 @@ def get_session_stats(
644
640
  detected_model = usage["model"]
645
641
 
646
642
  # Collect work times from this session file
647
- work_times = read_work_times_from_session_file(session_file, since=session_start)
648
643
  all_work_times.extend(work_times)
649
644
 
650
645
  # Check for subagent files in {sessionId}/subagents/
@@ -655,7 +650,7 @@ def get_session_stats(
655
650
  subagent_count += 1
656
651
  if now - subagent_file.stat().st_mtime < 30:
657
652
  live_subagent_count += 1
658
- sub_usage = read_token_usage_from_session_file(
653
+ sub_usage, _ = read_session_file_stats(
659
654
  subagent_file, since=session_start
660
655
  )
661
656
  total_input += sub_usage["input_tokens"]