overcode 0.2.2__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.
- {overcode-0.2.2/src/overcode.egg-info → overcode-0.2.3}/PKG-INFO +1 -1
- {overcode-0.2.2 → overcode-0.2.3}/pyproject.toml +1 -1
- overcode-0.2.3/src/overcode/agent_scanner.py +38 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/bundled_skills.py +7 -2
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/agent.py +29 -1
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/daemon.py +1 -1
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/launcher.py +20 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/monitor_daemon.py +56 -19
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/monitor_daemon_state.py +3 -2
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/presence_logger.py +4 -4
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/session_manager.py +6 -1
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/settings.py +15 -15
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/sister_poller.py +3 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/summary_columns.py +161 -50
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/summary_groups.py +6 -38
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui.py +159 -45
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui.tcss +27 -1
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_actions/view.py +16 -3
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/__init__.py +2 -0
- overcode-0.2.3/src/overcode/tui_widgets/agent_select_modal.py +126 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/command_bar.py +28 -2
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/session_summary.py +12 -20
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/status_timeline.py +1 -5
- overcode-0.2.3/src/overcode/tui_widgets/summary_config_modal.py +292 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/web_api.py +6 -0
- {overcode-0.2.2 → overcode-0.2.3/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode.egg-info/SOURCES.txt +2 -0
- overcode-0.2.2/src/overcode/tui_widgets/summary_config_modal.py +0 -164
- {overcode-0.2.2 → overcode-0.2.3}/LICENSE +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/MANIFEST.in +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/README.md +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/setup.cfg +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/__init__.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/claude_config.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/__init__.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/_shared.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/budget.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/config.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/monitoring.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/perms.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/sister.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/cli/skills.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/config.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/data_export.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/dependency_check.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/exceptions.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/follow_mode.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/history_reader.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/hook_handler.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/hook_status_detector.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/implementations.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/interfaces.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/logging_config.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/mocks.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/notifier.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/pid_utils.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/protocols.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/sister_controller.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/status_constants.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/status_detector.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/status_detector_factory.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/status_history.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/status_patterns.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/supervisor_daemon.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/supervisor_daemon_core.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/time_context.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tmux_manager.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_actions/daemon.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_actions/input.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_actions/navigation.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_actions/session.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_logic.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_render.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/help_overlay.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/web/__init__.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/web_control_api.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/web_server.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode/web_templates.py +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.2.2 → overcode-0.2.3}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -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
|
-
|
|
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
|
|
38
|
+
- User presence state
|
|
39
39
|
"""
|
|
40
40
|
from ..monitor_daemon import MonitorDaemon, is_monitor_daemon_running, get_monitor_daemon_pid
|
|
41
41
|
|
|
@@ -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 (
|
|
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
|
|
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 =
|
|
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
|
|
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=
|
|
219
|
-
idle_seconds=
|
|
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,
|
|
225
|
-
|
|
226
|
-
|
|
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: {'
|
|
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
|
|
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
|
|
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
|
-
|
|
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("
|
|
445
|
-
print("
|
|
446
|
-
|
|
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
|
|
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()
|
|
@@ -430,12 +430,11 @@ class TUIPreferences:
|
|
|
430
430
|
notifications: str = "off" # "off", "sound", "banner", "both" — macOS notifications (#235)
|
|
431
431
|
# Session IDs of stalled agents that have been visited by the user
|
|
432
432
|
visited_stalled_agents: Set[str] = field(default_factory=set)
|
|
433
|
-
#
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
})
|
|
433
|
+
# Per-level column overrides: {"low": {"uptime": true, ...}, "med": {...}, "high": {...}}
|
|
434
|
+
# Only stores explicit user overrides. Missing = use default from detail_levels.
|
|
435
|
+
column_config: dict = field(default_factory=dict)
|
|
436
|
+
# Show abbreviated column headers above summary lines
|
|
437
|
+
show_column_headers: bool = False
|
|
439
438
|
|
|
440
439
|
@classmethod
|
|
441
440
|
def load(cls, session: str) -> "TUIPreferences":
|
|
@@ -452,14 +451,13 @@ class TUIPreferences:
|
|
|
452
451
|
if not isinstance(data, dict):
|
|
453
452
|
return cls()
|
|
454
453
|
|
|
455
|
-
#
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
454
|
+
# Migration: map "custom" detail level to "full"
|
|
455
|
+
summary_detail = data.get("summary_detail", "low")
|
|
456
|
+
if summary_detail == "custom":
|
|
457
|
+
summary_detail = "full"
|
|
458
|
+
|
|
461
459
|
return cls(
|
|
462
|
-
summary_detail=
|
|
460
|
+
summary_detail=summary_detail,
|
|
463
461
|
detail_lines=data.get("detail_lines", 5),
|
|
464
462
|
timeline_visible=data.get("timeline_visible", True),
|
|
465
463
|
daemon_panel_visible=data.get("daemon_panel_visible", False),
|
|
@@ -474,7 +472,8 @@ class TUIPreferences:
|
|
|
474
472
|
monochrome=data.get("monochrome", False),
|
|
475
473
|
show_cost=data.get("show_cost", False),
|
|
476
474
|
visited_stalled_agents=set(data.get("visited_stalled_agents", [])),
|
|
477
|
-
|
|
475
|
+
column_config=data.get("column_config", {}),
|
|
476
|
+
show_column_headers=data.get("show_column_headers", False),
|
|
478
477
|
timeline_hours=data.get("timeline_hours", 3.0),
|
|
479
478
|
notifications=data.get("notifications", "off"),
|
|
480
479
|
)
|
|
@@ -505,7 +504,8 @@ class TUIPreferences:
|
|
|
505
504
|
"monochrome": self.monochrome,
|
|
506
505
|
"show_cost": self.show_cost,
|
|
507
506
|
"visited_stalled_agents": list(self.visited_stalled_agents),
|
|
508
|
-
"
|
|
507
|
+
"column_config": self.column_config,
|
|
508
|
+
"show_column_headers": self.show_column_headers,
|
|
509
509
|
"timeline_hours": self.timeline_hours,
|
|
510
510
|
"notifications": self.notifications,
|
|
511
511
|
}, f, indent=2)
|
|
@@ -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
|
|