fix: make ClaudeCliBackend work on Windows (.cmd shim resolution + argv length) (#98)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: codeL1985 <l@cypherlab.tech> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,12 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -614,11 +625,14 @@ class ClaudeCliBackend(CliBackend):
|
||||
]
|
||||
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 ""
|
||||
@@ -677,10 +691,10 @@ class ClaudeCliBackend(CliBackend):
|
||||
]
|
||||
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 "")
|
||||
@@ -779,6 +793,7 @@ class CodexCliBackend(CliBackend):
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
creationflags=_NO_WINDOW,
|
||||
text=True,
|
||||
timeout=self.timeout,
|
||||
cwd=self.project_dir or None,
|
||||
@@ -896,7 +911,7 @@ class CodexCliBackend(CliBackend):
|
||||
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
|
||||
@@ -1006,7 +1021,7 @@ class CopilotCliBackend(CliBackend):
|
||||
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:
|
||||
@@ -1119,7 +1134,7 @@ class CopilotCliBackend(CliBackend):
|
||||
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 "")
|
||||
|
||||
Reference in New Issue
Block a user