overcode 0.3.3__tar.gz → 0.3.5__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.5}/PKG-INFO +1 -1
- {overcode-0.3.3 → overcode-0.3.5}/pyproject.toml +1 -1
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/bundled_skills.py +31 -2
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/agent.py +82 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/config.py +2 -2
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/jobs.py +104 -1
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/monitoring.py +1 -1
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/split.py +50 -8
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/config.py +48 -7
- overcode-0.3.5/src/overcode/daemon_claude_skill.md +68 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/dependency_check.py +33 -1
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/history_reader.py +21 -2
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/hook_handler.py +51 -5
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/hook_status_detector.py +20 -1
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/launcher.py +41 -2
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/monitor_daemon.py +101 -9
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/monitor_daemon_state.py +3 -2
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/session_manager.py +17 -2
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/settings.py +17 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/sister_controller.py +22 -4
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/sister_poller.py +28 -6
- overcode-0.3.5/src/overcode/ssh_provisioner.py +234 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_detector_factory.py +6 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/summarizer_client.py +63 -6
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/summary_columns.py +98 -7
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/summary_groups.py +1 -1
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/supervisor_daemon.py +53 -2
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/supervisor_daemon_core.py +14 -7
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/time_context.py +21 -16
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tmux_manager.py +90 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui.py +285 -22
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui.tcss +13 -2
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/daemon.py +9 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/navigation.py +3 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/session.py +13 -13
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/view.py +28 -16
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/__init__.py +2 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/command_bar.py +53 -6
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/help_overlay.py +37 -22
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/session_summary.py +12 -3
- overcode-0.3.5/src/overcode/tui_widgets/tui_log_panel.py +145 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_api.py +14 -1
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_control_api.py +48 -17
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_server.py +4 -1
- {overcode-0.3.3 → overcode-0.3.5/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/SOURCES.txt +2 -0
- overcode-0.3.3/src/overcode/daemon_claude_skill.md +0 -183
- {overcode-0.3.3 → overcode-0.3.5}/LICENSE +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/MANIFEST.in +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/README.md +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/setup.cfg +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/agent_scanner.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/claude_config.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/claude_pid.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/_shared.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/budget.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/daemon.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/perms.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/sister.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/cli/skills.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/data_export.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/duration.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/exceptions.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/follow_mode.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/implementations.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/interfaces.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/job_launcher.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/job_manager.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/logging_config.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/mocks.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/notifier.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/pid_utils.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/presence_logger.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/protocols.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_constants.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_detector.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_history.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/status_patterns.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_actions/input.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_logic.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_render.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/job_summary.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/status_timeline.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web/__init__.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode/web_templates.py +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.3.3 → overcode-0.3.5}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -94,8 +94,10 @@ overcode bash "make deploy-staging" --agent my-agent # Link to an agent
|
|
|
94
94
|
|
|
95
95
|
# Manage jobs
|
|
96
96
|
overcode jobs list [--all] # List running (or all) jobs
|
|
97
|
+
overcode jobs tail <name> # Stream output (works without TTY)
|
|
98
|
+
overcode jobs tail <name> -n 50 # Last 50 lines and exit
|
|
97
99
|
overcode jobs kill <name> # Kill a running job
|
|
98
|
-
overcode jobs attach <name> # Attach to job's tmux window
|
|
100
|
+
overcode jobs attach <name> # Attach to job's tmux window (needs TTY)
|
|
99
101
|
overcode jobs clear # Remove completed/failed/killed jobs
|
|
100
102
|
|
|
101
103
|
# TUI: press J to toggle jobs view, j/k to navigate, x to kill, c to clear
|
|
@@ -227,7 +229,8 @@ overcode bash "npm run build" --name frontend-build --agent my-agent
|
|
|
227
229
|
|
|
228
230
|
# Check on it later
|
|
229
231
|
overcode jobs list
|
|
230
|
-
overcode jobs
|
|
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
|
|
|
@@ -248,6 +251,32 @@ Jobs are visible in the TUI jobs view (press `J`) and auto-clean after 24h (conf
|
|
|
248
251
|
}
|
|
249
252
|
|
|
250
253
|
|
|
254
|
+
def get_available_skills(project_dir: str | None = None) -> list[str]:
|
|
255
|
+
"""Scan for installed skill directories (user-level + project-level).
|
|
256
|
+
|
|
257
|
+
Returns sorted list of skill names found in ~/.claude/skills/
|
|
258
|
+
and optionally .claude/skills/ relative to project_dir.
|
|
259
|
+
"""
|
|
260
|
+
skills: set[str] = set()
|
|
261
|
+
|
|
262
|
+
# User-level skills
|
|
263
|
+
user_skills = Path.home() / ".claude" / "skills"
|
|
264
|
+
if user_skills.is_dir():
|
|
265
|
+
for d in user_skills.iterdir():
|
|
266
|
+
if d.is_dir() and (d / "SKILL.md").exists():
|
|
267
|
+
skills.add(d.name)
|
|
268
|
+
|
|
269
|
+
# Project-level skills
|
|
270
|
+
if project_dir:
|
|
271
|
+
proj_skills = Path(project_dir) / ".claude" / "skills"
|
|
272
|
+
if proj_skills.is_dir():
|
|
273
|
+
for d in proj_skills.iterdir():
|
|
274
|
+
if d.is_dir() and (d / "SKILL.md").exists():
|
|
275
|
+
skills.add(d.name)
|
|
276
|
+
|
|
277
|
+
return sorted(skills)
|
|
278
|
+
|
|
279
|
+
|
|
251
280
|
def any_skills_stale() -> bool:
|
|
252
281
|
"""Check if any installed skills are outdated vs bundled versions."""
|
|
253
282
|
base = Path.home() / ".claude" / "skills"
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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")],
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -128,6 +130,7 @@ def get_summarizer_config() -> dict:
|
|
|
128
130
|
api_key = os.environ.get(api_key_var)
|
|
129
131
|
|
|
130
132
|
return {
|
|
133
|
+
"api_type": api_type,
|
|
131
134
|
"api_url": api_url,
|
|
132
135
|
"model": model,
|
|
133
136
|
"api_key": api_key,
|
|
@@ -197,23 +200,35 @@ def get_web_time_presets() -> list:
|
|
|
197
200
|
]
|
|
198
201
|
|
|
199
202
|
|
|
200
|
-
def
|
|
201
|
-
"""Get
|
|
203
|
+
def get_enhanced_context_config() -> dict:
|
|
204
|
+
"""Get enhanced context configuration for the enhanced-context hook.
|
|
202
205
|
|
|
203
206
|
Config format in ~/.overcode/config.yaml:
|
|
204
|
-
|
|
207
|
+
enhanced_context:
|
|
205
208
|
office_start: 9
|
|
206
209
|
office_end: 17
|
|
207
210
|
heartbeat_interval_minutes: 15 # omit to disable
|
|
208
211
|
|
|
212
|
+
Falls back to legacy 'time_context' key for backwards compatibility.
|
|
213
|
+
|
|
209
214
|
Returns:
|
|
210
215
|
Dict with office_start (int), office_end (int),
|
|
211
216
|
heartbeat_interval_minutes (Optional[int])
|
|
212
217
|
"""
|
|
218
|
+
# Try new key first, fall back to legacy key
|
|
219
|
+
office_start = _get_config_value("enhanced_context.office_start")
|
|
220
|
+
if office_start is None:
|
|
221
|
+
office_start = _get_config_value("time_context.office_start", 9)
|
|
222
|
+
office_end = _get_config_value("enhanced_context.office_end")
|
|
223
|
+
if office_end is None:
|
|
224
|
+
office_end = _get_config_value("time_context.office_end", 17)
|
|
225
|
+
hb_interval = _get_config_value("enhanced_context.heartbeat_interval_minutes")
|
|
226
|
+
if hb_interval is None:
|
|
227
|
+
hb_interval = _get_config_value("time_context.heartbeat_interval_minutes")
|
|
213
228
|
return {
|
|
214
|
-
"office_start":
|
|
215
|
-
"office_end":
|
|
216
|
-
"heartbeat_interval_minutes":
|
|
229
|
+
"office_start": office_start,
|
|
230
|
+
"office_end": office_end,
|
|
231
|
+
"heartbeat_interval_minutes": hb_interval,
|
|
217
232
|
}
|
|
218
233
|
|
|
219
234
|
|
|
@@ -328,6 +343,7 @@ def get_new_agent_defaults() -> dict:
|
|
|
328
343
|
return {
|
|
329
344
|
"bypass_permissions": bool(defaults.get("bypass_permissions", False)),
|
|
330
345
|
"agent_teams": bool(defaults.get("agent_teams", False)),
|
|
346
|
+
"provider": defaults.get("provider", "web"),
|
|
331
347
|
}
|
|
332
348
|
|
|
333
349
|
|
|
@@ -342,6 +358,22 @@ def save_new_agent_defaults(defaults: dict) -> None:
|
|
|
342
358
|
save_config(config)
|
|
343
359
|
|
|
344
360
|
|
|
361
|
+
def get_bedrock_config() -> dict:
|
|
362
|
+
"""Get AWS Bedrock configuration.
|
|
363
|
+
|
|
364
|
+
Config format in ~/.overcode/config.yaml:
|
|
365
|
+
bedrock:
|
|
366
|
+
region: us-east-1
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Dict with region (str).
|
|
370
|
+
"""
|
|
371
|
+
bedrock = _get_config_value("bedrock", {})
|
|
372
|
+
return {
|
|
373
|
+
"region": bedrock.get("region", "us-east-1"),
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
345
377
|
def get_jobs_retention_hours() -> float:
|
|
346
378
|
"""Get job retention period in hours.
|
|
347
379
|
|
|
@@ -365,9 +397,11 @@ def get_sisters_config() -> List[dict]:
|
|
|
365
397
|
- name: "desktop"
|
|
366
398
|
url: "http://localhost:25337"
|
|
367
399
|
api_key: "secret"
|
|
400
|
+
ssh: "user@desktop" # optional: enables tmux attach + auto-provisioning
|
|
401
|
+
tmux_session: "agents" # optional: remote tmux session name (default: "agents")
|
|
368
402
|
|
|
369
403
|
Returns:
|
|
370
|
-
List of dicts with name, url, and optional api_key. Empty list if unconfigured.
|
|
404
|
+
List of dicts with name, url, and optional api_key/ssh/tmux_session. Empty list if unconfigured.
|
|
371
405
|
"""
|
|
372
406
|
sisters = _get_config_value("sisters", [])
|
|
373
407
|
if not isinstance(sisters, list):
|
|
@@ -385,6 +419,13 @@ def get_sisters_config() -> List[dict]:
|
|
|
385
419
|
api_key = s.get("api_key")
|
|
386
420
|
if api_key:
|
|
387
421
|
entry["api_key"] = api_key
|
|
422
|
+
# SSH connectivity (optional)
|
|
423
|
+
ssh = s.get("ssh")
|
|
424
|
+
if ssh:
|
|
425
|
+
entry["ssh"] = ssh
|
|
426
|
+
tmux_session = s.get("tmux_session")
|
|
427
|
+
if tmux_session:
|
|
428
|
+
entry["tmux_session"] = tmux_session
|
|
388
429
|
result.append(entry)
|
|
389
430
|
|
|
390
431
|
return result
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Overcode Supervisor Skill
|
|
2
|
+
|
|
3
|
+
You are the Overcode supervisor agent. Your mission: **Unblock each non-green session once, then exit**.
|
|
4
|
+
|
|
5
|
+
## Status Guide
|
|
6
|
+
|
|
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.
|
|
11
|
+
|
|
12
|
+
## Critical: Act Fast, Don't Investigate
|
|
13
|
+
|
|
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.
|
|
15
|
+
|
|
16
|
+
**For each non-green session in order:**
|
|
17
|
+
|
|
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
|
|
21
|
+
|
|
22
|
+
## How to Unblock
|
|
23
|
+
|
|
24
|
+
# Approve a permission request (ORANGE sessions)
|
|
25
|
+
overcode send my-agent enter
|
|
26
|
+
|
|
27
|
+
# Reject a permission request
|
|
28
|
+
overcode send my-agent escape
|
|
29
|
+
|
|
30
|
+
# Send text response (RED sessions with instructions)
|
|
31
|
+
overcode send my-agent "your guidance here"
|
|
32
|
+
|
|
33
|
+
## Approval Rules
|
|
34
|
+
|
|
35
|
+
Follow the session's **standing instructions** first. Then apply these defaults:
|
|
36
|
+
|
|
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
|
|
44
|
+
|
|
45
|
+
### Use Judgment
|
|
46
|
+
- git push (only if tests pass)
|
|
47
|
+
- Operations outside the project directory
|
|
48
|
+
- Destructive operations (rm, git reset)
|
|
49
|
+
|
|
50
|
+
### Reject
|
|
51
|
+
- rm -rf on large directories
|
|
52
|
+
- Operations on system files
|
|
53
|
+
- Network writes to external services (unless in standing instructions)
|
|
54
|
+
|
|
55
|
+
## Your Process
|
|
56
|
+
|
|
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.
|
|
63
|
+
|
|
64
|
+
**Do NOT:**
|
|
65
|
+
- Run `overcode list` (you already have the list)
|
|
66
|
+
- Read sessions.json (you already have the context)
|
|
67
|
+
- Loop back to check results
|
|
68
|
+
- Make multiple attempts on the same session
|