overcode 0.1.0__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 (43) hide show
  1. overcode/__init__.py +5 -0
  2. overcode/cli.py +812 -0
  3. overcode/config.py +72 -0
  4. overcode/daemon.py +1184 -0
  5. overcode/daemon_claude_skill.md +180 -0
  6. overcode/daemon_state.py +113 -0
  7. overcode/data_export.py +257 -0
  8. overcode/dependency_check.py +227 -0
  9. overcode/exceptions.py +219 -0
  10. overcode/history_reader.py +448 -0
  11. overcode/implementations.py +214 -0
  12. overcode/interfaces.py +49 -0
  13. overcode/launcher.py +434 -0
  14. overcode/logging_config.py +193 -0
  15. overcode/mocks.py +152 -0
  16. overcode/monitor_daemon.py +808 -0
  17. overcode/monitor_daemon_state.py +358 -0
  18. overcode/pid_utils.py +225 -0
  19. overcode/presence_logger.py +454 -0
  20. overcode/protocols.py +143 -0
  21. overcode/session_manager.py +606 -0
  22. overcode/settings.py +412 -0
  23. overcode/standing_instructions.py +276 -0
  24. overcode/status_constants.py +190 -0
  25. overcode/status_detector.py +339 -0
  26. overcode/status_history.py +164 -0
  27. overcode/status_patterns.py +264 -0
  28. overcode/summarizer_client.py +136 -0
  29. overcode/summarizer_component.py +312 -0
  30. overcode/supervisor_daemon.py +1000 -0
  31. overcode/supervisor_layout.sh +50 -0
  32. overcode/tmux_manager.py +228 -0
  33. overcode/tui.py +2549 -0
  34. overcode/tui_helpers.py +495 -0
  35. overcode/web_api.py +279 -0
  36. overcode/web_server.py +138 -0
  37. overcode/web_templates.py +563 -0
  38. overcode-0.1.0.dist-info/METADATA +87 -0
  39. overcode-0.1.0.dist-info/RECORD +43 -0
  40. overcode-0.1.0.dist-info/WHEEL +5 -0
  41. overcode-0.1.0.dist-info/entry_points.txt +2 -0
  42. overcode-0.1.0.dist-info/licenses/LICENSE +21 -0
  43. overcode-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,180 @@
1
+ # Overcode Supervisor Skill
2
+
3
+ You are the Overcode supervisor agent. Your mission: **Make all RED sessions GREEN**.
4
+
5
+ ## Your Role
6
+
7
+ You monitor and unblock Claude agent sessions running in tmux. When sessions get stuck (RED status), you help them make progress by:
8
+ - Reading their output to understand what they're stuck on
9
+ - Making decisions based on their autopilot instructions
10
+ - Approving safe permission requests
11
+ - Sending guidance or clarifying information
12
+ - Having multi-turn conversations with agents
13
+
14
+ **When all sessions are GREEN, your job is done - exit successfully.**
15
+
16
+ ## How to Control Sessions (Recommended)
17
+
18
+ Use the `overcode` CLI commands - they're simpler than raw tmux:
19
+
20
+ ### Check Status
21
+ ```bash
22
+ # List all agents with status
23
+ overcode list
24
+
25
+ # See what an agent is stuck on
26
+ overcode show my-agent
27
+ overcode show my-agent --lines 100 # more context
28
+ ```
29
+
30
+ ### Unblock Agents
31
+ ```bash
32
+ # Send a text response (+ Enter)
33
+ overcode send my-agent "yes"
34
+ overcode send my-agent "Focus on the core feature first"
35
+
36
+ # Approve a permission request (press Enter)
37
+ overcode send my-agent enter
38
+
39
+ # Reject a permission request (press Escape)
40
+ overcode send my-agent escape
41
+ ```
42
+
43
+ ## Alternative: Direct Tmux Commands
44
+
45
+ For fine-grained control, use tmux directly:
46
+
47
+ ### Read Session Output
48
+ ```bash
49
+ # Read last 50 lines from a session's pane
50
+ tmux capture-pane -t agents:{window_num} -p -S -50
51
+ ```
52
+
53
+ ### Send Text to Session
54
+ ```bash
55
+ # Send text (no Enter)
56
+ tmux send-keys -t agents:{window_num} "your text here"
57
+
58
+ # Send text with Enter
59
+ tmux send-keys -t agents:{window_num} "your text here" C-m
60
+ ```
61
+
62
+ ### Approve/Reject Permissions
63
+ ```bash
64
+ # Approve (press Enter)
65
+ tmux send-keys -t agents:{window_num} "" C-m
66
+
67
+ # Reject (press Escape)
68
+ tmux send-keys -t agents:{window_num} Escape
69
+ ```
70
+
71
+ ### List All Sessions
72
+ ```bash
73
+ # See all windows and their status
74
+ tmux list-windows -t agents
75
+ ```
76
+
77
+ ## Session State Information
78
+
79
+ Session states are tracked in `~/.overcode/sessions/sessions.json`. Read this to understand:
80
+ - Session name, window number, autopilot instructions
81
+ - Current status (running/waiting)
82
+ - Standing instructions for the session
83
+ - Repo context and working directory
84
+
85
+ ```bash
86
+ # View all session state
87
+ cat ~/.overcode/sessions/sessions.json | jq
88
+ ```
89
+
90
+ ## Approval Rules
91
+
92
+ You must follow these rules when deciding to approve operations:
93
+
94
+ ### ✅ Auto-Approve (Safe Operations)
95
+ - Read, Write, Edit, Grep, Glob within the session's working directory
96
+ - WebFetch (read-only web requests)
97
+ - git add, git commit, git status, git diff
98
+ - npm install, pip install (dependency management)
99
+ - Running tests (pytest, npm test, etc.)
100
+
101
+ ### ⚠️ Use Judgment (Check Context)
102
+ - git push (only if work is complete and tests pass)
103
+ - Operations near but not in working directory
104
+ - Creating files outside project structure
105
+
106
+ ### ❌ Reject (Unsafe/Out of Scope)
107
+ - Operations outside the working directory entirely
108
+ - rm -rf on large directories
109
+ - Operations on user's personal files (/Users/{user}/Documents, etc.)
110
+ - Network writes to external services (unless explicitly in autopilot goal)
111
+
112
+ ## Workflow Example
113
+
114
+ ```bash
115
+ # 1. Read current session states
116
+ cat ~/.overcode/sessions/sessions.json | jq '.[] | {name, tmux_window, standing_instructions, stats}'
117
+
118
+ # 2. Find RED sessions
119
+ # Check TUI or parse status
120
+
121
+ # 3. For each RED session, read output
122
+ tmux capture-pane -t agents:1 -p -S -100
123
+
124
+ # 4. Make decision based on:
125
+ # - What they're stuck on
126
+ # - Their autopilot instruction
127
+ # - Approval rules
128
+
129
+ # 5a. If permission request is safe, approve:
130
+ tmux send-keys -t agents:1 "" C-m
131
+
132
+ # 5b. If they need guidance, send message:
133
+ tmux send-keys -t agents:1 "Focus on the core feature first, implement error handling later." C-m
134
+
135
+ # 5c. If permission unsafe, reject:
136
+ tmux send-keys -t agents:1 Escape
137
+
138
+ # 6. Log your action
139
+ echo "$(date): Approved Write permission for recipe-book session (within working dir)" >> ~/.overcode/supervisor.log
140
+
141
+ # 7. Repeat for other RED sessions
142
+
143
+ # 8. When all GREEN, exit
144
+ exit 0
145
+ ```
146
+
147
+ ## Real Example
148
+
149
+ **Session:** recipe-book
150
+ **Window:** 1
151
+ **Autopilot:** "Keep organizing recipes into categories"
152
+ **Stuck on:** Permission to write `/home/user/recipes/desserts.md`
153
+
154
+ ```bash
155
+ # Read output to see context
156
+ tmux capture-pane -t agents:1 -p -S -100
157
+
158
+ # Decision: File is within working directory (/home/user/recipes)
159
+ # Decision: Aligns with "organizing recipes into categories"
160
+ # Decision: APPROVE
161
+
162
+ # Execute approval
163
+ tmux send-keys -t agents:1 "" C-m
164
+
165
+ # Log action
166
+ echo "$(date): recipe-book - Approved write to desserts.md (within working dir, aligns with goal)" >> ~/.overcode/supervisor.log
167
+ ```
168
+
169
+ ## Your Process
170
+
171
+ 1. **Survey** - Read all session states from sessions.json
172
+ 2. **Identify** - Find RED sessions (waiting for user)
173
+ 3. **Investigate** - Read their tmux output to see what they're stuck on
174
+ 4. **Decide** - Apply approval rules and autopilot context
175
+ 5. **Act** - Send tmux commands to unblock them
176
+ 6. **Log** - Record your decisions
177
+ 7. **Repeat** - Check if more sessions need help
178
+ 8. **Exit** - When all GREEN, your job is complete
179
+
180
+ 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.
@@ -0,0 +1,113 @@
1
+ """
2
+ Daemon state management.
3
+
4
+ Tracks and persists daemon state for communication with the TUI.
5
+ """
6
+
7
+ import json
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from .settings import PATHS, DAEMON
14
+
15
+
16
+ # Daemon operation modes
17
+ MODE_OFF = "off" # Daemon not running
18
+ MODE_MONITOR = "monitor" # Track stats/state but never launch supervisor claude
19
+ MODE_SUPERVISE = "supervise" # Full supervision (monitor + launch claude when needed)
20
+
21
+
22
+ @dataclass
23
+ class DaemonState:
24
+ """Tracks daemon state for TUI display.
25
+
26
+ This class is used by:
27
+ - The daemon to persist its current state
28
+ - The TUI to read and display daemon status
29
+ """
30
+
31
+ loop_count: int = 0
32
+ current_interval: int = field(default_factory=lambda: DAEMON.interval_fast)
33
+ last_loop_time: Optional[datetime] = None
34
+ started_at: Optional[datetime] = None
35
+ status: str = "starting" # starting, active, idle, sleeping, waiting, supervising, no_agents, stopped
36
+ last_activity: Optional[datetime] = None
37
+ daemon_claude_launches: int = 0
38
+ mode: str = MODE_SUPERVISE # off, monitor, supervise
39
+
40
+ def to_dict(self) -> dict:
41
+ """Convert state to dictionary for JSON serialization."""
42
+ return {
43
+ "loop_count": self.loop_count,
44
+ "current_interval": self.current_interval,
45
+ "last_loop_time": self.last_loop_time.isoformat() if self.last_loop_time else None,
46
+ "started_at": self.started_at.isoformat() if self.started_at else None,
47
+ "status": self.status,
48
+ "last_activity": self.last_activity.isoformat() if self.last_activity else None,
49
+ "daemon_claude_launches": self.daemon_claude_launches,
50
+ "mode": self.mode,
51
+ }
52
+
53
+ @classmethod
54
+ def from_dict(cls, data: dict) -> "DaemonState":
55
+ """Create state from dictionary."""
56
+ state = cls()
57
+ state.loop_count = data.get("loop_count", 0)
58
+ state.current_interval = data.get("current_interval", DAEMON.interval_fast)
59
+ state.status = data.get("status", "unknown")
60
+ state.daemon_claude_launches = data.get("daemon_claude_launches", 0)
61
+ state.mode = data.get("mode", MODE_SUPERVISE)
62
+
63
+ if data.get("last_loop_time"):
64
+ state.last_loop_time = datetime.fromisoformat(data["last_loop_time"])
65
+ if data.get("started_at"):
66
+ state.started_at = datetime.fromisoformat(data["started_at"])
67
+ if data.get("last_activity"):
68
+ state.last_activity = datetime.fromisoformat(data["last_activity"])
69
+
70
+ return state
71
+
72
+ def save(self, state_file: Optional[Path] = None) -> None:
73
+ """Save state to file for TUI to read.
74
+
75
+ Args:
76
+ state_file: Optional path override (for testing)
77
+ """
78
+ path = state_file or PATHS.daemon_state
79
+ path.parent.mkdir(parents=True, exist_ok=True)
80
+ with open(path, 'w') as f:
81
+ json.dump(self.to_dict(), f, indent=2)
82
+
83
+ @classmethod
84
+ def load(cls, state_file: Optional[Path] = None) -> Optional["DaemonState"]:
85
+ """Load state from file (used by TUI).
86
+
87
+ Args:
88
+ state_file: Optional path override (for testing)
89
+
90
+ Returns:
91
+ DaemonState if file exists and is valid, None otherwise
92
+ """
93
+ path = state_file or PATHS.daemon_state
94
+ if not path.exists():
95
+ return None
96
+
97
+ try:
98
+ with open(path) as f:
99
+ data = json.load(f)
100
+ return cls.from_dict(data)
101
+ except (json.JSONDecodeError, KeyError, ValueError):
102
+ return None
103
+
104
+
105
+ def get_daemon_state() -> Optional[DaemonState]:
106
+ """Get the current daemon state from file.
107
+
108
+ Convenience function for TUI and other consumers.
109
+
110
+ Returns:
111
+ DaemonState if daemon is running and state file exists, None otherwise
112
+ """
113
+ return DaemonState.load()
@@ -0,0 +1,257 @@
1
+ """
2
+ Data export functionality for Overcode.
3
+
4
+ Exports session data to Parquet format for analysis in Jupyter notebooks.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Dict, Any, Optional
10
+
11
+ from .session_manager import SessionManager
12
+ from .status_history import read_agent_status_history
13
+ from .presence_logger import read_presence_history
14
+
15
+
16
+ def export_to_parquet(
17
+ output_path: str,
18
+ include_archived: bool = True,
19
+ include_timeline: bool = True,
20
+ include_presence: bool = True,
21
+ ) -> Dict[str, Any]:
22
+ """Export overcode data to Parquet format.
23
+
24
+ Creates a multi-table parquet file suitable for pandas analysis.
25
+
26
+ Args:
27
+ output_path: Path to output parquet file
28
+ include_archived: Include archived sessions
29
+ include_timeline: Include agent status timeline
30
+ include_presence: Include user presence data
31
+
32
+ Returns:
33
+ Dict with counts of exported data
34
+
35
+ Raises:
36
+ ImportError: If pyarrow is not installed
37
+ """
38
+ try:
39
+ import pyarrow as pa
40
+ import pyarrow.parquet as pq
41
+ except ImportError:
42
+ raise ImportError(
43
+ "pyarrow is required for parquet export. "
44
+ "Install it with: pip install pyarrow"
45
+ )
46
+
47
+ sessions = SessionManager()
48
+ result = {
49
+ "sessions_count": 0,
50
+ "archived_count": 0,
51
+ "timeline_rows": 0,
52
+ "presence_rows": 0,
53
+ }
54
+
55
+ # Collect session data
56
+ session_records = []
57
+
58
+ # Active sessions
59
+ for s in sessions.list_sessions():
60
+ record = _session_to_record(s, is_archived=False)
61
+ session_records.append(record)
62
+
63
+ result["sessions_count"] = len(session_records)
64
+
65
+ # Archived sessions
66
+ if include_archived:
67
+ archived = sessions.list_archived_sessions()
68
+ for s in archived:
69
+ record = _session_to_record(s, is_archived=True)
70
+ record["end_time"] = getattr(s, "_end_time", None)
71
+ session_records.append(record)
72
+ result["archived_count"] = len(archived)
73
+
74
+ # Build sessions table
75
+ sessions_table = _build_sessions_table(session_records)
76
+
77
+ # Build timeline table
78
+ timeline_table = None
79
+ if include_timeline:
80
+ timeline_records = _build_timeline_records()
81
+ if timeline_records:
82
+ timeline_table = _build_timeline_table(timeline_records)
83
+ result["timeline_rows"] = len(timeline_records)
84
+
85
+ # Build presence table
86
+ presence_table = None
87
+ if include_presence:
88
+ presence_records = _build_presence_records()
89
+ if presence_records:
90
+ presence_table = _build_presence_table(presence_records)
91
+ result["presence_rows"] = len(presence_records)
92
+
93
+ # Write to parquet
94
+ # Use a directory-based approach for multiple tables
95
+ output = Path(output_path)
96
+ if output.suffix != ".parquet":
97
+ output = output.with_suffix(".parquet")
98
+
99
+ # For simplicity, write sessions as the main table
100
+ # with timeline and presence as separate files if requested
101
+ pq.write_table(sessions_table, output)
102
+
103
+ # Write additional tables as separate files
104
+ if timeline_table is not None:
105
+ timeline_path = output.with_stem(output.stem + "_timeline")
106
+ pq.write_table(timeline_table, timeline_path)
107
+
108
+ if presence_table is not None:
109
+ presence_path = output.with_stem(output.stem + "_presence")
110
+ pq.write_table(presence_table, presence_path)
111
+
112
+ return result
113
+
114
+
115
+ def _session_to_record(session, is_archived: bool) -> Dict[str, Any]:
116
+ """Convert a Session to a flat dictionary record."""
117
+ stats = session.stats
118
+ return {
119
+ "id": session.id,
120
+ "name": session.name,
121
+ "tmux_session": session.tmux_session,
122
+ "tmux_window": session.tmux_window,
123
+ "start_directory": session.start_directory,
124
+ "start_time": session.start_time,
125
+ "end_time": None, # Set for archived
126
+ "repo_name": session.repo_name,
127
+ "branch": session.branch,
128
+ "status": session.status,
129
+ "is_archived": is_archived,
130
+ "permissiveness_mode": session.permissiveness_mode,
131
+ "standing_instructions": session.standing_instructions,
132
+ "standing_instructions_preset": session.standing_instructions_preset,
133
+ # Stats
134
+ "interaction_count": stats.interaction_count,
135
+ "estimated_cost_usd": stats.estimated_cost_usd,
136
+ "total_tokens": stats.total_tokens,
137
+ "input_tokens": stats.input_tokens,
138
+ "output_tokens": stats.output_tokens,
139
+ "cache_creation_tokens": stats.cache_creation_tokens,
140
+ "cache_read_tokens": stats.cache_read_tokens,
141
+ "steers_count": stats.steers_count,
142
+ "last_activity": stats.last_activity,
143
+ "current_task": stats.current_task,
144
+ "current_state": stats.current_state,
145
+ "state_since": stats.state_since,
146
+ "green_time_seconds": stats.green_time_seconds,
147
+ "non_green_time_seconds": stats.non_green_time_seconds,
148
+ "last_stats_update": stats.last_stats_update,
149
+ }
150
+
151
+
152
+ def _build_sessions_table(records):
153
+ """Build a PyArrow table from session records."""
154
+ import pyarrow as pa
155
+
156
+ if not records:
157
+ # Return empty table with schema
158
+ schema = pa.schema([
159
+ ("id", pa.string()),
160
+ ("name", pa.string()),
161
+ ("start_time", pa.string()),
162
+ ("end_time", pa.string()),
163
+ ("is_archived", pa.bool_()),
164
+ ("interaction_count", pa.int64()),
165
+ ("total_tokens", pa.int64()),
166
+ ("estimated_cost_usd", pa.float64()),
167
+ ("green_time_seconds", pa.float64()),
168
+ ("non_green_time_seconds", pa.float64()),
169
+ ])
170
+ # Create empty arrays for each column
171
+ empty_arrays = {field.name: pa.array([], type=field.type) for field in schema}
172
+ return pa.table(empty_arrays, schema=schema)
173
+
174
+ # Build arrays from records
175
+ arrays = {}
176
+ for key in records[0].keys():
177
+ values = [r.get(key) for r in records]
178
+ arrays[key] = values
179
+
180
+ return pa.table(arrays)
181
+
182
+
183
+ def _build_timeline_records():
184
+ """Build timeline records from agent status history."""
185
+ records = []
186
+
187
+ # Read last 24 hours of timeline data
188
+ # Returns List[Tuple[datetime, agent, status, activity]]
189
+ history = read_agent_status_history(hours=24.0)
190
+
191
+ for ts, agent_name, status, activity in history:
192
+ records.append({
193
+ "timestamp": ts.isoformat() if isinstance(ts, datetime) else str(ts),
194
+ "agent": agent_name,
195
+ "status": status,
196
+ })
197
+
198
+ return records
199
+
200
+
201
+ def _build_timeline_table(records):
202
+ """Build a PyArrow table from timeline records."""
203
+ import pyarrow as pa
204
+
205
+ if not records:
206
+ schema = pa.schema([
207
+ ("timestamp", pa.string()),
208
+ ("agent", pa.string()),
209
+ ("status", pa.string()),
210
+ ])
211
+ empty_arrays = {field.name: pa.array([], type=field.type) for field in schema}
212
+ return pa.table(empty_arrays, schema=schema)
213
+
214
+ arrays = {}
215
+ for key in records[0].keys():
216
+ values = [r.get(key) for r in records]
217
+ arrays[key] = values
218
+
219
+ return pa.table(arrays)
220
+
221
+
222
+ def _build_presence_records():
223
+ """Build presence records from presence log."""
224
+ records = []
225
+
226
+ # Read last 24 hours of presence data
227
+ history = read_presence_history(hours=24.0)
228
+
229
+ for ts, state in history:
230
+ records.append({
231
+ "timestamp": ts.isoformat() if isinstance(ts, datetime) else str(ts),
232
+ "state": state,
233
+ "state_name": {1: "locked", 2: "inactive", 3: "active"}.get(state, "unknown"),
234
+ })
235
+
236
+ return records
237
+
238
+
239
+ def _build_presence_table(records):
240
+ """Build a PyArrow table from presence records."""
241
+ import pyarrow as pa
242
+
243
+ if not records:
244
+ schema = pa.schema([
245
+ ("timestamp", pa.string()),
246
+ ("state", pa.int64()),
247
+ ("state_name", pa.string()),
248
+ ])
249
+ empty_arrays = {field.name: pa.array([], type=field.type) for field in schema}
250
+ return pa.table(empty_arrays, schema=schema)
251
+
252
+ arrays = {}
253
+ for key in records[0].keys():
254
+ values = [r.get(key) for r in records]
255
+ arrays[key] = values
256
+
257
+ return pa.table(arrays)