From 93e5c3c8ba9e0e1b501c2549bc746962549f7684 Mon Sep 17 00:00:00 2001 From: codeL1985 Date: Fri, 3 Jul 2026 10:11:05 +0800 Subject: [PATCH 1/2] fix(backend): make ClaudeCliBackend work on Windows (.cmd shim + argv limit) Every claude call failed with WinError 2 and was swallowed by the bare except -> return '', so real-backend cycles silently scored 0.000/0.000 and produced zero edits. - resolve claude_path via shutil.which: the npm-installed claude is a .cmd shim that CreateProcess cannot resolve by bare name - pass the prompt via stdin instead of argv: the .cmd shim routes through cmd.exe whose command line caps at ~8K chars, which reflect/judge prompts exceed Verified: reflect() on a synthetic max_chars failure now returns a concrete bounded edit via the real CLI (was [] before). Co-Authored-By: Claude Fable 5 --- skillopt_sleep/backend.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/skillopt_sleep/backend.py b/skillopt_sleep/backend.py index cf01b0af..0e494e03 100644 --- a/skillopt_sleep/backend.py +++ b/skillopt_sleep/backend.py @@ -555,7 +555,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 @@ -614,11 +619,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, + input=prompt, ) except Exception: return "" @@ -677,10 +685,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, + input=prompt, ) resp = (proc.stdout or "").strip() self._detect_cli_error(resp, proc.stderr or "") From 2f6437b66bd2709e7ff328608d890bbb79fd4d14 Mon Sep 17 00:00:00 2001 From: codeL1985 Date: Fri, 3 Jul 2026 10:45:19 +0800 Subject: [PATCH 2/2] fix(backend): suppress console window flashes on Windows (CREATE_NO_WINDOW) Every CLI subprocess (claude/codex/copilot) spawned from a console-less parent allocates a visible console window on Windows. A cycle making hundreds of calls strobes cmd windows and steals focus, making the machine unusable while the engine runs. Pass CREATE_NO_WINDOW on all CLI call sites; no-op on POSIX. Co-Authored-By: Claude Fable 5 --- skillopt_sleep/backend.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/skillopt_sleep/backend.py b/skillopt_sleep/backend.py index 0e494e03..94b44e03 100644 --- a/skillopt_sleep/backend.py +++ b/skillopt_sleep/backend.py @@ -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 @@ -625,7 +631,7 @@ def _call(self, prompt: str, *, max_tokens: int = 1024) -> str: 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: @@ -687,7 +693,7 @@ def attempt_with_tools(self, task, skill, memory, tools): cmd += ["--model", self.model] 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() @@ -787,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, @@ -904,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 @@ -1014,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: @@ -1127,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 "")