agents-consilium: fix Windows/MSYS compatibility (CRLF, fcntl, os.getuid, destructive pid_alive, POSIX-only Popen) - #7
Conversation
The skill assumed a POSIX host in several places, which broke it entirely under Git Bash / MSYS2 with a native Windows python3. Refs CodeAlive-AI#5. Shell/encoding: - config.sh: force sys.stdout.reconfigure(newline='\n') in _cfg_python. Native Windows python3 emits \r\n from print(), so every config lookup consumed line-by-line matched against ids with a trailing CR and failed with "unknown agent id: <id>\r". - common.sh: export PYTHONIOENCODING=utf-8 so embedded non-ASCII output does not raise UnicodeEncodeError on a non-UTF-8 console codepage. - discovery-pass.sh, judge-runner.sh: resolve the backend by passing the config path and agent id through the environment instead of splicing them into the python source, and reconfigure stdout newlines. Removes a shell-injection surface as well as the CRLF hazard. - dedup-findings.py: read/write findings with an explicit encoding="utf-8" (and newline="\n" on write) instead of the process codepage, which silently mojibaked UTF-8 agent output. - steer/adapters/{claude,opencode}.py: decode backend output as UTF-8 with errors="replace". steer package (previously failed at import time on Windows): - util.py: guard "import fcntl" and add msvcrt-based lock_exclusive()/ unlock() fallbacks; mailbox.py now uses those helpers. - util.py: pid_alive() used os.kill(pid, 0), which on Windows maps onto TerminateProcess and killed the process it was probing. Use OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) + GetExitCodeProcess. kill_process_group() falls back to taskkill /F /T, since os.getpgid and os.killpg do not exist on Windows. - registry.py: add current_uid() (os.getuid() on POSIX, 0 on Windows) and a %LOCALAPPDATA% registry root, so os.uname()/os.getuid() no longer raise AttributeError before the cross-platform logic runs. - jsonrpc.py: start_new_session is POSIX-only; use CREATE_NEW_PROCESS_GROUP on Windows. Resolve argv[0] via shutil.which() so npm-style .cmd/.bat shims and shebang scripts do not fail with WinError 193. - supervisor.py: tolerate a missing os.setsid. POSIX behavior is unchanged: every branch is gated on os.name == "nt" or on the availability of fcntl.
Runs on ubuntu-latest and windows-latest (Git Bash). Imports every steer module, asserts pid_alive() probes without terminating its target, and checks that config_enabled_agents emits LF-only agent ids whose backends resolve. These are the checks that would have caught the import-time and CRLF regressions in CodeAlive-AI#5.
Follow-up to the previous two commits, from an external review pass.
Correctness:
- util.py: msvcrt.locking() locks a byte range starting at the CURRENT file
offset, unlike flock() which locks the whole file. lock_exclusive() locked
at wherever the "a+" handle happened to sit (EOF for a non-empty lock file)
while unlock() always seeked to 0, so the two named different bytes and the
lock leaked. Both helpers now seek to 0. lock_exclusive() also retried every
OSError forever; it now uses LK_NBLCK, re-raises anything that is not
EACCES contention, and fails with TimeoutError after a bounded wait.
flock_exclusive() delegates to the same primitive instead of repeating it.
- util.py: declare argtypes/restype for OpenProcess/GetExitCodeProcess/
CloseHandle. Without them ctypes assumes c_int and truncates a 64-bit
HANDLE. pid_alive() no longer reports a live process as dead when the query
is denied (ERROR_ACCESS_DENIED) or when GetExitCodeProcess fails.
- adapters/{claude,opencode}.py passed start_new_session=True directly to
Popen, which raises on Windows — the previous commit only fixed the
JSON-RPC child. Added util.detached_popen_kwargs() and routed all three
call sites through it.
- terminal_guard.py was entirely POSIX-only (signal.SIGHUP, os.killpg,
start_new_session) and is invoked for every opencode run, so that backend
still failed on Windows. Added a taskkill-based process-tree termination
path, a platform-aware termination signal set (SIGBREAK for SIGHUP), and
the shared detach kwargs.
POSIX invariance:
- Dropped errors="replace" from the Popen calls in jsonrpc.py and both
adapters. encoding="utf-8" alone fixes the Windows codepage decode; adding
errors="replace" would also have relaxed decoding on POSIX, where invalid
bytes previously raised.
Security:
- kill_process_group() and terminal_guard.py invoke taskkill through an
absolute %SystemRoot%\System32 path. Resolving the bare name would let a
taskkill.exe in the working directory run during cancellation.
- kill_process_group() now waits for the tree to actually go away instead of
assuming taskkill succeeded.
CI:
- Added steps for the lock round-trip (on a deliberately non-empty lock
file), the detach kwargs, and terminal_guard stopping a completed backend.
pid_alive is also checked against a reaped pid.
Metadata:
- Patch bump to 9.8.1 in plugin.json and marketplace.json, per AGENTS.md.
|
Pushed a third commit ( Bugs in my own patch
Windows break I had missed
Correction to this description "POSIX behavior is unchanged" was too strong as written. Also
One thing I did not touch: |
Fixes #5. @rodion-m — as offered in the issue.
agents-consiliumassumed a POSIX host in several places. Under Git Bash / MSYS2 with a native Windowspython3,review codefailed end-to-end and the wholesteerpackage failed at import time. This PR makes those paths cross-platform. POSIX behavior is unchanged — every new branch is gated onos.name == "nt"or onfcntlbeing importable.Shell / encoding
scripts/lib/config.sh_cfg_python()now forcessys.stdout.reconfigure(newline='\n'). Windowspython3print()emits\r\n; consumed line-by-line, every id carried a trailing CR, so lookups failed withunknown agent id: grok\r. This is the primary break in #5.scripts/lib/common.shexport PYTHONIOENCODING=utf-8, so embedded non-ASCII (e.g.✓) does not raiseUnicodeEncodeErroron a console-codepage stdout.scripts/lib/discovery-pass.sh,scripts/lib/judge-runner.shscripts/lib/dedup-findings.pyencoding="utf-8"on read and write (newline="\n"on write). Previously went through the process codepage and silently mojibaked UTF-8 agent output (—→—).scripts/lib/steer/adapters/{claude,opencode}.pyerrors="replace".steerpackage (previously unimportable on Windows)steer/util.py,steer/mailbox.pyimport fcntlwithmsvcrt-basedlock_exclusive()/unlock()fallbacks. Unguarded module-scopeimport fcntltook down the entire package.steer/util.pypid_alive()usedos.kill(pid, 0). On Windows CPython maps every signal exceptCTRL_C_EVENT/CTRL_BREAK_EVENTontoTerminateProcess, so the liveness probe killed the process it was asking about. Replaced withOpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)+GetExitCodeProcess.kill_process_group()falls back totaskkill /F /T(os.getpgid/os.killpgdon't exist on Windows).steer/registry.pycurrent_uid()(os.getuid()on POSIX,0on Windows) and a%LOCALAPPDATA%registry root;os.uname()/os.getuid()raisedAttributeErrorbefore any cross-platform logic ran.steer/jsonrpc.pystart_new_sessionis POSIX-only →CREATE_NEW_PROCESS_GROUPon Windows.argv[0]now resolves throughshutil.which(), so npm-style.cmd/.batshims and shebang scripts stop failing withWinError 193.steer/supervisor.pyos.setsid.CI (second commit — drop it if you'd rather not take it)
.github/workflows/consilium-smoke.ymlruns onubuntu-latestandwindows-latest(Git Bash) and:steermodule,pid_alive()reports a live child without terminating it, and rejects a bogus pid,config_enabled_agentsemits LF-only ids whose backends resolve viaconfig_get_field,consilium --help.Those checks would have caught the import-time failures and the CRLF break immediately. The repo has no workflows today, so this is the whole CI surface — happy to drop the commit or narrow it to
windows-latestonly.Verification
Run on Windows 11 / Git Bash (MSYS2) with native Windows Python against this branch:
Note on the process point from #5
The skill ships vendored (no
.gitin the skill dir), so a local patch is silently discarded on every update with no diff or changelog entry. That is how these exact defects came back after being fixed locally in July. The CI job above is the cheapest guard against a repeat; a line inSKILL.md/README stating the Windows support level would also help users know what to expect. Glad to add that wording if you tell me which level you want to commit to.