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
|
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:
|
def skill_hash(content: str) -> str:
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -555,7 +561,12 @@ class ClaudeCliBackend(CliBackend):
|
|||||||
def __init__(self, model: str = "", claude_path: str = "claude", timeout: int = 180) -> None:
|
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",
|
super().__init__(model=model or os.environ.get("SKILLOPT_SLEEP_CLAUDE_MODEL", "") or "sonnet",
|
||||||
timeout=timeout)
|
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.
|
# Known CLI error prefixes that indicate auth or config failures.
|
||||||
# When detected, we log a warning so the user doesn't mistake a
|
# When detected, we log a warning so the user doesn't mistake a
|
||||||
@@ -614,11 +625,14 @@ class ClaudeCliBackend(CliBackend):
|
|||||||
]
|
]
|
||||||
if self.model:
|
if self.model:
|
||||||
cmd += ["--model", 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_")
|
clean_cwd = tempfile.mkdtemp(prefix="skillopt_sleep_claude_")
|
||||||
try:
|
try:
|
||||||
proc = subprocess.run(
|
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:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
@@ -677,10 +691,10 @@ class ClaudeCliBackend(CliBackend):
|
|||||||
]
|
]
|
||||||
if self.model:
|
if self.model:
|
||||||
cmd += ["--model", self.model]
|
cmd += ["--model", self.model]
|
||||||
cmd += ["--", prompt]
|
|
||||||
try:
|
try:
|
||||||
proc = subprocess.run(
|
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()
|
resp = (proc.stdout or "").strip()
|
||||||
self._detect_cli_error(resp, proc.stderr or "")
|
self._detect_cli_error(resp, proc.stderr or "")
|
||||||
@@ -779,6 +793,7 @@ class CodexCliBackend(CliBackend):
|
|||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
|
creationflags=_NO_WINDOW,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
cwd=self.project_dir or None,
|
cwd=self.project_dir or None,
|
||||||
@@ -896,7 +911,7 @@ class CodexCliBackend(CliBackend):
|
|||||||
self.last_call_error = ""
|
self.last_call_error = ""
|
||||||
proc = None
|
proc = None
|
||||||
try:
|
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:
|
except subprocess.TimeoutExpired:
|
||||||
self.last_call_error = f"codex exec (tools) timed out after {self.timeout}s"
|
self.last_call_error = f"codex exec (tools) timed out after {self.timeout}s"
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
@@ -1006,7 +1021,7 @@ class CopilotCliBackend(CliBackend):
|
|||||||
env["COPILOT_HOME"] = self.copilot_home
|
env["COPILOT_HOME"] = self.copilot_home
|
||||||
try:
|
try:
|
||||||
proc = subprocess.run(
|
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,
|
encoding="utf-8", errors="replace", env=env,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1119,7 +1134,7 @@ class CopilotCliBackend(CliBackend):
|
|||||||
resp = ""
|
resp = ""
|
||||||
try:
|
try:
|
||||||
proc = subprocess.run(
|
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,
|
errors="replace", timeout=self.timeout, cwd=work, env=env,
|
||||||
)
|
)
|
||||||
resp = self._parse_jsonl_response(proc.stdout or "")
|
resp = self._parse_jsonl_response(proc.stdout or "")
|
||||||
|
|||||||
Reference in New Issue
Block a user