Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions skillopt_sleep/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@

from skillopt_sleep.types import EditRecord, ReplayResult, TaskRecord

# On Windows, console-attached children (cmd.exe shims, python) allocate a
# visible console window when the parent has none — a nightly cycle making
# hundreds of CLI calls strobes cmd windows and steals focus from the user.
# CREATE_NO_WINDOW suppresses that; harmless 0 elsewhere.
_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0


def skill_hash(content: str) -> str:
import hashlib
Expand Down Expand Up @@ -555,7 +561,12 @@ class ClaudeCliBackend(CliBackend):
def __init__(self, model: str = "", claude_path: str = "claude", timeout: int = 180) -> None:
super().__init__(model=model or os.environ.get("SKILLOPT_SLEEP_CLAUDE_MODEL", "") or "sonnet",
timeout=timeout)
self.claude_path = claude_path
# On Windows the npm-installed `claude` is a .cmd shim; CreateProcess
# cannot resolve it by bare name (WinError 2), so every call would
# silently return "" and the whole cycle scores 0.0. shutil.which
# honors PATHEXT and returns the full claude.CMD path.
import shutil as _shutil
self.claude_path = _shutil.which(claude_path) or claude_path

# Known CLI error prefixes that indicate auth or config failures.
# When detected, we log a warning so the user doesn't mistake a
Expand Down Expand Up @@ -614,11 +625,14 @@ def _call(self, prompt: str, *, max_tokens: int = 1024) -> str:
]
if self.model:
cmd += ["--model", self.model]
cmd += ["--", prompt]
# Prompt goes via stdin, not argv: the Windows .cmd shim routes through
# cmd.exe whose command line caps at ~8K chars — reflect/judge prompts
# exceed that. `claude -p` with no positional prompt reads stdin.
clean_cwd = tempfile.mkdtemp(prefix="skillopt_sleep_claude_")
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=self.timeout, cwd=clean_cwd,
cmd, capture_output=True, creationflags=_NO_WINDOW, text=True, timeout=self.timeout, cwd=clean_cwd,
input=prompt,
)
except Exception:
return ""
Expand Down Expand Up @@ -677,10 +691,10 @@ def attempt_with_tools(self, task, skill, memory, tools):
]
if self.model:
cmd += ["--model", self.model]
cmd += ["--", prompt]
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=self.timeout, cwd=work,
cmd, capture_output=True, creationflags=_NO_WINDOW, text=True, timeout=self.timeout, cwd=work,
input=prompt,
)
resp = (proc.stdout or "").strip()
self._detect_cli_error(resp, proc.stderr or "")
Expand Down Expand Up @@ -779,6 +793,7 @@ def _call_once(self, prompt: str, *, max_tokens: int = 1024) -> str:
proc = subprocess.run(
cmd,
capture_output=True,
creationflags=_NO_WINDOW,
text=True,
timeout=self.timeout,
cwd=self.project_dir or None,
Expand Down Expand Up @@ -896,7 +911,7 @@ def attempt_with_tools(self, task, skill, memory, tools):
self.last_call_error = ""
proc = None
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=self.timeout, cwd=work)
proc = subprocess.run(cmd, capture_output=True, creationflags=_NO_WINDOW, text=True, timeout=self.timeout, cwd=work)
except subprocess.TimeoutExpired:
self.last_call_error = f"codex exec (tools) timed out after {self.timeout}s"
except Exception as exc: # noqa: BLE001
Expand Down Expand Up @@ -1006,7 +1021,7 @@ def _call(self, prompt: str, *, max_tokens: int = 1024) -> str:
env["COPILOT_HOME"] = self.copilot_home
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=self.timeout, cwd=clean_cwd,
cmd, capture_output=True, creationflags=_NO_WINDOW, text=True, timeout=self.timeout, cwd=clean_cwd,
encoding="utf-8", errors="replace", env=env,
)
except Exception:
Expand Down Expand Up @@ -1119,7 +1134,7 @@ def attempt_with_tools(self, task, skill, memory, tools):
resp = ""
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, encoding="utf-8",
cmd, capture_output=True, creationflags=_NO_WINDOW, text=True, encoding="utf-8",
errors="replace", timeout=self.timeout, cwd=work, env=env,
)
resp = self._parse_jsonl_response(proc.stdout or "")
Expand Down