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.
- overcode/bundled_skills.py +5 -2
- overcode/cli/agent.py +82 -0
- overcode/cli/jobs.py +104 -1
- overcode/config.py +28 -0
- overcode/daemon_claude_skill.md +43 -158
- overcode/data_export.py +8 -1
- overcode/history_reader.py +21 -2
- overcode/hook_handler.py +47 -1
- overcode/hook_status_detector.py +2 -1
- overcode/launcher.py +41 -2
- overcode/monitor_daemon.py +85 -6
- overcode/monitor_daemon_core.py +2 -33
- overcode/monitor_daemon_state.py +2 -1
- overcode/pricing.py +106 -0
- overcode/session_manager.py +3 -0
- overcode/settings.py +2 -17
- overcode/sister_poller.py +5 -2
- overcode/status_history.py +26 -11
- overcode/summarizer_client.py +78 -6
- overcode/summarizer_component.py +17 -0
- overcode/summary_columns.py +24 -0
- overcode/supervisor_daemon.py +53 -2
- overcode/supervisor_daemon_core.py +14 -7
- overcode/tmux_manager.py +7 -0
- overcode/tui.py +69 -10
- overcode/tui.tcss +2 -2
- overcode/tui_actions/daemon.py +23 -0
- overcode/tui_logic.py +2 -2
- overcode/tui_render.py +36 -1
- overcode/tui_widgets/command_bar.py +36 -6
- overcode/tui_widgets/daemon_status_bar.py +18 -15
- overcode/tui_widgets/session_summary.py +5 -1
- overcode/tui_widgets/status_timeline.py +1 -1
- overcode/web_api.py +5 -4
- overcode/web_control_api.py +6 -6
- {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/METADATA +1 -1
- {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/RECORD +41 -40
- {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/WHEEL +0 -0
- {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/entry_points.txt +0 -0
- {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/licenses/LICENSE +0 -0
- {overcode-0.3.4.dist-info → overcode-0.3.6.dist-info}/top_level.txt +0 -0
overcode/bundled_skills.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
overcode/daemon_claude_skill.md
CHANGED
|
@@ -1,183 +1,68 @@
|
|
|
1
1
|
# Overcode Supervisor Skill
|
|
2
2
|
|
|
3
|
-
You are the Overcode supervisor agent. Your mission: **
|
|
3
|
+
You are the Overcode supervisor agent. Your mission: **Unblock each non-green session once, then exit**.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Status Guide
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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
|
-
|
|
12
|
+
## Critical: Act Fast, Don't Investigate
|
|
14
13
|
|
|
15
|
-
|
|
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
|
-
|
|
16
|
+
**For each non-green session in order:**
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
overcode show my-agent
|
|
26
|
-
overcode show my-agent --lines 100 # more context
|
|
27
|
-
```
|
|
22
|
+
## How to Unblock
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
#
|
|
36
|
-
overcode send my-agent
|
|
27
|
+
# Reject a permission request
|
|
28
|
+
overcode send my-agent escape
|
|
37
29
|
|
|
38
|
-
#
|
|
39
|
-
overcode send my-agent
|
|
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
|
-
|
|
35
|
+
Follow the session's **standing instructions** first. Then apply these defaults:
|
|
92
36
|
|
|
93
|
-
###
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- git add, git commit, git status, git diff
|
|
97
|
-
-
|
|
98
|
-
-
|
|
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
|
-
###
|
|
101
|
-
- git push (only if
|
|
102
|
-
- Operations
|
|
103
|
-
-
|
|
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
|
-
###
|
|
106
|
-
- Operations outside the working directory entirely
|
|
50
|
+
### Reject
|
|
107
51
|
- rm -rf on large directories
|
|
108
|
-
- Operations on
|
|
109
|
-
- Network writes to external services (unless
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
-
|
|
178
|
-
-
|
|
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
|
overcode/history_reader.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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"):
|