overcode 0.1.2__py3-none-any.whl → 0.1.4__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 (50) hide show
  1. overcode/__init__.py +1 -1
  2. overcode/cli.py +154 -51
  3. overcode/config.py +66 -0
  4. overcode/daemon_claude_skill.md +36 -33
  5. overcode/history_reader.py +69 -8
  6. overcode/implementations.py +178 -87
  7. overcode/monitor_daemon.py +87 -97
  8. overcode/monitor_daemon_core.py +261 -0
  9. overcode/monitor_daemon_state.py +24 -15
  10. overcode/pid_utils.py +17 -3
  11. overcode/session_manager.py +54 -0
  12. overcode/settings.py +34 -0
  13. overcode/status_constants.py +1 -1
  14. overcode/status_detector.py +8 -2
  15. overcode/status_patterns.py +19 -0
  16. overcode/summarizer_client.py +72 -27
  17. overcode/summarizer_component.py +87 -107
  18. overcode/supervisor_daemon.py +55 -38
  19. overcode/supervisor_daemon_core.py +210 -0
  20. overcode/testing/__init__.py +6 -0
  21. overcode/testing/renderer.py +268 -0
  22. overcode/testing/tmux_driver.py +223 -0
  23. overcode/testing/tui_eye.py +185 -0
  24. overcode/testing/tui_eye_skill.md +187 -0
  25. overcode/tmux_manager.py +117 -93
  26. overcode/tui.py +399 -1969
  27. overcode/tui_actions/__init__.py +20 -0
  28. overcode/tui_actions/daemon.py +201 -0
  29. overcode/tui_actions/input.py +128 -0
  30. overcode/tui_actions/navigation.py +117 -0
  31. overcode/tui_actions/session.py +428 -0
  32. overcode/tui_actions/view.py +357 -0
  33. overcode/tui_helpers.py +42 -9
  34. overcode/tui_logic.py +347 -0
  35. overcode/tui_render.py +414 -0
  36. overcode/tui_widgets/__init__.py +24 -0
  37. overcode/tui_widgets/command_bar.py +399 -0
  38. overcode/tui_widgets/daemon_panel.py +153 -0
  39. overcode/tui_widgets/daemon_status_bar.py +245 -0
  40. overcode/tui_widgets/help_overlay.py +71 -0
  41. overcode/tui_widgets/preview_pane.py +69 -0
  42. overcode/tui_widgets/session_summary.py +514 -0
  43. overcode/tui_widgets/status_timeline.py +253 -0
  44. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/METADATA +4 -1
  45. overcode-0.1.4.dist-info/RECORD +68 -0
  46. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/WHEEL +1 -1
  47. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/entry_points.txt +1 -0
  48. overcode-0.1.2.dist-info/RECORD +0 -45
  49. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/licenses/LICENSE +0 -0
  50. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/top_level.txt +0 -0
@@ -2,54 +2,93 @@
2
2
  OpenAI API client for agent summarization.
3
3
 
4
4
  Uses GPT-4o-mini for cost-effective, high-frequency summaries.
5
+
6
+ Configuration via ~/.overcode/config.yaml (preferred) or environment variables (fallback):
7
+
8
+ Config file format:
9
+ summarizer:
10
+ api_url: https://api.openai.com/v1/chat/completions
11
+ model: gpt-4o-mini
12
+ api_key_var: OPENAI_API_KEY
13
+
14
+ Environment variable fallbacks:
15
+ OVERCODE_SUMMARIZER_API_URL
16
+ OVERCODE_SUMMARIZER_MODEL
17
+ OVERCODE_SUMMARIZER_API_KEY_VAR
5
18
  """
6
19
 
7
20
  import json
8
21
  import logging
9
- import os
10
22
  import urllib.error
11
23
  import urllib.request
12
24
  from typing import Optional
13
25
 
14
- logger = logging.getLogger(__name__)
26
+ from .config import get_summarizer_config
15
27
 
16
- MODEL = "gpt-4o-mini"
17
- API_URL = "https://api.openai.com/v1/chat/completions"
28
+ logger = logging.getLogger(__name__)
18
29
 
19
- # Anti-oscillation prompt template
20
- SUMMARIZE_PROMPT = """You are summarizing a Claude Code agent's terminal output.
30
+ # Short summary prompt - focuses on IMMEDIATE ACTION (verb-first, what's happening this second)
31
+ SUMMARIZE_PROMPT_SHORT = """What is the agent doing RIGHT NOW? Answer with the immediate action only.
21
32
 
22
- ## Current Terminal Content (last {lines} lines):
33
+ ## Terminal (last {lines} lines):
23
34
  {pane_content}
24
35
 
25
- ## Current Status: {status}
36
+ ## Previous:
37
+ {previous_summary}
38
+
39
+ FORMAT: Start with a verb. Examples:
40
+ - "reading src/auth.py"
41
+ - "running pytest -v"
42
+ - "waiting for approval"
43
+ - "writing migration file"
44
+ - "editing line 45"
26
45
 
27
- ## Previous Summary:
46
+ RULES:
47
+ - Verb first, always (reading/writing/running/waiting/editing/fixing)
48
+ - Name the specific file or command if visible
49
+ - Max 40 chars
50
+ - If unchanged: UNCHANGED"""
51
+
52
+ # Context summary prompt - focuses on THE TASK (noun-first, the feature/bug/goal)
53
+ SUMMARIZE_PROMPT_CONTEXT = """What TASK or FEATURE is being worked on? Not the current action - the goal.
54
+
55
+ ## Terminal (last {lines} lines):
56
+ {pane_content}
57
+
58
+ ## Previous:
28
59
  {previous_summary}
29
60
 
30
- ## Instructions:
31
- 1. Summarize what the agent has been doing in 1-2 sentences
32
- 2. If not running, explain the halt state (waiting for user input, permission needed, etc.)
33
- 3. IMPORTANT: Only provide a new summary if there's meaningful new information.
34
- If the situation is essentially the same as the previous summary, respond with exactly:
35
- UNCHANGED
61
+ FORMAT: Describe the task/feature/bug. Examples:
62
+ - "JWT auth migration"
63
+ - "user search pagination"
64
+ - "fix: race condition in queue"
65
+ - "PR #42 review comments"
66
+ - "new settings dark mode"
36
67
 
37
- Keep summaries concise (under 80 words). Focus on:
38
- - What task/feature is being worked on
39
- - Current action (reading files, writing code, running tests, etc.)
40
- - If halted: why and what's needed to continue"""
68
+ RULES:
69
+ - Noun/task first (not a verb like "implementing")
70
+ - Include ticket/PR numbers if mentioned
71
+ - Focus on WHAT is being built/fixed, not HOW
72
+ - Max 60 chars
73
+ - If unchanged: UNCHANGED"""
41
74
 
42
75
 
43
76
  class SummarizerClient:
44
- """Client for OpenAI API to generate agent summaries."""
77
+ """Client for OpenAI-compatible API to generate agent summaries.
78
+
79
+ Supports custom API endpoints for corporate gateways via config file or env vars.
80
+ """
45
81
 
46
82
  def __init__(self, api_key: Optional[str] = None):
47
83
  """Initialize the client.
48
84
 
49
85
  Args:
50
- api_key: OpenAI API key. If None, reads from OPENAI_API_KEY env var.
86
+ api_key: API key. If None, reads from config file or env var.
51
87
  """
52
- self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
88
+ config = get_summarizer_config()
89
+ self.api_url = config["api_url"]
90
+ self.model = config["model"]
91
+ self.api_key = api_key or config["api_key"]
53
92
  self._available = bool(self.api_key)
54
93
 
55
94
  @property
@@ -64,6 +103,7 @@ class SummarizerClient:
64
103
  current_status: str,
65
104
  lines: int = 200,
66
105
  max_tokens: int = 150,
106
+ mode: str = "short",
67
107
  ) -> Optional[str]:
68
108
  """Get summary from GPT-4o-mini.
69
109
 
@@ -73,6 +113,7 @@ class SummarizerClient:
73
113
  current_status: Current agent status (running, waiting_user, etc.)
74
114
  lines: Number of lines being summarized (for prompt context)
75
115
  max_tokens: Maximum tokens in response
116
+ mode: "short" for current activity, "context" for wider context
76
117
 
77
118
  Returns:
78
119
  New summary text, "UNCHANGED" if no update needed, or None on error
@@ -80,7 +121,10 @@ class SummarizerClient:
80
121
  if not self.available:
81
122
  return None
82
123
 
83
- prompt = SUMMARIZE_PROMPT.format(
124
+ # Select prompt based on mode
125
+ prompt_template = SUMMARIZE_PROMPT_CONTEXT if mode == "context" else SUMMARIZE_PROMPT_SHORT
126
+
127
+ prompt = prompt_template.format(
84
128
  lines=lines,
85
129
  pane_content=pane_content,
86
130
  status=current_status,
@@ -88,14 +132,14 @@ class SummarizerClient:
88
132
  )
89
133
 
90
134
  payload = json.dumps({
91
- "model": MODEL,
135
+ "model": self.model,
92
136
  "max_tokens": max_tokens,
93
137
  "temperature": 0.3, # Low temperature for consistent summaries
94
138
  "messages": [{"role": "user", "content": prompt}],
95
139
  }).encode("utf-8")
96
140
 
97
141
  req = urllib.request.Request(
98
- API_URL,
142
+ self.api_url,
99
143
  data=payload,
100
144
  headers={
101
145
  "Authorization": f"Bearer {self.api_key}",
@@ -132,5 +176,6 @@ class SummarizerClient:
132
176
 
133
177
  @staticmethod
134
178
  def is_available() -> bool:
135
- """Check if OPENAI_API_KEY is set in environment."""
136
- return bool(os.environ.get("OPENAI_API_KEY"))
179
+ """Check if API key is available (from config or environment)."""
180
+ config = get_summarizer_config()
181
+ return bool(config["api_key"])
@@ -3,18 +3,17 @@ Summarizer component for generating agent activity summaries.
3
3
 
4
4
  Uses GPT-4o-mini to summarize what each agent has been doing and
5
5
  their current halt state if not running.
6
+
7
+ Note: The summarizer now lives in the TUI, not the daemon.
8
+ This ensures zero API costs when the TUI is closed (no one would see the summaries anyway).
6
9
  """
7
10
 
8
- import json
9
11
  import logging
10
- import os
11
- from dataclasses import dataclass, field
12
+ from dataclasses import dataclass
12
13
  from datetime import datetime
13
- from pathlib import Path
14
14
  from typing import Dict, Optional, TYPE_CHECKING
15
15
 
16
16
  from .summarizer_client import SummarizerClient
17
- from .settings import get_session_dir
18
17
 
19
18
  if TYPE_CHECKING:
20
19
  from .interfaces import TmuxInterface
@@ -26,17 +25,23 @@ logger = logging.getLogger(__name__)
26
25
  class AgentSummary:
27
26
  """Summary for a single agent."""
28
27
 
28
+ # Short summary - current activity (~50 chars)
29
29
  text: str = ""
30
30
  updated_at: Optional[str] = None # ISO timestamp
31
31
  tokens_used: int = 0
32
32
 
33
+ # Context summary - wider context (~80 chars)
34
+ context: str = ""
35
+ context_updated_at: Optional[str] = None # ISO timestamp
36
+
33
37
 
34
38
  @dataclass
35
39
  class SummarizerConfig:
36
40
  """Configuration for the summarizer."""
37
41
 
38
42
  enabled: bool = False # Off by default
39
- interval: float = 5.0 # Seconds between updates per agent
43
+ interval: float = 5.0 # Seconds between short summary updates per agent
44
+ context_interval: float = 15.0 # Seconds between context summary updates (less frequent)
40
45
  lines: int = 200 # Pane lines to capture
41
46
  max_tokens: int = 150 # Max response tokens
42
47
 
@@ -78,8 +83,9 @@ class SummarizerComponent:
78
83
  # Per-agent summaries
79
84
  self.summaries: Dict[str, AgentSummary] = {}
80
85
 
81
- # Rate limiting per session
86
+ # Rate limiting per session (separate for short and context)
82
87
  self._last_update: Dict[str, datetime] = {}
88
+ self._last_context_update: Dict[str, datetime] = {}
83
89
 
84
90
  # Content hashes for change detection (avoid API calls when nothing changed)
85
91
  self._last_content_hash: Dict[str, int] = {}
@@ -88,9 +94,6 @@ class SummarizerComponent:
88
94
  self.total_calls = 0
89
95
  self.total_tokens = 0
90
96
 
91
- # Control file path
92
- self._control_file = get_session_dir(tmux_session) / "summarizer_control.json"
93
-
94
97
  @property
95
98
  def available(self) -> bool:
96
99
  """Check if summarizer is available (API key present)."""
@@ -101,36 +104,6 @@ class SummarizerComponent:
101
104
  """Check if summarizer is currently enabled."""
102
105
  return self.config.enabled and self._client is not None
103
106
 
104
- def check_control_file(self) -> None:
105
- """Check control file for enable/disable commands."""
106
- if not self._control_file.exists():
107
- return
108
-
109
- try:
110
- with open(self._control_file) as f:
111
- data = json.load(f)
112
-
113
- should_enable = data.get("enabled", False)
114
-
115
- if should_enable and not self.config.enabled:
116
- # Enable requested
117
- if SummarizerClient.is_available():
118
- self.config.enabled = True
119
- self._client = SummarizerClient()
120
- logger.info("Summarizer enabled via control file")
121
- else:
122
- logger.warning("Summarizer enable requested but OPENAI_API_KEY not set")
123
- elif not should_enable and self.config.enabled:
124
- # Disable requested
125
- self.config.enabled = False
126
- if self._client:
127
- self._client.close()
128
- self._client = None
129
- logger.info("Summarizer disabled via control file")
130
-
131
- except (json.JSONDecodeError, OSError) as e:
132
- logger.warning(f"Error reading summarizer control file: {e}")
133
-
134
107
  def update(self, sessions) -> Dict[str, AgentSummary]:
135
108
  """Update summaries for all sessions.
136
109
 
@@ -140,9 +113,6 @@ class SummarizerComponent:
140
113
  Returns:
141
114
  Dict mapping session_id to AgentSummary
142
115
  """
143
- # Check control file for enable/disable
144
- self.check_control_file()
145
-
146
116
  if not self.enabled:
147
117
  return self.summaries
148
118
 
@@ -152,7 +122,11 @@ class SummarizerComponent:
152
122
  return self.summaries
153
123
 
154
124
  def _update_session(self, session) -> None:
155
- """Update summary for a single session.
125
+ """Update summaries for a single session.
126
+
127
+ Generates two types of summaries:
128
+ - Short: current activity (updated frequently)
129
+ - Context: wider context (updated less frequently)
156
130
 
157
131
  Args:
158
132
  session: Session object with id, tmux_window, current_status
@@ -163,12 +137,18 @@ class SummarizerComponent:
163
137
  session_id = session.id
164
138
  now = datetime.now()
165
139
 
166
- # Rate limiting - don't call API too frequently for same session
167
- last_update = self._last_update.get(session_id)
168
- if last_update:
169
- elapsed = (now - last_update).total_seconds()
170
- if elapsed < self.config.interval:
171
- return
140
+ # Check rate limits for each summary type
141
+ last_short = self._last_update.get(session_id)
142
+ last_context = self._last_context_update.get(session_id)
143
+
144
+ short_elapsed = (now - last_short).total_seconds() if last_short else float('inf')
145
+ context_elapsed = (now - last_context).total_seconds() if last_context else float('inf')
146
+
147
+ need_short = short_elapsed >= self.config.interval
148
+ need_context = context_elapsed >= self.config.context_interval
149
+
150
+ if not need_short and not need_context:
151
+ return
172
152
 
173
153
  # Skip terminated sessions
174
154
  current_status = getattr(session, 'stats', None)
@@ -184,47 +164,88 @@ class SummarizerComponent:
184
164
 
185
165
  # Check if content has actually changed (avoid unnecessary API calls)
186
166
  content_hash = hash(content)
167
+ content_changed = True
187
168
  if session_id in self._last_content_hash:
188
169
  if self._last_content_hash[session_id] == content_hash:
189
- # Content hasn't changed - skip API call
190
- return
170
+ content_changed = False
171
+
172
+ # If content hasn't changed, skip short summary but still allow context
173
+ # (context changes less often so we're more lenient)
174
+ if not content_changed and not need_context:
175
+ return
191
176
 
192
177
  self._last_content_hash[session_id] = content_hash
193
178
 
194
- # Get previous summary for anti-oscillation
179
+ # Get or create summary object
195
180
  prev_summary = self.summaries.get(session_id)
196
- prev_text = prev_summary.text if prev_summary else ""
181
+ if not prev_summary:
182
+ prev_summary = AgentSummary()
183
+ self.summaries[session_id] = prev_summary
197
184
 
198
185
  # Get current detected status
199
186
  status = "unknown"
200
187
  if current_status:
201
188
  status = getattr(current_status, 'current_state', 'unknown')
202
189
 
203
- # Call API
190
+ # Update short summary if needed and content changed
191
+ if need_short and content_changed:
192
+ self._update_short_summary(session, prev_summary, content, status, now)
193
+
194
+ # Update context summary if needed (less frequent, runs even if content same)
195
+ if need_context:
196
+ self._update_context_summary(session, prev_summary, content, status, now)
197
+
198
+ def _update_short_summary(
199
+ self, session, summary: AgentSummary, content: str, status: str, now: datetime
200
+ ) -> None:
201
+ """Update the short (current activity) summary."""
204
202
  try:
205
203
  result = self._client.summarize(
206
204
  pane_content=content,
207
- previous_summary=prev_text,
205
+ previous_summary=summary.text,
208
206
  current_status=status,
209
207
  lines=self.config.lines,
210
- max_tokens=self.config.max_tokens,
208
+ max_tokens=50, # Aggressive limit for terse output
209
+ mode="short",
211
210
  )
212
211
 
213
212
  self.total_calls += 1
214
213
 
215
214
  if result and result.strip().upper() != "UNCHANGED":
216
- # New summary
217
- self.summaries[session_id] = AgentSummary(
218
- text=result.strip(),
219
- updated_at=now.isoformat(),
220
- )
221
- logger.debug(f"Updated summary for {session.name}: {result[:50]}...")
222
- # If "UNCHANGED", keep the old summary
215
+ summary.text = result.strip()
216
+ summary.updated_at = now.isoformat()
217
+ logger.debug(f"Updated short summary for {session.name}: {result[:50]}...")
223
218
 
224
- self._last_update[session_id] = now
219
+ self._last_update[session.id] = now
225
220
 
226
221
  except Exception as e:
227
- logger.warning(f"Summarizer error for {session.name}: {e}")
222
+ logger.warning(f"Short summary error for {session.name}: {e}")
223
+
224
+ def _update_context_summary(
225
+ self, session, summary: AgentSummary, content: str, status: str, now: datetime
226
+ ) -> None:
227
+ """Update the context (wider context) summary."""
228
+ try:
229
+ result = self._client.summarize(
230
+ pane_content=content,
231
+ previous_summary=summary.context,
232
+ current_status=status,
233
+ lines=self.config.lines,
234
+ max_tokens=75, # Aggressive limit for terse output
235
+ mode="context",
236
+ )
237
+
238
+ self.total_calls += 1
239
+
240
+ if result and result.strip().upper() != "UNCHANGED":
241
+ summary.context = result.strip()
242
+ summary.context_updated_at = now.isoformat()
243
+ logger.debug(f"Updated context summary for {session.name}: {result[:50]}...")
244
+
245
+ self._last_context_update[session.id] = now
246
+
247
+ except Exception as e:
248
+ logger.warning(f"Context summary error for {session.name}: {e}")
228
249
 
229
250
  def _capture_pane(self, window: int) -> Optional[str]:
230
251
  """Capture pane content for summarization.
@@ -269,44 +290,3 @@ class SummarizerComponent:
269
290
  if self._client:
270
291
  self._client.close()
271
292
  self._client = None
272
-
273
-
274
- def get_summarizer_control_path(session: str) -> Path:
275
- """Get the summarizer control file path for a session."""
276
- return get_session_dir(session) / "summarizer_control.json"
277
-
278
-
279
- def set_summarizer_enabled(session: str, enabled: bool) -> None:
280
- """Set summarizer enabled state via control file.
281
-
282
- The daemon reads this file each loop and adjusts accordingly.
283
-
284
- Args:
285
- session: tmux session name
286
- enabled: Whether to enable or disable
287
- """
288
- control_path = get_summarizer_control_path(session)
289
- control_path.parent.mkdir(parents=True, exist_ok=True)
290
- with open(control_path, 'w') as f:
291
- json.dump({"enabled": enabled}, f)
292
-
293
-
294
- def is_summarizer_enabled(session: str) -> bool:
295
- """Check if summarizer is enabled for a session.
296
-
297
- Args:
298
- session: tmux session name
299
-
300
- Returns:
301
- True if enabled, False otherwise
302
- """
303
- control_path = get_summarizer_control_path(session)
304
- if not control_path.exists():
305
- return False
306
-
307
- try:
308
- with open(control_path) as f:
309
- data = json.load(f)
310
- return data.get("enabled", False)
311
- except (json.JSONDecodeError, OSError):
312
- return False
@@ -16,7 +16,7 @@ Prerequisites:
16
16
  Architecture:
17
17
  Monitor Daemon (metrics) → monitor_daemon_state.json → Supervisor Daemon (claude)
18
18
 
19
- TODO: Add unit tests (currently 0% coverage)
19
+ Pure business logic is extracted to supervisor_daemon_core.py for testability.
20
20
  TODO: Extract _send_prompt_to_window to a shared tmux utilities module
21
21
  (duplicated in launcher.py)
22
22
  """
@@ -60,6 +60,13 @@ from .status_constants import (
60
60
  )
61
61
  from .tmux_manager import TmuxManager
62
62
  from .history_reader import encode_project_path, read_token_usage_from_session_file
63
+ from .supervisor_daemon_core import (
64
+ build_daemon_claude_context as _build_daemon_claude_context,
65
+ filter_non_green_sessions,
66
+ calculate_daemon_claude_run_seconds,
67
+ should_launch_daemon_claude,
68
+ parse_intervention_log_line,
69
+ )
63
70
 
64
71
 
65
72
  @dataclass
@@ -473,14 +480,14 @@ class SupervisorDaemon:
473
480
  def _mark_daemon_claude_stopped(self) -> None:
474
481
  """Mark daemon claude as stopped and accumulate run time."""
475
482
  if self.supervisor_stats.supervisor_claude_running:
476
- # Calculate run duration
477
- if self.supervisor_stats.supervisor_claude_started_at:
478
- try:
479
- started_at = datetime.fromisoformat(self.supervisor_stats.supervisor_claude_started_at)
480
- run_seconds = (datetime.now() - started_at).total_seconds()
481
- self.supervisor_stats.supervisor_claude_total_run_seconds += run_seconds
482
- except (ValueError, TypeError):
483
- pass
483
+ # Calculate total run time using pure function
484
+ self.supervisor_stats.supervisor_claude_total_run_seconds = (
485
+ calculate_daemon_claude_run_seconds(
486
+ started_at_iso=self.supervisor_stats.supervisor_claude_started_at,
487
+ now_iso=datetime.now().isoformat(),
488
+ previous_total=self.supervisor_stats.supervisor_claude_total_run_seconds,
489
+ )
490
+ )
484
491
 
485
492
  self.supervisor_stats.supervisor_claude_running = False
486
493
  self.supervisor_stats.supervisor_claude_started_at = None
@@ -555,31 +562,18 @@ class SupervisorDaemon:
555
562
  non_green_sessions: List[SessionDaemonState]
556
563
  ) -> str:
557
564
  """Build initial context for daemon claude."""
558
- context_parts = []
559
-
560
- context_parts.append("You are the Overcode daemon claude agent.")
561
- context_parts.append("Your mission: Make all RED/YELLOW/ORANGE sessions GREEN.")
562
- context_parts.append("")
563
- context_parts.append(f"TMUX SESSION: {self.tmux_session}")
564
- context_parts.append(f"Sessions needing attention: {len(non_green_sessions)}")
565
- context_parts.append("")
566
-
567
- for session in non_green_sessions:
568
- emoji = get_status_emoji(session.current_status)
569
- context_parts.append(f"{emoji} {session.name} (window {session.tmux_window})")
570
- if session.standing_instructions:
571
- context_parts.append(f" Autopilot: {session.standing_instructions}")
572
- else:
573
- context_parts.append(f" No autopilot instructions set")
574
- if session.repo_name:
575
- context_parts.append(f" Repo: {session.repo_name}")
576
- context_parts.append("")
577
-
578
- context_parts.append("Read the daemon claude skill for how to control sessions via tmux.")
579
- context_parts.append("Start by reading ~/.overcode/sessions/sessions.json to see full state.")
580
- context_parts.append("Then check each non-green session and help them make progress.")
581
-
582
- return "\n".join(context_parts)
565
+ # Convert dataclass objects to dicts for pure function
566
+ session_dicts = [
567
+ {
568
+ "name": s.name,
569
+ "tmux_window": s.tmux_window,
570
+ "current_status": s.current_status,
571
+ "standing_instructions": s.standing_instructions,
572
+ "repo_name": s.repo_name,
573
+ }
574
+ for s in non_green_sessions
575
+ ]
576
+ return _build_daemon_claude_context(self.tmux_session, session_dicts)
583
577
 
584
578
  def _send_prompt_to_window(self, window_index: int, prompt: str) -> bool:
585
579
  """Send a large prompt to a tmux window via load-buffer/paste-buffer."""
@@ -678,12 +672,35 @@ class SupervisorDaemon:
678
672
  self,
679
673
  monitor_state: MonitorDaemonState
680
674
  ) -> List[SessionDaemonState]:
681
- """Get sessions that are not in running state from monitor daemon state."""
682
- return [
683
- s for s in monitor_state.sessions
684
- if s.current_status != STATUS_RUNNING and s.name != 'daemon_claude'
675
+ """Get sessions that are not in running state from monitor daemon state.
676
+
677
+ Filters out:
678
+ - Running (green) sessions
679
+ - The daemon_claude session itself
680
+ - Asleep sessions (#70)
681
+ - Sessions with DO_NOTHING standing orders (#70)
682
+ """
683
+ # Convert to dicts for pure function
684
+ session_dicts = [
685
+ {
686
+ "name": s.name,
687
+ "current_status": s.current_status,
688
+ "is_asleep": s.is_asleep,
689
+ "standing_instructions": s.standing_instructions,
690
+ "_session": s, # Keep reference to original
691
+ }
692
+ for s in monitor_state.sessions
685
693
  ]
686
694
 
695
+ # Filter using pure function
696
+ filtered = filter_non_green_sessions(
697
+ session_dicts,
698
+ exclude_names=["daemon_claude"],
699
+ )
700
+
701
+ # Return original SessionDaemonState objects
702
+ return [d["_session"] for d in filtered]
703
+
687
704
  def wait_for_monitor_daemon(self, timeout: int = 30, poll_interval: int = 2) -> bool:
688
705
  """Wait for monitor daemon to be running.
689
706