overcode 0.3.4__py3-none-any.whl → 0.3.6__py3-none-any.whl

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 (41) hide show
  1. overcode/bundled_skills.py +5 -2
  2. overcode/cli/agent.py +82 -0
  3. overcode/cli/jobs.py +104 -1
  4. overcode/config.py +28 -0
  5. overcode/daemon_claude_skill.md +43 -158
  6. overcode/data_export.py +8 -1
  7. overcode/history_reader.py +21 -2
  8. overcode/hook_handler.py +47 -1
  9. overcode/hook_status_detector.py +2 -1
  10. overcode/launcher.py +41 -2
  11. overcode/monitor_daemon.py +85 -6
  12. overcode/monitor_daemon_core.py +2 -33
  13. overcode/monitor_daemon_state.py +2 -1
  14. overcode/pricing.py +106 -0
  15. overcode/session_manager.py +3 -0
  16. overcode/settings.py +2 -17
  17. overcode/sister_poller.py +5 -2
  18. overcode/status_history.py +26 -11
  19. overcode/summarizer_client.py +78 -6
  20. overcode/summarizer_component.py +17 -0
  21. overcode/summary_columns.py +24 -0
  22. overcode/supervisor_daemon.py +53 -2
  23. overcode/supervisor_daemon_core.py +14 -7
  24. overcode/tmux_manager.py +7 -0
  25. overcode/tui.py +69 -10
  26. overcode/tui.tcss +2 -2
  27. overcode/tui_actions/daemon.py +23 -0
  28. overcode/tui_logic.py +2 -2
  29. overcode/tui_render.py +36 -1
  30. overcode/tui_widgets/command_bar.py +36 -6
  31. overcode/tui_widgets/daemon_status_bar.py +18 -15
  32. overcode/tui_widgets/session_summary.py +5 -1
  33. overcode/tui_widgets/status_timeline.py +1 -1
  34. overcode/web_api.py +5 -4
  35. overcode/web_control_api.py +6 -6
  36. {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/METADATA +1 -1
  37. {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/RECORD +41 -40
  38. {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/WHEEL +0 -0
  39. {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/entry_points.txt +0 -0
  40. {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/licenses/LICENSE +0 -0
  41. {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/top_level.txt +0 -0
@@ -94,8 +94,10 @@ overcode bash "make deploy-staging" --agent my-agent # Link to an agent
94
94
 
95
95
  # Manage jobs
96
96
  overcode jobs list [--all] # List running (or all) jobs
97
+ overcode jobs tail <name> # Stream output (works without TTY)
98
+ overcode jobs tail <name> -n 50 # Last 50 lines and exit
97
99
  overcode jobs kill <name> # Kill a running job
98
- overcode jobs attach <name> # Attach to job's tmux window
100
+ overcode jobs attach <name> # Attach to job's tmux window (needs TTY)
99
101
  overcode jobs clear # Remove completed/failed/killed jobs
100
102
 
101
103
  # TUI: press J to toggle jobs view, j/k to navigate, x to kill, c to clear
@@ -227,7 +229,8 @@ overcode bash "npm run build" --name frontend-build --agent my-agent
227
229
 
228
230
  # Check on it later
229
231
  overcode jobs list
230
- overcode jobs attach full-tests # Attach to see live output
232
+ overcode jobs tail full-tests # Stream output (no TTY needed)
233
+ overcode jobs tail full-tests -n 50 # Last 50 lines snapshot
231
234
  overcode jobs kill full-tests # Kill if needed
232
235
  ```
233
236
 
overcode/cli/agent.py CHANGED
@@ -212,6 +212,10 @@ def launch(
212
212
  bool,
213
213
  typer.Option("--teams", help="Enable Claude Code agent teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)"),
214
214
  ] = False,
215
+ provider: Annotated[
216
+ Optional[str],
217
+ typer.Option("--provider", "-P", help="API provider: 'web' (Claude.ai OAuth) or 'bedrock' (AWS Bedrock)"),
218
+ ] = None,
215
219
  sister: Annotated[
216
220
  Optional[str],
217
221
  typer.Option("--sister", "-S", help="Launch on a remote sister machine (by name from config)"),
@@ -270,6 +274,15 @@ def launch(
270
274
  # Parse oversight policy
271
275
  oversight_policy, oversight_timeout_seconds = _parse_oversight_policy(on_stuck, oversight_timeout)
272
276
 
277
+ # Resolve provider: CLI flag > config default > "web"
278
+ resolved_provider = provider
279
+ if resolved_provider is None:
280
+ from ..config import get_new_agent_defaults
281
+ resolved_provider = get_new_agent_defaults().get("provider", "web")
282
+ if resolved_provider not in ("web", "bedrock"):
283
+ rprint(f"[red]Error: Invalid provider '{resolved_provider}'. Use: web, bedrock[/red]")
284
+ raise typer.Exit(code=1)
285
+
273
286
  # Default to current directory if not specified
274
287
  working_dir = directory if directory else os.getcwd()
275
288
 
@@ -288,6 +301,7 @@ def launch(
288
301
  budget_usd=budget,
289
302
  claude_agent=agent,
290
303
  model=model,
304
+ provider=resolved_provider,
291
305
  )
292
306
 
293
307
  if result:
@@ -304,6 +318,8 @@ def launch(
304
318
  rprint(f" Agent: {agent}")
305
319
  if teams:
306
320
  rprint(" Agent teams: enabled")
321
+ if resolved_provider != "web":
322
+ rprint(f" Provider: {resolved_provider}")
307
323
  if budget is not None and budget > 0:
308
324
  rprint(f" Budget: ${budget:.2f}")
309
325
 
@@ -479,6 +495,8 @@ def list_agents(
479
495
 
480
496
  # Pre-compute: any agent with budget, column alignment widths
481
497
  any_has_budget = any(s.cost_budget_usd > 0 for s in sessions)
498
+ any_has_provider = any(getattr(s, 'provider', 'web') not in ('web', None, '') for s in sessions)
499
+ any_has_model = any(getattr(s, 'model', None) for s in sessions)
482
500
  max_name_len = max((len(s.name) for s in sessions), default=10)
483
501
  name_width = min(max(max_name_len, 10), 20)
484
502
  max_repo_width = max((len(s.repo_name or "n/a") for s in sessions), default=5)
@@ -585,6 +603,8 @@ def list_agents(
585
603
  oversight_deadline=oversight_deadline,
586
604
  pr_number=getattr(sess, 'pr_number', None),
587
605
  any_has_pr=any_has_pr,
606
+ any_has_model=any_has_model,
607
+ any_has_provider=any_has_provider,
588
608
  monochrome=False,
589
609
  summary_detail=detail,
590
610
  has_sisters=has_sisters,
@@ -673,6 +693,59 @@ def kill(
673
693
  launcher.kill_session(name, cascade=not no_cascade)
674
694
 
675
695
 
696
+ @app.command()
697
+ def restart(
698
+ name: Annotated[str, typer.Argument(help="Name of agent to restart")],
699
+ session: SessionOption = "agents",
700
+ ):
701
+ """Restart a running agent with the same configuration.
702
+
703
+ Gracefully exits Claude (Ctrl-C + /exit), then relaunches with the
704
+ same permissions mode. Useful when MCP server configs change.
705
+ """
706
+ import os
707
+ import time
708
+ from ..session_manager import SessionManager
709
+ from ..tmux_manager import TmuxManager
710
+ from ..tmux_utils import tmux_window_target, _build_tmux_cmd
711
+
712
+ sm = SessionManager()
713
+ sess = sm.get_session_by_name(name)
714
+ if not sess:
715
+ rprint(f"[red]Error: Agent '{name}' not found[/red]")
716
+ raise typer.Exit(code=1)
717
+
718
+ tmux = TmuxManager(session)
719
+ if not tmux.window_exists(sess.tmux_window):
720
+ rprint(f"[red]Error: Tmux window for '{name}' no longer exists[/red]")
721
+ raise typer.Exit(code=1)
722
+
723
+ # Build the claude command based on permissiveness mode
724
+ claude_command = os.environ.get("CLAUDE_COMMAND", "claude")
725
+ cmd_parts = [claude_command]
726
+ if sess.permissiveness_mode == "bypass":
727
+ cmd_parts.append("--dangerously-skip-permissions")
728
+ elif sess.permissiveness_mode == "permissive":
729
+ cmd_parts.extend(["--permission-mode", "dontAsk"])
730
+ cmd_str = " ".join(cmd_parts)
731
+
732
+ # Gracefully exit Claude: Ctrl-C + /exit
733
+ rprint(f"[dim]Stopping '{name}'...[/dim]")
734
+ tmux.send_keys(sess.tmux_window, "C-c", enter=False)
735
+ time.sleep(0.5)
736
+ tmux.send_keys(sess.tmux_window, "/exit", enter=True)
737
+ time.sleep(3.0)
738
+
739
+ # Relaunch
740
+ if tmux.send_keys(sess.tmux_window, cmd_str, enter=True):
741
+ sm.update_stats(sess.id, current_task="Restarting...")
742
+ sm.update_session(sess.id, claude_session_ids=[])
743
+ rprint(f"[green]Restarted agent: {name}[/green]")
744
+ else:
745
+ rprint(f"[red]Failed to restart agent: {name}[/red]")
746
+ raise typer.Exit(code=1)
747
+
748
+
676
749
  @app.command()
677
750
  def follow(
678
751
  name: Annotated[str, typer.Argument(help="Name of agent to follow")],
@@ -903,6 +976,15 @@ def send(
903
976
  overcode send my-agent escape # Press Escape (reject)
904
977
  overcode send my-agent --no-enter "y" # Send "y" without Enter
905
978
  """
979
+ from ..session_manager import SessionManager
980
+ sm = SessionManager()
981
+
982
+ # Auto-wake sleeping agent (#168)
983
+ agent_session = sm.get_session_by_name(name)
984
+ if agent_session and agent_session.is_asleep:
985
+ sm.update_session(agent_session.id, is_asleep=False)
986
+ rprint(f"[dim]Woke agent '{name}' to send command[/dim]")
987
+
906
988
  launcher = ClaudeLauncher(session)
907
989
 
908
990
  # Join all text parts if multiple were given
overcode/cli/jobs.py CHANGED
@@ -55,7 +55,10 @@ def bash(
55
55
  rprint(f" Linked to agent: {agent_name}")
56
56
 
57
57
  if follow:
58
- launcher.attach(job.name)
58
+ if os.isatty(0):
59
+ launcher.attach(job.name)
60
+ else:
61
+ rprint(f" [dim]No TTY — use 'overcode jobs tail {job.name}' to stream output[/dim]")
59
62
 
60
63
 
61
64
  @jobs_app.command("list")
@@ -144,6 +147,106 @@ def clear_completed():
144
147
  rprint("[green]✓[/green] Cleared completed jobs")
145
148
 
146
149
 
150
+ @jobs_app.command("tail")
151
+ def tail_job(
152
+ name: Annotated[str, typer.Argument(help="Job name to tail")],
153
+ lines: Annotated[Optional[int], typer.Option("--lines", "-n", help="Show last N lines and exit")] = None,
154
+ follow: Annotated[bool, typer.Option("--follow/--no-follow", "-f", help="Stream output until job completes")] = True,
155
+ poll_interval: Annotated[float, typer.Option("--poll", hidden=True)] = 0.5,
156
+ ):
157
+ """Stream a job's output (like tail -f). Works without a TTY.
158
+
159
+ Default: streams output until the job completes or Ctrl-C.
160
+ With --lines N: shows last N lines and exits.
161
+
162
+ Examples:
163
+ overcode jobs tail my-job
164
+ overcode jobs tail my-job --lines 50
165
+ overcode jobs tail my-job --no-follow
166
+ """
167
+ import signal
168
+ import sys
169
+ import time
170
+ from collections import deque
171
+ from ..follow_mode import _capture_pane, _find_dedup_start
172
+ from ..job_manager import JobManager
173
+ from ..status_patterns import strip_ansi
174
+
175
+ manager = JobManager()
176
+ job = manager.get_job_by_name(name)
177
+ if not job:
178
+ rprint(f"[red]Error: Job '{name}' not found[/red]")
179
+ raise typer.Exit(1)
180
+
181
+ tmux_session = job.tmux_session
182
+ window_name = job.tmux_window
183
+
184
+ # One-shot: capture and print last N lines
185
+ if lines is not None:
186
+ raw = _capture_pane(tmux_session, window_name, lines=lines)
187
+ if raw:
188
+ for line in raw.rstrip().split('\n'):
189
+ cleaned = strip_ansi(line).strip()
190
+ if cleaned:
191
+ print(cleaned)
192
+ return
193
+
194
+ # Streaming mode
195
+ interrupted = False
196
+
197
+ def _sigint(sig, frame):
198
+ nonlocal interrupted
199
+ interrupted = True
200
+
201
+ old_handler = signal.signal(signal.SIGINT, _sigint)
202
+
203
+ recent_lines: deque = deque(maxlen=50)
204
+
205
+ try:
206
+ while not interrupted:
207
+ raw = _capture_pane(tmux_session, window_name)
208
+ if raw:
209
+ new_lines = []
210
+ for line in raw.rstrip().split('\n'):
211
+ cleaned = strip_ansi(line).strip()
212
+ new_lines.append(cleaned)
213
+
214
+ output_start = _find_dedup_start(new_lines, recent_lines)
215
+ if output_start < len(new_lines):
216
+ for line in new_lines[output_start:]:
217
+ if line:
218
+ print(line)
219
+ recent_lines.append(line)
220
+
221
+ # Check if job is done
222
+ if follow:
223
+ job = manager.get_job_by_name(name)
224
+ if job and job.status in ("completed", "failed", "killed"):
225
+ # Final capture
226
+ time.sleep(poll_interval)
227
+ raw = _capture_pane(tmux_session, window_name)
228
+ if raw:
229
+ new_lines = [strip_ansi(l).strip() for l in raw.rstrip().split('\n')]
230
+ output_start = _find_dedup_start(new_lines, recent_lines)
231
+ for line in new_lines[output_start:]:
232
+ if line:
233
+ print(line)
234
+ recent_lines.append(line)
235
+ status_msg = f"completed (exit {job.exit_code})" if job.exit_code is not None else job.status
236
+ print(f"\n[tail] Job '{name}' {status_msg}", file=sys.stderr)
237
+ raise typer.Exit(0 if job.status == "completed" else 1)
238
+ else:
239
+ # --no-follow: one capture cycle then exit
240
+ break
241
+
242
+ time.sleep(poll_interval)
243
+ finally:
244
+ signal.signal(signal.SIGINT, old_handler)
245
+
246
+ if interrupted:
247
+ raise typer.Exit(130)
248
+
249
+
147
250
  @jobs_app.command("_complete", hidden=True)
148
251
  def mark_complete(
149
252
  job_id: Annotated[str, typer.Argument(help="Job ID")],
overcode/config.py CHANGED
@@ -118,8 +118,10 @@ def get_summarizer_config() -> dict:
118
118
  default_api_url = "https://api.openai.com/v1/chat/completions"
119
119
  default_model = "gpt-4o-mini"
120
120
  default_api_key_var = "OPENAI_API_KEY"
121
+ default_api_type = "openai"
121
122
 
122
123
  # Config file takes precedence, env vars are fallback
124
+ api_type = _get_config_value("summarizer.api_type") or os.environ.get("OVERCODE_SUMMARIZER_API_TYPE") or default_api_type
123
125
  api_url = _get_config_value("summarizer.api_url") or os.environ.get("OVERCODE_SUMMARIZER_API_URL") or default_api_url
124
126
  model = _get_config_value("summarizer.model") or os.environ.get("OVERCODE_SUMMARIZER_MODEL") or default_model
125
127
  api_key_var = _get_config_value("summarizer.api_key_var") or os.environ.get("OVERCODE_SUMMARIZER_API_KEY_VAR") or default_api_key_var
@@ -127,11 +129,20 @@ def get_summarizer_config() -> dict:
127
129
  # Resolve the actual API key from the configured env var
128
130
  api_key = os.environ.get(api_key_var)
129
131
 
132
+ # Cost cap: default $100, configurable
133
+ cost_cap = _get_config_value("summarizer.cost_cap")
134
+ if cost_cap is None:
135
+ cost_cap = 100.0
136
+ else:
137
+ cost_cap = float(cost_cap)
138
+
130
139
  return {
140
+ "api_type": api_type,
131
141
  "api_url": api_url,
132
142
  "model": model,
133
143
  "api_key": api_key,
134
144
  "api_key_var": api_key_var,
145
+ "cost_cap": cost_cap,
135
146
  }
136
147
 
137
148
 
@@ -340,6 +351,7 @@ def get_new_agent_defaults() -> dict:
340
351
  return {
341
352
  "bypass_permissions": bool(defaults.get("bypass_permissions", False)),
342
353
  "agent_teams": bool(defaults.get("agent_teams", False)),
354
+ "provider": defaults.get("provider", "web"),
343
355
  }
344
356
 
345
357
 
@@ -354,6 +366,22 @@ def save_new_agent_defaults(defaults: dict) -> None:
354
366
  save_config(config)
355
367
 
356
368
 
369
+ def get_bedrock_config() -> dict:
370
+ """Get AWS Bedrock configuration.
371
+
372
+ Config format in ~/.overcode/config.yaml:
373
+ bedrock:
374
+ region: us-east-1
375
+
376
+ Returns:
377
+ Dict with region (str).
378
+ """
379
+ bedrock = _get_config_value("bedrock", {})
380
+ return {
381
+ "region": bedrock.get("region", "us-east-1"),
382
+ }
383
+
384
+
357
385
  def get_jobs_retention_hours() -> float:
358
386
  """Get job retention period in hours.
359
387
 
@@ -1,183 +1,68 @@
1
1
  # Overcode Supervisor Skill
2
2
 
3
- You are the Overcode supervisor agent. Your mission: **Attempt to unblock each RED session once, then exit**.
3
+ You are the Overcode supervisor agent. Your mission: **Unblock each non-green session once, then exit**.
4
4
 
5
- ## Your Role
5
+ ## Status Guide
6
6
 
7
- You unblock Claude agent sessions running in tmux. When sessions are stuck (RED status), you make ONE attempt to help each by:
8
- - Reading their output to understand what they're stuck on
9
- - Making decisions based on their standing instructions
10
- - Approving safe permission requests
11
- - Sending guidance or clarifying information
7
+ - ORANGE (`waiting_approval`) -- Agent blocked on a permission prompt. This is your PRIMARY target. Approve or reject based on standing instructions and approval rules.
8
+ - RED (`waiting_user`) -- Agent waiting for human input at the prompt. If it has standing instructions, send guidance. If not, skip it.
9
+ - YELLOW (`busy_sleeping`) -- Agent is sleeping. Usually skip.
10
+ - PURPLE (`error`) -- API error. Usually skip.
12
11
 
13
- **IMPORTANT: Make ONE attempt per RED session, then exit. Do not loop or wait to see if your action worked. The supervisor daemon will call you again later if sessions are still RED.**
12
+ ## Critical: Act Fast, Don't Investigate
14
13
 
15
- ## How to Control Sessions (Recommended)
14
+ You have LIMITED TIME. Do NOT waste it on `overcode list` or reading sessions.json -- the context below already tells you which sessions need help and their standing instructions.
16
15
 
17
- Use the `overcode` CLI commands - they're simpler than raw tmux:
16
+ **For each non-green session in order:**
18
17
 
19
- ### Check Status
20
- ```bash
21
- # List all agents with status
22
- overcode list
18
+ 1. Run `overcode show <name>` to see what it's stuck on
19
+ 2. Immediately act: `overcode send <name> enter` (approve) or `overcode send <name> escape` (reject)
20
+ 3. Move to the next session -- do NOT check if it worked
23
21
 
24
- # See what an agent is stuck on
25
- overcode show my-agent
26
- overcode show my-agent --lines 100 # more context
27
- ```
22
+ ## How to Unblock
28
23
 
29
- ### Unblock Agents
30
- ```bash
31
- # Send a text response (+ Enter)
32
- overcode send my-agent "yes"
33
- overcode send my-agent "Focus on the core feature first"
24
+ # Approve a permission request (ORANGE sessions)
25
+ overcode send my-agent enter
34
26
 
35
- # Approve a permission request (press Enter)
36
- overcode send my-agent enter
27
+ # Reject a permission request
28
+ overcode send my-agent escape
37
29
 
38
- # Reject a permission request (press Escape)
39
- overcode send my-agent escape
40
- ```
41
-
42
- ## Alternative: Direct Tmux Commands
43
-
44
- For fine-grained control, use tmux directly:
45
-
46
- ### Read Session Output
47
- ```bash
48
- # Read last 50 lines from a session's pane
49
- tmux capture-pane -t agents:{window_num} -p -S -50
50
- ```
51
-
52
- ### Send Text to Session
53
- ```bash
54
- # Send text (no Enter)
55
- tmux send-keys -t agents:{window_num} "your text here"
56
-
57
- # Send text with Enter
58
- tmux send-keys -t agents:{window_num} "your text here" C-m
59
- ```
60
-
61
- ### Approve/Reject Permissions
62
- ```bash
63
- # Approve (press Enter)
64
- tmux send-keys -t agents:{window_num} "" C-m
65
-
66
- # Reject (press Escape)
67
- tmux send-keys -t agents:{window_num} Escape
68
- ```
69
-
70
- ### List All Sessions
71
- ```bash
72
- # See all windows and their status
73
- tmux list-windows -t agents
74
- ```
75
-
76
- ## Session State Information
77
-
78
- Session states are tracked in `~/.overcode/sessions/sessions.json`. Read this to understand:
79
- - Session name, window number, autopilot instructions
80
- - Current status (running/waiting)
81
- - Standing instructions for the session
82
- - Repo context and working directory
83
-
84
- ```bash
85
- # View all session state
86
- cat ~/.overcode/sessions/sessions.json | jq
87
- ```
30
+ # Send text response (RED sessions with instructions)
31
+ overcode send my-agent "your guidance here"
88
32
 
89
33
  ## Approval Rules
90
34
 
91
- You must follow these rules when deciding to approve operations:
35
+ Follow the session's **standing instructions** first. Then apply these defaults:
92
36
 
93
- ### Auto-Approve (Safe Operations)
94
- - Read, Write, Edit, Grep, Glob within the session's working directory
95
- - WebFetch (read-only web requests)
96
- - git add, git commit, git status, git diff
97
- - npm install, pip install (dependency management)
98
- - Running tests (pytest, npm test, etc.)
37
+ ### Auto-Approve
38
+ - File reads/writes/edits, Grep, Glob
39
+ - Shell commands: ls, cat, head, tail, find, grep, mkdir, touch, wc, sort, diff
40
+ - git add, git commit, git status, git diff, git log, git branch
41
+ - Running tests, linters, builds
42
+ - WebFetch, web searches
43
+ - pip/npm/uv install
99
44
 
100
- ### ⚠️ Use Judgment (Check Context)
101
- - git push (only if work is complete and tests pass)
102
- - Operations near but not in working directory
103
- - Creating files outside project structure
45
+ ### Use Judgment
46
+ - git push (only if tests pass)
47
+ - Operations outside the project directory
48
+ - Destructive operations (rm, git reset)
104
49
 
105
- ### Reject (Unsafe/Out of Scope)
106
- - Operations outside the working directory entirely
50
+ ### Reject
107
51
  - rm -rf on large directories
108
- - Operations on user's personal files (/Users/{user}/Documents, etc.)
109
- - Network writes to external services (unless explicitly in autopilot goal)
110
-
111
- ## Workflow Example
112
-
113
- ```bash
114
- # 1. Read current session states
115
- cat ~/.overcode/sessions/sessions.json | jq '.[] | {name, tmux_window, standing_instructions, stats}'
116
-
117
- # 2. Find RED sessions (use overcode list)
118
- overcode list
119
-
120
- # 3. For EACH RED session, make ONE attempt:
121
-
122
- # a. Read output to understand what they're stuck on
123
- overcode show agent-name --lines 100
124
-
125
- # b. Make decision based on:
126
- # - What they're stuck on
127
- # - Their standing instructions
128
- # - Approval rules below
129
-
130
- # c. Take action:
131
- overcode send agent-name enter # Approve permission
132
- overcode send agent-name escape # Reject permission
133
- overcode send agent-name "guidance" # Send instructions
134
-
135
- # d. Move to next RED session immediately (don't wait)
136
-
137
- # 4. After attempting ALL RED sessions once, EXIT
138
- exit 0
139
- ```
140
-
141
- **Key point:** Do NOT loop back to check if sessions turned green. Make one attempt per session and exit. The supervisor daemon will invoke you again if needed.
142
-
143
- ## Real Example
144
-
145
- **Session:** recipe-book
146
- **Window:** 1
147
- **Autopilot:** "Keep organizing recipes into categories"
148
- **Stuck on:** Permission to write `/home/user/recipes/desserts.md`
149
-
150
- ```bash
151
- # Read output to see context
152
- tmux capture-pane -t agents:1 -p -S -100
153
-
154
- # Decision: File is within working directory (/home/user/recipes)
155
- # Decision: Aligns with "organizing recipes into categories"
156
- # Decision: APPROVE
157
-
158
- # Execute approval
159
- tmux send-keys -t agents:1 "" C-m
160
-
161
- # Log action
162
- echo "$(date): recipe-book - Approved write to desserts.md (within working dir, aligns with goal)" >> ~/.overcode/supervisor.log
163
- ```
52
+ - Operations on system files
53
+ - Network writes to external services (unless in standing instructions)
164
54
 
165
55
  ## Your Process
166
56
 
167
- 1. **Survey** - Run `overcode list` to see all sessions and their status
168
- 2. **Identify** - Note which sessions are RED (waiting for user)
169
- 3. **For each RED session:**
170
- - **Investigate** - Run `overcode show <name>` to see what they're stuck on
171
- - **Decide** - Apply approval rules and check their standing instructions
172
- - **Act** - Send ONE command to unblock them
173
- - **Move on** - Immediately proceed to next RED session
174
- 4. **Exit** - After attempting each RED session once, run `exit 0`
57
+ For EACH non-green session listed in the context below:
58
+ 1. `overcode show <name>` -- see what it needs
59
+ 2. Decide and act immediately
60
+ 3. Move on
61
+
62
+ After attempting ALL sessions once, run `exit 0`. The daemon will call you again if needed.
175
63
 
176
64
  **Do NOT:**
177
- - Loop back to check if sessions turned green
178
- - Wait to see if your action worked
65
+ - Run `overcode list` (you already have the list)
66
+ - Read sessions.json (you already have the context)
67
+ - Loop back to check results
179
68
  - Make multiple attempts on the same session
180
-
181
- The supervisor daemon runs continuously and will invoke you again if sessions are still RED.
182
-
183
- Remember: You're a decision-making agent that helps other agents make progress. Be helpful but safe. When in doubt, err on the side of caution.
overcode/data_export.py CHANGED
@@ -8,6 +8,7 @@ from datetime import datetime
8
8
  from pathlib import Path
9
9
  from typing import Dict, Any
10
10
 
11
+ from .config import get_hostname
11
12
  from .session_manager import SessionManager
12
13
  from .status_history import read_agent_status_history
13
14
  from .presence_logger import read_presence_history
@@ -118,6 +119,7 @@ def _session_to_record(session, is_archived: bool) -> Dict[str, Any]:
118
119
  return {
119
120
  "id": session.id,
120
121
  "name": session.name,
122
+ "hostname": getattr(session, 'source_host', '') or get_hostname(),
121
123
  "tmux_session": session.tmux_session,
122
124
  "tmux_window": session.tmux_window,
123
125
  "start_directory": session.start_directory,
@@ -184,6 +186,7 @@ def _get_sessions_schema():
184
186
  return pa.schema([
185
187
  ("id", pa.string()),
186
188
  ("name", pa.string()),
189
+ ("hostname", pa.string()),
187
190
  ("start_time", pa.string()),
188
191
  ("end_time", pa.string()),
189
192
  ("is_archived", pa.bool_()),
@@ -201,6 +204,8 @@ def _get_timeline_schema():
201
204
  ("timestamp", pa.string()),
202
205
  ("agent", pa.string()),
203
206
  ("status", pa.string()),
207
+ ("session_id", pa.string()),
208
+ ("hostname", pa.string()),
204
209
  ])
205
210
 
206
211
 
@@ -223,11 +228,13 @@ def _build_timeline_records():
223
228
  records = []
224
229
  history = read_agent_status_history(hours=24.0)
225
230
 
226
- for ts, agent_name, status, activity in history:
231
+ for ts, agent_name, status, activity, session_id, hostname in history:
227
232
  records.append({
228
233
  "timestamp": ts.isoformat() if isinstance(ts, datetime) else str(ts),
229
234
  "agent": agent_name,
230
235
  "status": status,
236
+ "session_id": session_id,
237
+ "hostname": hostname,
231
238
  })
232
239
 
233
240
  return records
@@ -650,8 +650,15 @@ def get_session_stats(
650
650
  interactions = hf.get_interactions_for_session(session)
651
651
  interaction_count = len(interactions)
652
652
 
653
- # Derive Claude sessionIds from the already-scoped interactions
653
+ # Derive Claude sessionIds and their project paths from interactions.
654
+ # Claude Code may store session files under a different project path
655
+ # than start_directory (e.g., when the directory doesn't exist or Claude
656
+ # chooses a different project root).
654
657
  session_ids = {e.session_id for e in interactions if e.session_id}
658
+ sid_to_project: Dict[str, str] = {}
659
+ for e in interactions:
660
+ if e.session_id and e.project:
661
+ sid_to_project[e.session_id] = e.project
655
662
 
656
663
  # Active session ID for context window after /clear (#116)
657
664
  active_session_id = getattr(session, 'active_claude_session_id', None)
@@ -673,6 +680,16 @@ def get_session_stats(
673
680
  session_file = get_session_file_path(
674
681
  session.start_directory, sid, projects_path
675
682
  )
683
+ # Fall back to the project path from history entries if the session
684
+ # file doesn't exist at the expected start_directory path. Claude
685
+ # Code may use a different project root (e.g. home dir) when the
686
+ # launch directory no longer exists.
687
+ if not session_file.exists():
688
+ alt_project = sid_to_project.get(sid)
689
+ if alt_project:
690
+ session_file = get_session_file_path(
691
+ alt_project, sid, projects_path
692
+ )
676
693
  usage, work_times = read_session_file_stats(session_file, since=session_start)
677
694
  total_input += usage["input_tokens"]
678
695
  total_output += usage["output_tokens"]
@@ -695,7 +712,9 @@ def get_session_stats(
695
712
  all_work_times.extend(work_times)
696
713
 
697
714
  # Check for subagent files in {sessionId}/subagents/
698
- encoded = encode_project_path(session.start_directory)
715
+ # Use the actual project path where the session file was found.
716
+ actual_project = sid_to_project.get(sid, session.start_directory)
717
+ encoded = encode_project_path(actual_project)
699
718
  subagents_dir = projects_path / encoded / sid / "subagents"
700
719
  if subagents_dir.exists():
701
720
  for subagent_file in subagents_dir.glob("agent-*.jsonl"):