overcode 0.3.3__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {overcode-0.3.3/src/overcode.egg-info → overcode-0.3.4}/PKG-INFO +1 -1
- {overcode-0.3.3 → overcode-0.3.4}/pyproject.toml +1 -1
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/bundled_skills.py +26 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/config.py +2 -2
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/monitoring.py +1 -1
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/split.py +50 -8
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/config.py +28 -7
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/dependency_check.py +33 -1
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/hook_handler.py +4 -4
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/hook_status_detector.py +18 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/monitor_daemon.py +26 -4
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/monitor_daemon_state.py +1 -1
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/session_manager.py +14 -2
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/settings.py +17 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/sister_controller.py +22 -4
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/sister_poller.py +26 -5
- overcode-0.3.4/src/overcode/ssh_provisioner.py +234 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/status_detector_factory.py +6 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/summary_columns.py +74 -7
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/summary_groups.py +1 -1
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/time_context.py +21 -16
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tmux_manager.py +83 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui.py +268 -15
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui.tcss +11 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_actions/daemon.py +9 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_actions/navigation.py +3 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_actions/session.py +13 -13
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_actions/view.py +28 -16
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/__init__.py +2 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/command_bar.py +17 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/help_overlay.py +37 -22
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/session_summary.py +7 -2
- overcode-0.3.4/src/overcode/tui_widgets/tui_log_panel.py +145 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/web_api.py +13 -1
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/web_control_api.py +42 -11
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/web_server.py +4 -1
- {overcode-0.3.3 → overcode-0.3.4/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode.egg-info/SOURCES.txt +2 -0
- {overcode-0.3.3 → overcode-0.3.4}/LICENSE +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/MANIFEST.in +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/README.md +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/setup.cfg +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/agent_scanner.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/claude_config.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/claude_pid.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/_shared.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/agent.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/budget.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/daemon.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/jobs.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/perms.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/sister.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/cli/skills.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/data_export.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/duration.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/exceptions.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/follow_mode.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/history_reader.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/implementations.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/interfaces.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/job_launcher.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/job_manager.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/launcher.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/logging_config.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/mocks.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/notifier.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/pid_utils.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/presence_logger.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/protocols.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/status_constants.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/status_detector.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/status_history.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/status_patterns.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/supervisor_daemon.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/supervisor_daemon_core.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_actions/input.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_logic.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_render.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/job_summary.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/status_timeline.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/web/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode/web_templates.py +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.3.3 → overcode-0.3.4}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -248,6 +248,32 @@ Jobs are visible in the TUI jobs view (press `J`) and auto-clean after 24h (conf
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
|
|
251
|
+
def get_available_skills(project_dir: str | None = None) -> list[str]:
|
|
252
|
+
"""Scan for installed skill directories (user-level + project-level).
|
|
253
|
+
|
|
254
|
+
Returns sorted list of skill names found in ~/.claude/skills/
|
|
255
|
+
and optionally .claude/skills/ relative to project_dir.
|
|
256
|
+
"""
|
|
257
|
+
skills: set[str] = set()
|
|
258
|
+
|
|
259
|
+
# User-level skills
|
|
260
|
+
user_skills = Path.home() / ".claude" / "skills"
|
|
261
|
+
if user_skills.is_dir():
|
|
262
|
+
for d in user_skills.iterdir():
|
|
263
|
+
if d.is_dir() and (d / "SKILL.md").exists():
|
|
264
|
+
skills.add(d.name)
|
|
265
|
+
|
|
266
|
+
# Project-level skills
|
|
267
|
+
if project_dir:
|
|
268
|
+
proj_skills = Path(project_dir) / ".claude" / "skills"
|
|
269
|
+
if proj_skills.is_dir():
|
|
270
|
+
for d in proj_skills.iterdir():
|
|
271
|
+
if d.is_dir() and (d / "SKILL.md").exists():
|
|
272
|
+
skills.add(d.name)
|
|
273
|
+
|
|
274
|
+
return sorted(skills)
|
|
275
|
+
|
|
276
|
+
|
|
251
277
|
def any_skills_stale() -> bool:
|
|
252
278
|
"""Check if any installed skills are outdated vs bundled versions."""
|
|
253
279
|
base = Path.home() / ".claude" / "skills"
|
|
@@ -44,8 +44,8 @@ CONFIG_TEMPLATE = """\
|
|
|
44
44
|
# start: "09:00"
|
|
45
45
|
# end: "17:00"
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
47
|
+
# Enhanced context hook settings (agent identity, clock, presence, uptime)
|
|
48
|
+
# enhanced_context:
|
|
49
49
|
# office_start: 9
|
|
50
50
|
# office_end: 17
|
|
51
51
|
# heartbeat_interval_minutes: 15 # omit to disable
|
|
@@ -18,7 +18,7 @@ def hook_handler_cmd():
|
|
|
18
18
|
|
|
19
19
|
Called by Claude Code hooks, not by users directly.
|
|
20
20
|
Reads event JSON from stdin, writes state for status detection,
|
|
21
|
-
and outputs
|
|
21
|
+
and outputs enhanced context for UserPromptSubmit events.
|
|
22
22
|
"""
|
|
23
23
|
from ..hook_handler import handle_hook_event
|
|
24
24
|
|
|
@@ -263,20 +263,51 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
|
|
|
263
263
|
# --- Scrollback for the nested tmux in the bottom pane ---
|
|
264
264
|
# The bottom pane runs a nested tmux client. The outer tmux
|
|
265
265
|
# intercepts the prefix key, so copy-mode can't be entered
|
|
266
|
-
# normally.
|
|
267
|
-
#
|
|
266
|
+
# normally.
|
|
267
|
+
#
|
|
268
|
+
# For local agents: use `copy-mode -t` to directly enter copy mode
|
|
269
|
+
# in the inner session via the tmux API (the linked session shares
|
|
270
|
+
# the actual agent panes, so scrollback is real).
|
|
271
|
+
#
|
|
272
|
+
# For SSH proxy windows (name starts with "ssh:"): the linked
|
|
273
|
+
# session pane is an SSH+tmux-attach client whose local scrollback
|
|
274
|
+
# is just rendering frames. Instead, forward PageUp/scroll through
|
|
275
|
+
# to the remote tmux which has the actual agent scrollback.
|
|
268
276
|
if linked_session:
|
|
269
277
|
# PageUp: enter copy mode + scroll up, but only when in the
|
|
270
278
|
# bottom pane (pane_index != base) of the split window.
|
|
271
279
|
# Top pane (Textual TUI) and other windows get normal PageUp.
|
|
280
|
+
#
|
|
281
|
+
# For SSH proxy windows: forward PageUp through the SSH pane
|
|
282
|
+
# so the *remote* tmux enters copy mode (the local proxy pane
|
|
283
|
+
# has no real scrollback — just rendered inner-tmux frames).
|
|
284
|
+
# For local agents: enter copy mode directly on the linked
|
|
285
|
+
# session which shares the real agent pane.
|
|
286
|
+
#
|
|
287
|
+
# Detection uses the @is_ssh_proxy window option (set by
|
|
288
|
+
# create_ssh_proxy_window) via a shell-based if-shell — this
|
|
289
|
+
# avoids tmux format expansion issues in stored bindings.
|
|
290
|
+
_ssh_check = (
|
|
291
|
+
f"tmux show-window-option -t {linked_session} -v @is_ssh_proxy "
|
|
292
|
+
"2>/dev/null | grep -q on"
|
|
293
|
+
)
|
|
272
294
|
_tmux(
|
|
273
295
|
"bind-key", "-n", "PPage",
|
|
274
296
|
"if-shell", "-F",
|
|
275
297
|
f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},{_base}}}}}",
|
|
276
|
-
|
|
298
|
+
# SSH proxy: send prefix + PPage so the REMOTE tmux enters
|
|
299
|
+
# copy mode (the remote's root-table PPage is overridden by
|
|
300
|
+
# overcode, but prefix PPage still maps to copy-mode -u).
|
|
301
|
+
# Local: enter copy mode directly on the linked session.
|
|
302
|
+
f'if-shell "{_ssh_check}" '
|
|
303
|
+
f'"send-keys -t {linked_session} C-b ; '
|
|
304
|
+
f'send-keys -t {linked_session} PPage" '
|
|
305
|
+
f'"copy-mode -t {linked_session} -u"',
|
|
277
306
|
"send-keys PPage",
|
|
278
307
|
)
|
|
279
308
|
# PageDown in the inner session's copy mode
|
|
309
|
+
# (send-keys works for both local and SSH proxy — for local it
|
|
310
|
+
# controls copy mode, for SSH proxy it forwards to remote tmux)
|
|
280
311
|
_tmux(
|
|
281
312
|
"bind-key", "-n", "NPage",
|
|
282
313
|
"if-shell", "-F",
|
|
@@ -288,8 +319,9 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
|
|
|
288
319
|
# Without this, scrolling enters copy mode in the outer pane
|
|
289
320
|
# which has no scrollback (just rendered inner tmux frames).
|
|
290
321
|
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
322
|
+
# For local agents: enter copy mode in the inner session then
|
|
323
|
+
# send scroll commands. For SSH proxies: forward mouse events
|
|
324
|
+
# to the remote tmux through the pane.
|
|
293
325
|
_in_bottom = (
|
|
294
326
|
f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},"
|
|
295
327
|
f"#{{!=:#{{pane_index}},{_base}}}}}"
|
|
@@ -297,8 +329,14 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
|
|
|
297
329
|
_tmux(
|
|
298
330
|
"bind-key", "-n", "WheelUpPane",
|
|
299
331
|
"if-shell", "-F", _in_bottom,
|
|
300
|
-
|
|
301
|
-
|
|
332
|
+
# SSH proxy: send prefix+PPage to enter copy mode on remote
|
|
333
|
+
# (no-op if already in copy mode, then PPage scrolls up).
|
|
334
|
+
# Local: enter copy mode + scroll up directly.
|
|
335
|
+
f'if-shell "{_ssh_check}" '
|
|
336
|
+
f'"send-keys -t {linked_session} C-b ; '
|
|
337
|
+
f'send-keys -t {linked_session} PPage" '
|
|
338
|
+
f'"copy-mode -t {linked_session} -e ; '
|
|
339
|
+
f'send-keys -t {linked_session} -X -N 3 scroll-up"',
|
|
302
340
|
# Default behaviour for other contexts
|
|
303
341
|
"if-shell -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' "
|
|
304
342
|
"'send-keys -M' 'copy-mode -e'",
|
|
@@ -306,7 +344,11 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
|
|
|
306
344
|
_tmux(
|
|
307
345
|
"bind-key", "-n", "WheelDownPane",
|
|
308
346
|
"if-shell", "-F", _in_bottom,
|
|
309
|
-
|
|
347
|
+
# SSH proxy: send NPage to remote (works in copy mode).
|
|
348
|
+
# Local: send scroll-down in local copy mode.
|
|
349
|
+
f'if-shell "{_ssh_check}" '
|
|
350
|
+
f'"send-keys -t {linked_session} NPage" '
|
|
351
|
+
f'"send-keys -t {linked_session} -X -N 3 scroll-down"',
|
|
310
352
|
"send-keys -M",
|
|
311
353
|
)
|
|
312
354
|
|
|
@@ -197,23 +197,35 @@ def get_web_time_presets() -> list:
|
|
|
197
197
|
]
|
|
198
198
|
|
|
199
199
|
|
|
200
|
-
def
|
|
201
|
-
"""Get
|
|
200
|
+
def get_enhanced_context_config() -> dict:
|
|
201
|
+
"""Get enhanced context configuration for the enhanced-context hook.
|
|
202
202
|
|
|
203
203
|
Config format in ~/.overcode/config.yaml:
|
|
204
|
-
|
|
204
|
+
enhanced_context:
|
|
205
205
|
office_start: 9
|
|
206
206
|
office_end: 17
|
|
207
207
|
heartbeat_interval_minutes: 15 # omit to disable
|
|
208
208
|
|
|
209
|
+
Falls back to legacy 'time_context' key for backwards compatibility.
|
|
210
|
+
|
|
209
211
|
Returns:
|
|
210
212
|
Dict with office_start (int), office_end (int),
|
|
211
213
|
heartbeat_interval_minutes (Optional[int])
|
|
212
214
|
"""
|
|
215
|
+
# Try new key first, fall back to legacy key
|
|
216
|
+
office_start = _get_config_value("enhanced_context.office_start")
|
|
217
|
+
if office_start is None:
|
|
218
|
+
office_start = _get_config_value("time_context.office_start", 9)
|
|
219
|
+
office_end = _get_config_value("enhanced_context.office_end")
|
|
220
|
+
if office_end is None:
|
|
221
|
+
office_end = _get_config_value("time_context.office_end", 17)
|
|
222
|
+
hb_interval = _get_config_value("enhanced_context.heartbeat_interval_minutes")
|
|
223
|
+
if hb_interval is None:
|
|
224
|
+
hb_interval = _get_config_value("time_context.heartbeat_interval_minutes")
|
|
213
225
|
return {
|
|
214
|
-
"office_start":
|
|
215
|
-
"office_end":
|
|
216
|
-
"heartbeat_interval_minutes":
|
|
226
|
+
"office_start": office_start,
|
|
227
|
+
"office_end": office_end,
|
|
228
|
+
"heartbeat_interval_minutes": hb_interval,
|
|
217
229
|
}
|
|
218
230
|
|
|
219
231
|
|
|
@@ -365,9 +377,11 @@ def get_sisters_config() -> List[dict]:
|
|
|
365
377
|
- name: "desktop"
|
|
366
378
|
url: "http://localhost:25337"
|
|
367
379
|
api_key: "secret"
|
|
380
|
+
ssh: "user@desktop" # optional: enables tmux attach + auto-provisioning
|
|
381
|
+
tmux_session: "agents" # optional: remote tmux session name (default: "agents")
|
|
368
382
|
|
|
369
383
|
Returns:
|
|
370
|
-
List of dicts with name, url, and optional api_key. Empty list if unconfigured.
|
|
384
|
+
List of dicts with name, url, and optional api_key/ssh/tmux_session. Empty list if unconfigured.
|
|
371
385
|
"""
|
|
372
386
|
sisters = _get_config_value("sisters", [])
|
|
373
387
|
if not isinstance(sisters, list):
|
|
@@ -385,6 +399,13 @@ def get_sisters_config() -> List[dict]:
|
|
|
385
399
|
api_key = s.get("api_key")
|
|
386
400
|
if api_key:
|
|
387
401
|
entry["api_key"] = api_key
|
|
402
|
+
# SSH connectivity (optional)
|
|
403
|
+
ssh = s.get("ssh")
|
|
404
|
+
if ssh:
|
|
405
|
+
entry["ssh"] = ssh
|
|
406
|
+
tmux_session = s.get("tmux_session")
|
|
407
|
+
if tmux_session:
|
|
408
|
+
entry["tmux_session"] = tmux_session
|
|
388
409
|
result.append(entry)
|
|
389
410
|
|
|
390
411
|
return result
|
|
@@ -15,13 +15,45 @@ from .exceptions import TmuxNotFoundError, ClaudeNotFoundError
|
|
|
15
15
|
def find_executable(name: str) -> Optional[str]:
|
|
16
16
|
"""Find the path to an executable.
|
|
17
17
|
|
|
18
|
+
Checks PATH first, then common install locations that may not be on PATH
|
|
19
|
+
in non-login shells (e.g., web server subprocesses, SSH non-interactive).
|
|
20
|
+
|
|
18
21
|
Args:
|
|
19
22
|
name: Name of the executable
|
|
20
23
|
|
|
21
24
|
Returns:
|
|
22
25
|
Full path to executable, or None if not found
|
|
23
26
|
"""
|
|
24
|
-
|
|
27
|
+
import os
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
path = shutil.which(name)
|
|
31
|
+
if path:
|
|
32
|
+
return path
|
|
33
|
+
|
|
34
|
+
# Check common locations not always on PATH in non-login shells
|
|
35
|
+
return _find_in_fallback_dirs(name)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _find_in_fallback_dirs(name: str) -> Optional[str]:
|
|
39
|
+
"""Check common install directories for an executable."""
|
|
40
|
+
import os
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
home = Path.home()
|
|
44
|
+
fallback_dirs = [
|
|
45
|
+
home / ".local" / "bin", # pip/pipx, claude CLI
|
|
46
|
+
home / ".npm-global" / "bin", # npm global
|
|
47
|
+
home / ".nvm" / "current" / "bin", # nvm
|
|
48
|
+
Path("/usr/local/bin"),
|
|
49
|
+
Path("/opt/homebrew/bin"), # macOS ARM homebrew
|
|
50
|
+
]
|
|
51
|
+
for d in fallback_dirs:
|
|
52
|
+
candidate = d / name
|
|
53
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
54
|
+
return str(candidate)
|
|
55
|
+
|
|
56
|
+
return None
|
|
25
57
|
|
|
26
58
|
|
|
27
59
|
def _check_executable(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A single command (`overcode hook-handler`) handles all hook events.
|
|
4
4
|
It reads stdin JSON from Claude Code, writes state files for hook-based
|
|
5
|
-
status detection, and outputs
|
|
5
|
+
status detection, and outputs enhanced context for UserPromptSubmit events.
|
|
6
6
|
|
|
7
7
|
Hook registrations (all use the same command):
|
|
8
8
|
UserPromptSubmit -> overcode hook-handler
|
|
@@ -110,7 +110,7 @@ def handle_hook_event() -> None:
|
|
|
110
110
|
# Write state file for status detection
|
|
111
111
|
write_hook_state(event, tmux_session, session_name, tool_name=tool_name, tool_input=tool_input)
|
|
112
112
|
|
|
113
|
-
# For UserPromptSubmit, check budget and output
|
|
113
|
+
# For UserPromptSubmit, check budget and output enhanced context
|
|
114
114
|
if event == "UserPromptSubmit":
|
|
115
115
|
from .time_context import _load_daemon_state, _find_session_in_state
|
|
116
116
|
|
|
@@ -127,8 +127,8 @@ def handle_hook_event() -> None:
|
|
|
127
127
|
)
|
|
128
128
|
sys.exit(2)
|
|
129
129
|
|
|
130
|
-
from .time_context import
|
|
130
|
+
from .time_context import generate_enhanced_context
|
|
131
131
|
|
|
132
|
-
line =
|
|
132
|
+
line = generate_enhanced_context(tmux_session, session_name)
|
|
133
133
|
if line:
|
|
134
134
|
print(line)
|
|
@@ -95,6 +95,8 @@ class HookStatusDetector:
|
|
|
95
95
|
# Diagnostic phase tracking (same interface as PollingStatusDetector)
|
|
96
96
|
self._last_detect_phase: Dict[str, str] = {}
|
|
97
97
|
self._content_changed: Dict[str, bool] = {}
|
|
98
|
+
# Skills observed via Skill tool_use events, keyed by session name (#252)
|
|
99
|
+
self._loaded_skills: Dict[str, set] = {}
|
|
98
100
|
|
|
99
101
|
# Resolve state directory — must match hook_handler._get_hook_state_path()
|
|
100
102
|
if state_dir is not None:
|
|
@@ -180,6 +182,17 @@ class HookStatusDetector:
|
|
|
180
182
|
self._last_detect_phase[session.id] = "hook:no_state"
|
|
181
183
|
return STATUS_WAITING_USER, "Waiting for first hook event", pane_content
|
|
182
184
|
|
|
185
|
+
# Track Skill tool_use events (#252)
|
|
186
|
+
tool_name = hook_state.get("tool_name")
|
|
187
|
+
if tool_name == "Skill":
|
|
188
|
+
tool_input = hook_state.get("tool_input")
|
|
189
|
+
if isinstance(tool_input, dict):
|
|
190
|
+
skill_name = tool_input.get("skill", "")
|
|
191
|
+
if skill_name:
|
|
192
|
+
if session.name not in self._loaded_skills:
|
|
193
|
+
self._loaded_skills[session.name] = set()
|
|
194
|
+
self._loaded_skills[session.name].add(skill_name)
|
|
195
|
+
|
|
183
196
|
# Hook state exists — use it for status
|
|
184
197
|
event = hook_state.get("event", "")
|
|
185
198
|
|
|
@@ -308,3 +321,8 @@ class HookStatusDetector:
|
|
|
308
321
|
return "Claude exited"
|
|
309
322
|
|
|
310
323
|
return "Unknown state"
|
|
324
|
+
|
|
325
|
+
def get_loaded_skills(self, session_name: str) -> list[str]:
|
|
326
|
+
"""Return skills observed via Skill tool_use for a session (#252)."""
|
|
327
|
+
skills = self._loaded_skills.get(session_name, set())
|
|
328
|
+
return sorted(skills)
|
|
@@ -312,6 +312,10 @@ class MonitorDaemon:
|
|
|
312
312
|
self._last_session_id_sync: Optional[datetime] = None
|
|
313
313
|
self._session_id_sync_interval = 10 # seconds
|
|
314
314
|
|
|
315
|
+
# Available skills sync (#252) — infrequent, skills rarely change
|
|
316
|
+
self._last_skills_sync: Optional[datetime] = None
|
|
317
|
+
self._skills_sync_interval = 60 # seconds
|
|
318
|
+
|
|
315
319
|
# Relay configuration (for pushing state to cloud)
|
|
316
320
|
self._relay_config = get_relay_config()
|
|
317
321
|
self._last_relay_push = datetime.min
|
|
@@ -452,7 +456,7 @@ class MonitorDaemon:
|
|
|
452
456
|
permissiveness_mode=session.permissiveness_mode,
|
|
453
457
|
start_directory=session.start_directory,
|
|
454
458
|
is_asleep=session.is_asleep,
|
|
455
|
-
|
|
459
|
+
enhanced_context_enabled=session.enhanced_context_enabled,
|
|
456
460
|
agent_value=session.agent_value,
|
|
457
461
|
# Heartbeat state (#171)
|
|
458
462
|
heartbeat_enabled=session.heartbeat_enabled,
|
|
@@ -899,6 +903,7 @@ class MonitorDaemon:
|
|
|
899
903
|
self._legacy_windows_migrated = True
|
|
900
904
|
self._sync_session_ids(sessions, now)
|
|
901
905
|
self._sync_session_stats(sessions, now)
|
|
906
|
+
self._sync_available_skills(sessions, now)
|
|
902
907
|
self._dispatch_heartbeats(sessions)
|
|
903
908
|
session_states, all_waiting = self._detect_and_enrich(sessions, now)
|
|
904
909
|
self._cleanup_stale(sessions)
|
|
@@ -924,6 +929,16 @@ class MonitorDaemon:
|
|
|
924
929
|
self.sync_claude_code_stats(session)
|
|
925
930
|
self._last_stats_sync = now
|
|
926
931
|
|
|
932
|
+
def _sync_available_skills(self, sessions: list, now: datetime) -> None:
|
|
933
|
+
"""Scan installed skill directories every 60s (#252)."""
|
|
934
|
+
if should_sync_stats(self._last_skills_sync, now, self._skills_sync_interval):
|
|
935
|
+
from .bundled_skills import get_available_skills
|
|
936
|
+
for session in sessions:
|
|
937
|
+
available = get_available_skills(session.start_directory)
|
|
938
|
+
if available != session.available_skills:
|
|
939
|
+
self.session_manager.update_session(session.id, available_skills=available)
|
|
940
|
+
self._last_skills_sync = now
|
|
941
|
+
|
|
927
942
|
def _dispatch_heartbeats(self, sessions: list) -> None:
|
|
928
943
|
"""Send heartbeats before status detection (#171)."""
|
|
929
944
|
self._heartbeat_triggered_sessions = self.check_and_send_heartbeats(sessions)
|
|
@@ -952,6 +967,12 @@ class MonitorDaemon:
|
|
|
952
967
|
# Log hook events when they change (diagnostic visibility)
|
|
953
968
|
self._log_hook_event(session, status, activity)
|
|
954
969
|
|
|
970
|
+
# Track loaded skills from hook events (#252)
|
|
971
|
+
if hasattr(self.detector, 'get_loaded_skills'):
|
|
972
|
+
new_skills = self.detector.get_loaded_skills(session.name)
|
|
973
|
+
if new_skills and sorted(new_skills) != sorted(session.loaded_skills):
|
|
974
|
+
self.session_manager.update_session(session.id, loaded_skills=new_skills)
|
|
975
|
+
|
|
955
976
|
# Extract PR number from pane content
|
|
956
977
|
if pane_content:
|
|
957
978
|
pr = extract_pr_number(pane_content)
|
|
@@ -985,9 +1006,10 @@ class MonitorDaemon:
|
|
|
985
1006
|
continue
|
|
986
1007
|
|
|
987
1008
|
# Track stats and build state
|
|
988
|
-
#
|
|
989
|
-
|
|
990
|
-
|
|
1009
|
+
# Precedence: terminated > asleep > heartbeat variants > default (#399, #68, #171)
|
|
1010
|
+
if status == STATUS_TERMINATED:
|
|
1011
|
+
effective_status = STATUS_TERMINATED
|
|
1012
|
+
elif session.is_asleep:
|
|
991
1013
|
effective_status = STATUS_ASLEEP
|
|
992
1014
|
elif status == STATUS_RUNNING and session.id in self._sessions_running_from_heartbeat:
|
|
993
1015
|
if session.id in self._heartbeat_start_pending:
|
|
@@ -76,7 +76,7 @@ class SessionDaemonState:
|
|
|
76
76
|
permissiveness_mode: str = "normal" # normal, permissive, bypass
|
|
77
77
|
start_directory: Optional[str] = None # For git diff stats
|
|
78
78
|
is_asleep: bool = False # Agent is paused and excluded from stats (#70)
|
|
79
|
-
|
|
79
|
+
enhanced_context_enabled: bool = False # Per-agent enhanced context toggle
|
|
80
80
|
|
|
81
81
|
# Agent priority value (#61)
|
|
82
82
|
agent_value: int = 1000 # Default 1000, higher = more important
|
|
@@ -97,8 +97,8 @@ class Session:
|
|
|
97
97
|
# Sleep mode - agent is paused and excluded from stats
|
|
98
98
|
is_asleep: bool = False
|
|
99
99
|
|
|
100
|
-
#
|
|
101
|
-
|
|
100
|
+
# Enhanced context hook - per-agent toggle for enhanced context injection
|
|
101
|
+
enhanced_context_enabled: bool = False
|
|
102
102
|
|
|
103
103
|
# Agent value - priority indicator for sorting/attention (#61)
|
|
104
104
|
# Default 1000, higher = more important
|
|
@@ -129,6 +129,10 @@ class Session:
|
|
|
129
129
|
# Hook-based status detection - per-agent toggle (#5)
|
|
130
130
|
hook_status_detection: bool = True
|
|
131
131
|
|
|
132
|
+
# Skills loaded during this session (#252)
|
|
133
|
+
loaded_skills: List[str] = field(default_factory=list)
|
|
134
|
+
available_skills: List[str] = field(default_factory=list)
|
|
135
|
+
|
|
132
136
|
# Claude CLI flag passthrough (#290)
|
|
133
137
|
allowed_tools: Optional[str] = None # Comma-separated tool list for --allowedTools
|
|
134
138
|
extra_claude_args: List[str] = field(default_factory=list) # Extra CLI flags via --claude-arg
|
|
@@ -158,6 +162,10 @@ class Session:
|
|
|
158
162
|
remote_activity_summary_context: str = "" # AI context summary from remote summarizer
|
|
159
163
|
remote_daemon_state: Optional[dict] = None # Raw daemon state dict from sister API (for generic forwarding)
|
|
160
164
|
|
|
165
|
+
# SSH connectivity for remote agents
|
|
166
|
+
source_ssh: str = "" # SSH target (e.g., "user@host") for tmux attach
|
|
167
|
+
source_tmux_session: str = "" # Remote tmux session name (default: "agents")
|
|
168
|
+
|
|
161
169
|
def to_dict(self) -> dict:
|
|
162
170
|
# asdict() recursively converts nested dataclasses (stats)
|
|
163
171
|
return asdict(self)
|
|
@@ -193,6 +201,10 @@ class Session:
|
|
|
193
201
|
if 'tmux_window' in filtered and isinstance(filtered['tmux_window'], int):
|
|
194
202
|
filtered['tmux_window'] = str(filtered['tmux_window'])
|
|
195
203
|
|
|
204
|
+
# Backward compat: migrate time_context_enabled → enhanced_context_enabled (#378)
|
|
205
|
+
if 'enhanced_context_enabled' not in filtered and 'time_context_enabled' in data:
|
|
206
|
+
filtered['enhanced_context_enabled'] = data['time_context_enabled']
|
|
207
|
+
|
|
196
208
|
try:
|
|
197
209
|
return cls(**filtered)
|
|
198
210
|
except TypeError:
|
|
@@ -262,6 +262,9 @@ class UserConfig:
|
|
|
262
262
|
# Per-model pricing overrides (loaded from config.yaml model_pricing section)
|
|
263
263
|
model_pricing: dict = field(default_factory=dict)
|
|
264
264
|
|
|
265
|
+
# Skill emoji overrides (loaded from config.yaml skill_emoji section) (#252)
|
|
266
|
+
skill_emoji: dict = field(default_factory=dict)
|
|
267
|
+
|
|
265
268
|
@classmethod
|
|
266
269
|
def load(cls) -> "UserConfig":
|
|
267
270
|
"""Load configuration from config file."""
|
|
@@ -292,6 +295,13 @@ class UserConfig:
|
|
|
292
295
|
cache_read=vals.get("cache_read", 0.30),
|
|
293
296
|
)
|
|
294
297
|
|
|
298
|
+
# Parse skill emoji overrides (#252)
|
|
299
|
+
skill_emoji_raw = data.get("skill_emoji", {})
|
|
300
|
+
skill_emoji_parsed = (
|
|
301
|
+
{str(k): str(v) for k, v in skill_emoji_raw.items()}
|
|
302
|
+
if isinstance(skill_emoji_raw, dict) else {}
|
|
303
|
+
)
|
|
304
|
+
|
|
295
305
|
return cls(
|
|
296
306
|
default_standing_instructions=data.get(
|
|
297
307
|
"default_standing_instructions", ""
|
|
@@ -302,6 +312,7 @@ class UserConfig:
|
|
|
302
312
|
price_cache_write=pricing.get("cache_write", 3.75),
|
|
303
313
|
price_cache_read=pricing.get("cache_read", 0.30),
|
|
304
314
|
model_pricing=model_pricing_parsed,
|
|
315
|
+
skill_emoji=skill_emoji_parsed,
|
|
305
316
|
)
|
|
306
317
|
except (yaml.YAMLError, IOError):
|
|
307
318
|
return cls()
|
|
@@ -439,6 +450,11 @@ def resolve_detection_mode(session: str) -> str:
|
|
|
439
450
|
return "hooks" if ClaudeConfigEditor.are_overcode_hooks_installed() else "polling"
|
|
440
451
|
|
|
441
452
|
|
|
453
|
+
def get_tui_log_path(session: str) -> Path:
|
|
454
|
+
"""Get the TUI diagnostic log file path for a specific session."""
|
|
455
|
+
return get_session_dir(session) / "tui.log"
|
|
456
|
+
|
|
457
|
+
|
|
442
458
|
def get_diagnostics_dir(session: str) -> Path:
|
|
443
459
|
"""Get the diagnostics directory for a specific session."""
|
|
444
460
|
return get_session_dir(session) / "diagnostics"
|
|
@@ -521,6 +537,7 @@ class TUIPreferences:
|
|
|
521
537
|
summary_detail: str = "full" # low, med, full
|
|
522
538
|
timeline_visible: bool = True
|
|
523
539
|
daemon_panel_visible: bool = False
|
|
540
|
+
tui_log_panel_visible: bool = False
|
|
524
541
|
preview_visible: bool = False # preview pane visibility
|
|
525
542
|
tmux_sync: bool = False # sync navigation to external tmux pane
|
|
526
543
|
show_terminated: bool = False # keep killed sessions visible in timeline
|
|
@@ -6,11 +6,14 @@ request to the sister's web API and returns a Result.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import logging
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from typing import Optional
|
|
11
12
|
from urllib.error import HTTPError, URLError
|
|
12
13
|
from urllib.request import Request, urlopen
|
|
13
14
|
|
|
15
|
+
_log = logging.getLogger("overcode.sister")
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
@dataclass
|
|
16
19
|
class ControlResult:
|
|
@@ -40,6 +43,7 @@ class SisterController:
|
|
|
40
43
|
) -> ControlResult:
|
|
41
44
|
"""Send an HTTP request to a sister's control API."""
|
|
42
45
|
url = f"{sister_url.rstrip('/')}{path}"
|
|
46
|
+
_log.debug("HTTP %s %s body=%s", method, url, body)
|
|
43
47
|
|
|
44
48
|
data = json.dumps(body or {}).encode("utf-8")
|
|
45
49
|
req = Request(url, data=data, method=method)
|
|
@@ -50,14 +54,18 @@ class SisterController:
|
|
|
50
54
|
try:
|
|
51
55
|
with urlopen(req, timeout=self.timeout) as resp:
|
|
52
56
|
result = json.loads(resp.read().decode("utf-8"))
|
|
57
|
+
_log.debug("HTTP %s %s -> ok=%s", method, url, result.get("ok", True))
|
|
53
58
|
return ControlResult(ok=result.get("ok", True), data=result)
|
|
54
59
|
except HTTPError as e:
|
|
55
60
|
try:
|
|
56
61
|
error_body = json.loads(e.read().decode("utf-8"))
|
|
57
|
-
|
|
62
|
+
error_msg = error_body.get("error", str(e))
|
|
58
63
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
59
|
-
|
|
64
|
+
error_msg = f"HTTP {e.code}: {e.reason}"
|
|
65
|
+
_log.error("HTTP %s %s -> HTTPError %s: %s", method, url, e.code, error_msg)
|
|
66
|
+
return ControlResult(ok=False, error=error_msg)
|
|
60
67
|
except (URLError, OSError) as e:
|
|
68
|
+
_log.error("HTTP %s %s -> ConnectionError: %s", method, url, e)
|
|
61
69
|
return ControlResult(ok=False, error=f"Connection error: {e}")
|
|
62
70
|
|
|
63
71
|
# --- Agent Interaction ---
|
|
@@ -122,6 +130,16 @@ class SisterController:
|
|
|
122
130
|
{"name": fork_name, "prompt": prompt},
|
|
123
131
|
)
|
|
124
132
|
|
|
133
|
+
def resize_agent(
|
|
134
|
+
self, sister_url: str, api_key: str, agent_name: str,
|
|
135
|
+
width: int, height: int,
|
|
136
|
+
) -> ControlResult:
|
|
137
|
+
return self._request(
|
|
138
|
+
"POST", sister_url, api_key,
|
|
139
|
+
f"/api/agents/{agent_name}/resize",
|
|
140
|
+
{"width": width, "height": height},
|
|
141
|
+
)
|
|
142
|
+
|
|
125
143
|
# --- Agent Configuration ---
|
|
126
144
|
|
|
127
145
|
def set_standing_orders(
|
|
@@ -220,12 +238,12 @@ class SisterController:
|
|
|
220
238
|
|
|
221
239
|
# --- Feature Toggles ---
|
|
222
240
|
|
|
223
|
-
def
|
|
241
|
+
def set_enhanced_context(
|
|
224
242
|
self, sister_url: str, api_key: str, agent_name: str, enabled: bool,
|
|
225
243
|
) -> ControlResult:
|
|
226
244
|
return self._request(
|
|
227
245
|
"PUT", sister_url, api_key,
|
|
228
|
-
f"/api/agents/{agent_name}/
|
|
246
|
+
f"/api/agents/{agent_name}/enhanced-context",
|
|
229
247
|
{"enabled": enabled},
|
|
230
248
|
)
|
|
231
249
|
|