overcode 0.2.2__tar.gz → 0.2.4__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 (112) hide show
  1. {overcode-0.2.2/src/overcode.egg-info → overcode-0.2.4}/PKG-INFO +1 -1
  2. {overcode-0.2.2 → overcode-0.2.4}/pyproject.toml +1 -1
  3. overcode-0.2.4/src/overcode/agent_scanner.py +38 -0
  4. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/bundled_skills.py +7 -2
  5. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/agent.py +29 -1
  6. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/daemon.py +1 -1
  7. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/implementations.py +11 -0
  8. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/launcher.py +20 -0
  9. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/monitor_daemon.py +56 -19
  10. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/monitor_daemon_state.py +3 -2
  11. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/presence_logger.py +4 -4
  12. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/session_manager.py +6 -1
  13. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/settings.py +22 -15
  14. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/sister_poller.py +3 -0
  15. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/status_constants.py +82 -4
  16. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/summary_columns.py +213 -85
  17. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/summary_groups.py +6 -38
  18. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tmux_manager.py +11 -0
  19. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui.py +348 -60
  20. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui.tcss +43 -1
  21. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_actions/session.py +2 -1
  22. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_actions/view.py +37 -3
  23. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_helpers.py +3 -2
  24. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/__init__.py +4 -0
  25. overcode-0.2.4/src/overcode/tui_widgets/agent_select_modal.py +126 -0
  26. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/command_bar.py +29 -2
  27. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/help_overlay.py +4 -3
  28. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/session_summary.py +21 -25
  29. overcode-0.2.4/src/overcode/tui_widgets/sister_selection_modal.py +154 -0
  30. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/status_timeline.py +1 -5
  31. overcode-0.2.4/src/overcode/tui_widgets/summary_config_modal.py +292 -0
  32. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/web_api.py +6 -0
  33. {overcode-0.2.2 → overcode-0.2.4/src/overcode.egg-info}/PKG-INFO +1 -1
  34. {overcode-0.2.2 → overcode-0.2.4}/src/overcode.egg-info/SOURCES.txt +3 -0
  35. overcode-0.2.2/src/overcode/tui_widgets/summary_config_modal.py +0 -164
  36. {overcode-0.2.2 → overcode-0.2.4}/LICENSE +0 -0
  37. {overcode-0.2.2 → overcode-0.2.4}/MANIFEST.in +0 -0
  38. {overcode-0.2.2 → overcode-0.2.4}/README.md +0 -0
  39. {overcode-0.2.2 → overcode-0.2.4}/setup.cfg +0 -0
  40. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/__init__.py +0 -0
  41. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/claude_config.py +0 -0
  42. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/__init__.py +0 -0
  43. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/__main__.py +0 -0
  44. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/_shared.py +0 -0
  45. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/budget.py +0 -0
  46. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/config.py +0 -0
  47. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/hooks.py +0 -0
  48. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/monitoring.py +0 -0
  49. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/perms.py +0 -0
  50. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/sister.py +0 -0
  51. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/cli/skills.py +0 -0
  52. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/config.py +0 -0
  53. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/daemon_claude_skill.md +0 -0
  54. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/daemon_logging.py +0 -0
  55. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/daemon_utils.py +0 -0
  56. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/data_export.py +0 -0
  57. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/dependency_check.py +0 -0
  58. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/exceptions.py +0 -0
  59. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/follow_mode.py +0 -0
  60. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/history_reader.py +0 -0
  61. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/hook_handler.py +0 -0
  62. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/hook_status_detector.py +0 -0
  63. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/interfaces.py +0 -0
  64. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/logging_config.py +0 -0
  65. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/mocks.py +0 -0
  66. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/monitor_daemon_core.py +0 -0
  67. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/notifier.py +0 -0
  68. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/pid_utils.py +0 -0
  69. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/protocols.py +0 -0
  70. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/sister_controller.py +0 -0
  71. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/standing_instructions.py +0 -0
  72. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/status_detector.py +0 -0
  73. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/status_detector_factory.py +0 -0
  74. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/status_history.py +0 -0
  75. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/status_patterns.py +0 -0
  76. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/summarizer_client.py +0 -0
  77. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/summarizer_component.py +0 -0
  78. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/supervisor_daemon.py +0 -0
  79. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/supervisor_daemon_core.py +0 -0
  80. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/supervisor_layout.sh +0 -0
  81. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/testing/__init__.py +0 -0
  82. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/testing/renderer.py +0 -0
  83. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/testing/tmux_driver.py +0 -0
  84. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/testing/tui_eye.py +0 -0
  85. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/testing/tui_eye_skill.md +0 -0
  86. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/time_context.py +0 -0
  87. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tmux_utils.py +0 -0
  88. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_actions/__init__.py +0 -0
  89. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_actions/daemon.py +0 -0
  90. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_actions/input.py +0 -0
  91. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_actions/navigation.py +0 -0
  92. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_logic.py +0 -0
  93. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_render.py +0 -0
  94. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  95. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
  96. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  97. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  98. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/tui_widgets/preview_pane.py +0 -0
  99. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/usage_monitor.py +0 -0
  100. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/web/__init__.py +0 -0
  101. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/web/templates/analytics.html +0 -0
  102. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/web/templates/dashboard.html +0 -0
  103. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/web_chartjs.py +0 -0
  104. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/web_control_api.py +0 -0
  105. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/web_server.py +0 -0
  106. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/web_server_runner.py +0 -0
  107. {overcode-0.2.2 → overcode-0.2.4}/src/overcode/web_templates.py +0 -0
  108. {overcode-0.2.2 → overcode-0.2.4}/src/overcode.egg-info/dependency_links.txt +0 -0
  109. {overcode-0.2.2 → overcode-0.2.4}/src/overcode.egg-info/entry_points.txt +0 -0
  110. {overcode-0.2.2 → overcode-0.2.4}/src/overcode.egg-info/requires.txt +0 -0
  111. {overcode-0.2.2 → overcode-0.2.4}/src/overcode.egg-info/top_level.txt +0 -0
  112. {overcode-0.2.2 → overcode-0.2.4}/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.2
3
+ Version: 0.2.4
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.2"
7
+ version = "0.2.4"
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,6 +59,14 @@ 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,
62
70
  teams: Annotated[
63
71
  bool,
64
72
  typer.Option("--teams", help="Enable Claude Code agent teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)"),
@@ -159,6 +167,8 @@ def launch(
159
167
  allowed_tools=allowed_tools,
160
168
  extra_claude_args=claude_args,
161
169
  agent_teams=teams,
170
+ budget_usd=budget,
171
+ claude_agent=agent,
162
172
  )
163
173
 
164
174
  if result:
@@ -171,8 +181,12 @@ def launch(
171
181
  rprint(f" Allowed tools: {allowed_tools}")
172
182
  if claude_args:
173
183
  rprint(f" Extra Claude args: {' '.join(claude_args)}")
184
+ if agent:
185
+ rprint(f" Agent: {agent}")
174
186
  if teams:
175
187
  rprint(" Agent teams: enabled")
188
+ if budget is not None and budget > 0:
189
+ rprint(f" Budget: ${budget:.2f}")
176
190
 
177
191
  # Store oversight policy on session
178
192
  if oversight_policy != "wait" or oversight_timeout_seconds > 0:
@@ -306,16 +320,26 @@ def list_agents(
306
320
  # Compute cross-session flags from daemon state
307
321
  any_has_oversight_timeout = False
308
322
  any_has_pr = False
323
+ subtree_costs = {}
309
324
  if use_daemon:
310
325
  any_has_oversight_timeout = any(
311
326
  ds.oversight_timeout_seconds > 0
312
327
  for ds in daemon_state.sessions
313
328
  )
314
- else:
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:
315
338
  any_has_oversight_timeout = any(
316
339
  getattr(s, 'oversight_timeout_seconds', 0) > 0
317
340
  for s in sessions
318
341
  )
342
+ any_has_subtree_cost = bool(subtree_costs)
319
343
  any_has_pr = any(
320
344
  getattr(s, 'pr_number', None) is not None
321
345
  for s in sessions
@@ -413,6 +437,8 @@ def list_agents(
413
437
  max_repo_width=max_repo_width,
414
438
  max_branch_width=max_branch_width,
415
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,
416
442
  )
417
443
  ctx.status_color = f"bold {status_color}"
418
444
  ctx.show_cost = cost
@@ -859,6 +885,8 @@ def show(
859
885
  print(f"{'Tools:':<{label_width + 1}} {sess.allowed_tools}")
860
886
  if sess.extra_claude_args:
861
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}")
862
890
  if sess.agent_teams:
863
891
  print(f"{'Teams:':<{label_width + 1}} enabled")
864
892
 
@@ -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
 
@@ -151,6 +151,17 @@ class RealTmux:
151
151
  if rest:
152
152
  pane.send_keys(rest, enter=False)
153
153
  time.sleep(0.1)
154
+ elif keys.startswith('/') and len(keys) > 1:
155
+ # Special handling for slash commands (#307)
156
+ # Claude Code shows a command menu when / is typed;
157
+ # send / separately so the menu has time to appear
158
+ # before the rest of the command and Enter arrive.
159
+ pane.send_keys('/', enter=False)
160
+ time.sleep(0.3)
161
+ rest = keys[1:]
162
+ if rest:
163
+ pane.send_keys(rest, enter=False)
164
+ time.sleep(0.15)
154
165
  else:
155
166
  pane.send_keys(keys, enter=False)
156
167
  # Small delay for Claude Code to process text
@@ -79,6 +79,8 @@ class ClaudeLauncher:
79
79
  allowed_tools: Optional[str] = None,
80
80
  extra_claude_args: Optional[List[str]] = None,
81
81
  agent_teams: bool = False,
82
+ budget_usd: Optional[float] = None,
83
+ claude_agent: Optional[str] = None,
82
84
  ) -> Optional[Session]:
83
85
  """
84
86
  Launch an interactive Claude Code session in a tmux window.
@@ -163,6 +165,10 @@ class ClaudeLauncher:
163
165
  elif skip_permissions:
164
166
  claude_cmd.extend(["--permission-mode", "dontAsk"])
165
167
 
168
+ # Claude agent persona
169
+ if claude_agent:
170
+ claude_cmd.extend(["--agent", claude_agent])
171
+
166
172
  # Claude CLI flag passthrough (#290)
167
173
  if allowed_tools:
168
174
  claude_cmd.extend(["--allowedTools", allowed_tools])
@@ -217,6 +223,7 @@ class ClaudeLauncher:
217
223
  allowed_tools=allowed_tools,
218
224
  extra_claude_args=extra_claude_args,
219
225
  agent_teams=agent_teams,
226
+ claude_agent=claude_agent,
220
227
  )
221
228
 
222
229
  # Set parent if launching as child agent (#244)
@@ -224,6 +231,19 @@ class ClaudeLauncher:
224
231
  self.sessions.update_session(session.id, parent_session_id=parent_session.id)
225
232
  session.parent_session_id = parent_session.id
226
233
 
234
+ # Apply budget at launch time
235
+ if budget_usd is not None and budget_usd > 0:
236
+ if parent_session:
237
+ # transfer_budget handles unlimited parent (budget=0) correctly
238
+ success = self.sessions.transfer_budget(parent_session.id, session.id, budget_usd)
239
+ if not success:
240
+ print(f"Cannot launch: parent has insufficient budget for ${budget_usd:.2f}")
241
+ self.tmux.kill_window(window_index)
242
+ self.sessions.delete_session(session.id)
243
+ return None
244
+ else:
245
+ self.sessions.set_cost_budget(session.id, budget_usd)
246
+
227
247
  print(f"✓ Launched '{name}' in tmux window {window_index}")
228
248
 
229
249
  # Send initial prompt if provided (after Claude starts)
@@ -6,7 +6,7 @@ This daemon handles all monitoring responsibilities:
6
6
  - Agent status detection (via StatusDetector)
7
7
  - Time tracking (green_time_seconds, non_green_time_seconds)
8
8
  - Claude Code stats sync (tokens, interactions)
9
- - Presence tracking (macOS only, graceful degradation)
9
+ - Presence tracking (graceful degradation on non-macOS)
10
10
  - Status history logging (CSV)
11
11
 
12
12
  The Monitor Daemon publishes MonitorDaemonState to a JSON file that
@@ -149,18 +149,18 @@ def _create_monitor_logger(session: str = "agents", log_file: Optional[Path] = N
149
149
 
150
150
 
151
151
  class PresenceComponent:
152
- """Presence tracking with graceful degradation for non-macOS."""
152
+ """Presence tracking (works on all platforms; richer on macOS with Quartz)."""
153
153
 
154
154
  # TUI heartbeat is considered fresh if within this many seconds
155
155
  TUI_HEARTBEAT_FRESHNESS = 60
156
156
 
157
157
  def __init__(self, tmux_session: str = "agents"):
158
- self.available = MACOS_APIS_AVAILABLE
158
+ self.available = True
159
159
  self._logger: Optional[PresenceLogger] = None
160
160
  self._tmux_session = tmux_session
161
161
  self._last_publish_time: Optional[datetime] = None
162
162
 
163
- if self.available and PresenceLogger is not None:
163
+ if PresenceLogger is not None:
164
164
  heartbeat_path = str(get_tui_heartbeat_path(tmux_session))
165
165
  config = PresenceLoggerConfig(
166
166
  tui_heartbeat_path=heartbeat_path,
@@ -200,30 +200,39 @@ class PresenceComponent:
200
200
  """Get current presence state.
201
201
 
202
202
  Returns:
203
- Tuple of (state, idle_seconds, locked) or (None, None, None) if unavailable
203
+ Tuple of (state, idle_seconds, locked) or (None, None, None) if unavailable.
204
+ On non-macOS, idle is always 0 and locked is always False, but sleep
205
+ detection and TUI heartbeat still produce meaningful state values.
204
206
  """
205
- if not self.available or get_current_presence_state is None:
206
- return None, None, None
207
-
208
207
  try:
208
+ from .presence_logger import classify_state, DEFAULT_IDLE_THRESHOLD
209
+
209
210
  tui_active = self._is_tui_active()
210
211
  slept = self._detect_sleep()
211
212
 
212
213
  if slept:
213
214
  # Machine just woke — override to asleep state for this sample
214
- idle = 0.0
215
- locked = False
216
- from .presence_logger import classify_state, DEFAULT_IDLE_THRESHOLD
217
215
  state = classify_state(
218
- locked=locked,
219
- idle_seconds=idle,
216
+ locked=False,
217
+ idle_seconds=0.0,
220
218
  slept=True,
221
219
  idle_threshold=DEFAULT_IDLE_THRESHOLD,
222
220
  tui_active=False,
223
221
  )
224
- return state, idle, locked
225
-
226
- return get_current_presence_state(tui_active=tui_active)
222
+ return state, 0.0, False
223
+
224
+ if MACOS_APIS_AVAILABLE and get_current_presence_state is not None:
225
+ return get_current_presence_state(tui_active=tui_active)
226
+
227
+ # Non-macOS: idle=0, locked=False; TUI heartbeat still works
228
+ state = classify_state(
229
+ locked=False,
230
+ idle_seconds=0.0,
231
+ slept=False,
232
+ idle_threshold=DEFAULT_IDLE_THRESHOLD,
233
+ tui_active=tui_active,
234
+ )
235
+ return state, 0.0, False
227
236
  except Exception:
228
237
  return None, None, None
229
238
 
@@ -856,7 +865,7 @@ class MonitorDaemon:
856
865
  if pane_content:
857
866
  pr = extract_pr_number(pane_content)
858
867
  if pr is not None and pr != session.pr_number:
859
- self.session_manager.update_session(session.id, pr_number=pr)
868
+ self.session_manager.update_session(session.id, pr_number=pr, pr_branch=session.branch)
860
869
 
861
870
  # Clear heartbeat tracking when session stops running
862
871
  if status != STATUS_RUNNING and session.id in self._sessions_running_from_heartbeat:
@@ -864,7 +873,14 @@ class MonitorDaemon:
864
873
  self._heartbeat_start_pending.discard(session.id)
865
874
 
866
875
  # Refresh git context (branch may have changed)
867
- self.session_manager.refresh_git_context(session.id)
876
+ git_changed = self.session_manager.refresh_git_context(session.id)
877
+ if git_changed and session.pr_number is not None:
878
+ # Re-read session to get updated branch
879
+ refreshed = self.session_manager.get_session(session.id)
880
+ if refreshed and refreshed.branch is not None:
881
+ # Clear if pr_branch not set (pre-migration) or branch mismatch
882
+ if refreshed.pr_branch is None or refreshed.branch != refreshed.pr_branch:
883
+ self.session_manager.update_session(session.id, pr_number=None, pr_branch=None)
868
884
 
869
885
  # Update current task in session
870
886
  self.session_manager.update_stats(
@@ -911,8 +927,29 @@ class MonitorDaemon:
911
927
  if status != "waiting_user":
912
928
  all_waiting_user = False
913
929
 
930
+ # Compute subtree costs for parent agents
931
+ self._compute_subtree_costs(session_states)
932
+
914
933
  return session_states, all_waiting_user
915
934
 
935
+ def _compute_subtree_costs(self, session_states):
936
+ """Compute subtree cost (self + all descendants) for each parent agent."""
937
+ by_name = {s.name: s for s in session_states}
938
+ children_map = {}
939
+ for s in session_states:
940
+ if s.parent_name and s.parent_name in by_name:
941
+ children_map.setdefault(s.parent_name, []).append(s.name)
942
+
943
+ def _sum(name):
944
+ total = by_name[name].estimated_cost_usd
945
+ for child in children_map.get(name, []):
946
+ total += _sum(child)
947
+ return total
948
+
949
+ for s in session_states:
950
+ if children_map.get(s.name):
951
+ s.subtree_cost_usd = _sum(s.name)
952
+
916
953
  def _cleanup_stale(self, sessions: list) -> None:
917
954
  """Remove stale tracking entries for deleted sessions."""
918
955
  current_session_ids = {s.id for s in sessions}
@@ -971,7 +1008,7 @@ class MonitorDaemon:
971
1008
  self.log.section("Monitor Daemon")
972
1009
  self.log.info(f"PID: {os.getpid()}")
973
1010
  self.log.info(f"tmux session: {self.tmux_session}")
974
- self.log.info(f"Presence tracking: {'available' if self.presence.available else 'unavailable (non-macOS)'}")
1011
+ self.log.info(f"Presence tracking: available (macOS Quartz: {'yes' if MACOS_APIS_AVAILABLE else 'no'})")
975
1012
 
976
1013
  # Setup signal handlers
977
1014
  def handle_shutdown(signum, frame):
@@ -8,7 +8,7 @@ The Monitor Daemon is the single source of truth for:
8
8
  - Agent status detection
9
9
  - Time tracking (green_time_seconds, non_green_time_seconds)
10
10
  - Claude Code stats (tokens, interactions)
11
- - User presence state (macOS only)
11
+ - User presence state
12
12
  """
13
13
 
14
14
  import json
@@ -97,6 +97,7 @@ class SessionDaemonState:
97
97
  # Cost budget (#173)
98
98
  cost_budget_usd: float = 0.0 # 0 = unlimited
99
99
  budget_exceeded: bool = False # True when cost >= budget
100
+ subtree_cost_usd: float = 0.0 # self + all descendants (0 = leaf or not computed)
100
101
 
101
102
  # Agent hierarchy (#244)
102
103
  parent_name: Optional[str] = None # Name of parent agent (None = root)
@@ -140,7 +141,7 @@ class MonitorDaemonState:
140
141
  # Session states (one per agent)
141
142
  sessions: List[SessionDaemonState] = field(default_factory=list)
142
143
 
143
- # Presence state (optional, macOS only)
144
+ # Presence state
144
145
  presence_available: bool = False
145
146
  presence_state: Optional[int] = None # 0=asleep, 1=locked, 2=idle, 3=active, 4=tui_active
146
147
  presence_idle_seconds: Optional[float] = None
@@ -1,7 +1,7 @@
1
1
  """
2
2
  presence_logger.py
3
3
 
4
- Mac-only presence logger that records user presence/absence stats.
4
+ Presence logger that records user presence/absence stats.
5
5
 
6
6
  Records once per SAMPLE_INTERVAL:
7
7
  - timestamp (ISO8601 local time)
@@ -441,9 +441,9 @@ def main() -> int:
441
441
  overcode presence
442
442
  """
443
443
  if not MACOS_APIS_AVAILABLE:
444
- print("Error: macOS APIs not available.")
445
- print("Install dependencies: pip install pyobjc-framework-Quartz pyobjc-framework-ApplicationServices")
446
- return 1
444
+ print("Note: macOS Quartz APIs not available; idle and screen-lock detection disabled.")
445
+ print("Sleep detection and TUI heartbeat still work.")
446
+ print()
447
447
 
448
448
  # Check if already running
449
449
  if is_presence_running():
@@ -79,6 +79,7 @@ class Session:
79
79
  repo_name: Optional[str] = None
80
80
  branch: Optional[str] = None
81
81
  pr_number: Optional[int] = None
82
+ pr_branch: Optional[str] = None
82
83
 
83
84
  # Management
84
85
  status: str = "running"
@@ -129,6 +130,7 @@ class Session:
129
130
  allowed_tools: Optional[str] = None # Comma-separated tool list for --allowedTools
130
131
  extra_claude_args: List[str] = field(default_factory=list) # Extra CLI flags via --claude-arg
131
132
  agent_teams: bool = False # Claude Code agent teams mode (#309)
133
+ claude_agent: Optional[str] = None # Claude agent persona (from .claude/agents/)
132
134
 
133
135
  # Agent hierarchy (#244) - parent/child relationships
134
136
  parent_session_id: Optional[str] = None # ID of parent agent (None = root)
@@ -150,6 +152,7 @@ class Session:
150
152
  remote_median_work_time: float = 0.0 # Median work time from remote API
151
153
  remote_activity_summary: str = "" # AI summary from remote summarizer
152
154
  remote_activity_summary_context: str = "" # AI context summary from remote summarizer
155
+ remote_daemon_state: Optional[dict] = None # Raw daemon state dict from sister API (for generic forwarding)
153
156
 
154
157
  def to_dict(self) -> dict:
155
158
  data = asdict(self)
@@ -452,7 +455,8 @@ class SessionManager:
452
455
  permissiveness_mode: str = "normal",
453
456
  allowed_tools: Optional[str] = None,
454
457
  extra_claude_args: Optional[List[str]] = None,
455
- agent_teams: bool = False) -> Session:
458
+ agent_teams: bool = False,
459
+ claude_agent: Optional[str] = None) -> Session:
456
460
  """Create and register a new session.
457
461
 
458
462
  Args:
@@ -486,6 +490,7 @@ class SessionManager:
486
490
  allowed_tools=allowed_tools,
487
491
  extra_claude_args=extra_claude_args or [],
488
492
  agent_teams=agent_teams,
493
+ claude_agent=claude_agent,
489
494
  )
490
495
 
491
496
  state = self._load_state()
@@ -425,17 +425,19 @@ class TUIPreferences:
425
425
  summary_content_mode: str = "ai_short" # ai_short, ai_long, orders, annotation, heartbeat (#98, #171)
426
426
  baseline_minutes: int = 60 # 0=now (instantaneous), 15/30/.../180 = minutes back for mean spin
427
427
  monochrome: bool = False # B&W mode for terminals with ANSI issues (#138)
428
+ emoji_free: bool = False # ASCII fallbacks for terminals without emoji (#315)
428
429
  show_cost: bool = False # Show $ cost instead of token counts
429
430
  timeline_hours: float = 3.0 # 1, 3, 6, 12, 24 — timeline scope (#191)
430
431
  notifications: str = "off" # "off", "sound", "banner", "both" — macOS notifications (#235)
431
432
  # Session IDs of stalled agents that have been visited by the user
432
433
  visited_stalled_agents: Set[str] = field(default_factory=set)
433
- # Column group visibility (group_id -> enabled) for summary line (#178)
434
- summary_groups: dict = field(default_factory=lambda: {
435
- "time": True, "llm_usage": True, "context": True, "git": True,
436
- "supervision": True, "priority": True, "performance": True,
437
- "subprocesses": True,
438
- })
434
+ # Per-level column overrides: {"low": {"uptime": true, ...}, "med": {...}, "high": {...}}
435
+ # Only stores explicit user overrides. Missing = use default from detail_levels.
436
+ column_config: dict = field(default_factory=dict)
437
+ # Show abbreviated column headers above summary lines
438
+ show_column_headers: bool = False
439
+ # Sister instances hidden from agent list (#323)
440
+ disabled_sisters: Set[str] = field(default_factory=set)
439
441
 
440
442
  @classmethod
441
443
  def load(cls, session: str) -> "TUIPreferences":
@@ -452,14 +454,13 @@ class TUIPreferences:
452
454
  if not isinstance(data, dict):
453
455
  return cls()
454
456
 
455
- # Default summary groups visibility
456
- default_summary_groups = {
457
- "time": True, "llm_usage": True, "context": True, "git": True,
458
- "supervision": True, "priority": True, "performance": True,
459
- "subprocesses": True,
460
- }
457
+ # Migration: map "custom" detail level to "full"
458
+ summary_detail = data.get("summary_detail", "low")
459
+ if summary_detail == "custom":
460
+ summary_detail = "full"
461
+
461
462
  return cls(
462
- summary_detail=data.get("summary_detail", "low"),
463
+ summary_detail=summary_detail,
463
464
  detail_lines=data.get("detail_lines", 5),
464
465
  timeline_visible=data.get("timeline_visible", True),
465
466
  daemon_panel_visible=data.get("daemon_panel_visible", False),
@@ -472,11 +473,14 @@ class TUIPreferences:
472
473
  summary_content_mode=data.get("summary_content_mode", "ai_short"),
473
474
  baseline_minutes=data.get("baseline_minutes", 0),
474
475
  monochrome=data.get("monochrome", False),
476
+ emoji_free=data.get("emoji_free", False),
475
477
  show_cost=data.get("show_cost", False),
476
478
  visited_stalled_agents=set(data.get("visited_stalled_agents", [])),
477
- summary_groups=data.get("summary_groups", default_summary_groups),
479
+ column_config=data.get("column_config", {}),
480
+ show_column_headers=data.get("show_column_headers", False),
478
481
  timeline_hours=data.get("timeline_hours", 3.0),
479
482
  notifications=data.get("notifications", "off"),
483
+ disabled_sisters=set(data.get("disabled_sisters", [])),
480
484
  )
481
485
  except (json.JSONDecodeError, IOError):
482
486
  return cls()
@@ -503,11 +507,14 @@ class TUIPreferences:
503
507
  "summary_content_mode": self.summary_content_mode,
504
508
  "baseline_minutes": self.baseline_minutes,
505
509
  "monochrome": self.monochrome,
510
+ "emoji_free": self.emoji_free,
506
511
  "show_cost": self.show_cost,
507
512
  "visited_stalled_agents": list(self.visited_stalled_agents),
508
- "summary_groups": self.summary_groups,
513
+ "column_config": self.column_config,
514
+ "show_column_headers": self.show_column_headers,
509
515
  "timeline_hours": self.timeline_hours,
510
516
  "notifications": self.notifications,
517
+ "disabled_sisters": sorted(self.disabled_sisters),
511
518
  }, f, indent=2)
512
519
  except (IOError, OSError):
513
520
  pass # Best effort
@@ -261,6 +261,9 @@ def _agent_to_session(agent: dict, host_name: str, source_url: str = "", source_
261
261
  # AI summaries from remote summarizer
262
262
  remote_activity_summary=agent.get("activity_summary", ""),
263
263
  remote_activity_summary_context=agent.get("activity_summary_context", ""),
264
+ # Raw daemon state for generic field forwarding — new SessionDaemonState
265
+ # fields automatically flow through without manual plumbing here.
266
+ remote_daemon_state=agent.get("daemon_state"),
264
267
  )
265
268
 
266
269