feat(windows): add native Windows support#67
Conversation
|
Nice! Here is AI review, some may not be relevant/correct, you decide: P1Background processes are not detached on Windows
npm tool shims can't be launched through ConPTY
Generated PowerShell files are not safe for Unicode input
The Windows does not record user keyboard activity
The stdin thread forwards bytes to ConPTY but never updates Gemini, Antigravity, and Claude hooks are POSIX-only
All three emit OpenCode and Kilo config/database paths are Unix-derived
Database archival fails while another process holds the DB open
Before archiving an incompatible DB, Native Windows is absent from every release channel
The cargo-dist target list and the wheel matrix have no Windows target; the P2Approval detection is missing from the Windows delivery state
The Windows path defers approval scraping and never sets A terminated process can be reported as alive
Codex file-edit tracking is not started
The Unix path starts Delivery starts before the tool is ready
Windows starts the delivery thread right after the IO threads. Unix gates Delivery-init failures don't fail the launch
If the Windows delivery thread can't open the DB or create its notify server, it ConPTY is never resized after startup
The size is captured once at spawn; Headless shutdown skips the graceful phase
The stop path calls Process-tree termination can miss descendants spawned after the snapshot
The kill walks one ToolHelp snapshot, deepest-first, root last. A child the
|
|
Hi, @aannoo Thank you for your feedback! I'll check them later! |
Progress on review feedbackHere's where things stand. 18 of 37 items are resolved; the remaining 19 are work in progress. Summary
P1 — Resolved ✅
P1 — Open ❌ (work in progress)
P2 — Resolved ✅
P2 — Open ❌ (work in progress)
P3 — Resolved ✅
P3 — Open ❌ (work in progress)
Design notes — Resolved ✅
The bulk of the P2 runtime gaps (ConPTY lifecycle, delivery orchestration, approval detection, keyboard activity, Codex watcher, resize, dynamic titles, early launch diagnostics) and both structural design-note items are addressed. The remaining open items — hooks (Gemini/Antigravity), the update/workflow/shell-env stack, the release pipeline, and the three P1 launch-path issues (process detach, |
…itives
Introduce src/sys/ as the single boundary for OS-specific behavior. Migrate
process liveness/termination/session-setup and file identity/permissions off
direct libc/nix/std::os::unix calls so call sites are platform-neutral.
- sys::process: is_alive, kill, terminate, kill_child_group, detach_session
(Unix: libc/nix signals + setsid; Windows: Win32 process APIs + job group flag)
- sys::fs: set_private, set_executable, file_id
(Unix: mode bits + inode; Windows: no-op modes + GetFileInformationByHandle)
- Make nix/libc/signal-hook Unix-only; add windows-sys for the Windows backend.
Migrated callers: pidtrack, shell_env, db, commands/{daemon,relay}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PTY proxy is built on nix openpty/termios/poll and cannot compile for Windows. Gate the module and its entry points so the rest of hcom builds on Windows, where run_pty returns a clear 'not yet supported' error. - main.rs: #[cfg(unix)] mod pty; split run_pty into Unix impl + Windows stub. - Move EXIT_WAS_KILLED from pty into delivery so the delivery loop compiles without the wrapper; the proxy still sets it on Unix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…layer
Add sys::{net,io,signal} and extend sys::{fs,process} so all direct
libc/nix/std::os::unix usage outside the Unix-only PTY module funnels through
the platform boundary. With this, the crate compiles for x86_64-pc-windows
(cargo check) while macOS/Linux behavior is unchanged.
- sys::net::wait_readable: notify-server socket wait (Unix poll / Windows select)
- sys::io::stdin_appears_broken: orphan-detection stdin probe
- sys::signal::register_{term,int}: shutdown flags (signal-hook / ConsoleCtrlHandler)
- sys::fs: lock_exclusive, create_private_new, is_socket
- sys::process: terminate_group/kill_group (GroupSignal), exec_replace
Migrated: notify/server, hooks/common, commands/listen, relay/{worker,mod},
instance_names, instance_lifecycle, launcher, router, terminal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Context/FromStr are only used by the Unix run_pty; gate them behind cfg(unix) so the Windows build has no unused-import warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a windows-latest job that builds the crate and compiles all test targets. The Unix PTY wrapper is gated off on Windows (Phase 1); running the full test suite on Windows is a follow-up once Windows runtime paths are validated on a real host, so this job asserts the build/compile only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Run rustfmt over the cfg(windows) bodies in sys/net.rs and sys/process.rs that fmt --check flagged (formatted regardless of target). - Gate the ExitStatusExt-based terminal tests with #[cfg(unix)] so the bin unit tests compile on Windows. - Scope the windows-build test-compile to --bins; the tests/ integration suite is Unix-only (process groups, real-tool lifecycle) and is not compiled on Windows in Phase 1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 / M1 step 1. Add create_powershell_script as the Windows-native counterpart to create_bash_script: it emits a .ps1 that sets the window title, scrubs inherited tool/identity vars, prepends discovered tool dirs to PATH (;-separated), assigns the per-launch env ($env:K = 'V'), changes directory, and runs the tool via the call operator. Window-open vs run-once cleanup mirrors the bash version (window persists via the forthcoming powershell -NoExit invocation). launch_terminal now selects the PowerShell generator and a .ps1 extension on Windows (cfg!(windows) runtime branch so both arms compile everywhere). The launch *invocation* is still bash-only and is wired up in the next step; this commit only changes the generated script content. Pure string generation, covered by unit tests that run on macOS/Linux. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 / M1 step 2 — the launch invocation side.
- which_bin: make cross-platform. Split PATH with std::env::split_paths
(so ';' separates and Windows 'C:' drive letters aren't split) and, on
Windows, probe PATHEXT extensions (.exe/.cmd/...) for an extension-less
name. Unix behavior is unchanged.
- get_windows_terminal_argv: default Windows launch prefers Windows
Terminal ("wt -- powershell -ExecutionPolicy Bypass -NoExit -File
{script}"), falling back to a fresh console via "cmd /c start". Wires
the previously-bailing "Windows" arm of the platform-default match.
- windows_shellify_preset: rewrite a preset's "bash {script}" to the
PowerShell invocation on Windows (wezterm/wt), leaving mintty (Git
Bash) alone. Applied to the resolved custom command template.
Both new helpers are referenced through cfg!(windows)/runtime match arms
so they compile on every target. Unit tests cover the preset rewrite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 / M1 step 3 — make the remaining launch modes consistent with the .ps1 script the generator now emits on Windows. The new-window path was already wired (step 2), but background and run-here still invoked the script through bash. On Windows they now run it through "powershell -ExecutionPolicy Bypass -File <script>"; the Unix branches are byte-identical to before (bash for background, resolved bash path for run-here). This closes the only paths that would have created a .ps1 and then tried to execute it with bash. Verified: macOS build + full test suite (1838 passed) + clippy clean; Windows (x86_64-pc-windows-gnu) full build links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runtime testing surfaced that interactive launch routes through the
runner script's `hcom pty <tool>` call — the PTY wrapper, which is gated
off on Windows (Phase 1). So the agent never started and the bash runner
also mangled the Windows path.
Since ConPTY is a later phase, give Windows a non-PTY direct launch (the
documented Phase 1 behavior: the agent runs in a terminal window and
participates in hcom via hooks; screen-tracking / injection arrive with
ConPTY):
- create_runner_script_windows: emit a .ps1 that runs the tool directly
("& '<toolpath>' <args>") instead of `hcom pty <tool>`, mirroring the
bash runner's env scrub, HCOM env, secret sidecar (now a dot-sourced
.ps1), and PATH setup. Sets HCOM_LAUNCHED=1 so hooks engage.
create_runner_script early-returns to it on Windows; the bash path is
unchanged.
- runner_invocation_command: run the runner via
"powershell -ExecutionPolicy Bypass -File <script>" on Windows instead
of "bash <script>". Used by both foreground and background launches.
- terminal: expose ps_quote; add a "powershell" build_env_string format
($env:K = 'V').
This removes the bash layer entirely from the Windows launch path, which
also fixes the path-mangling. Verified: macOS build + full suite (1839
passed) + clippy clean; Windows (x86_64-pc-windows-gnu) full build links.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runtime testing: claude launched and registered, but it neither learned the hcom commands (no bootstrap) nor received messages. Cause: the Claude hook command is POSIX-shell only, and Claude runs Windows hooks through cmd.exe — so every hook failed silently (no SessionStart bootstrap, no delivery). - build_hook_entry_command: emit a cmd.exe form on Windows — "where %HCOM% >nul 2>nul && %HCOM% <suffix> || exit /b 0" — using the %HCOM% var from the settings env block and exiting 0 when hcom is absent. Unix keeps the POSIX form. (%HCOM% is already recognized by the hook-removal matcher, so install/uninstall stays idempotent.) - create_runner_script_windows: also prepend the launching binary's own directory to the child PATH, so the tool's hooks can call back to this same hcom by name even when it isn't on the global PATH (dev builds). Verified: macOS build + full suite (1839 passed) + clippy clean; Windows (x86_64-pc-windows-gnu) full build links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runtime testing showed Claude executes hooks through /usr/bin/bash (Git Bash) on Windows, not cmd.exe: the cmd.exe form from the previous commit made bash choke on 'exit /b 0' and BLOCK the prompt. Revert the hook command to the POSIX form, which Git Bash runs correctly on all platforms. The real cause of the earlier silent failure was just that hcom wasn't on the PATH the hook's 'command -v hcom' searches; that stays fixed by the launcher prepending the launching binary's own directory to the child PATH. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runtime testing: claude received messages but never auto-reacted to them. The Stop hook logged pty_mode=true on Windows, so handle_poll took the PTY branch — set status=listening and returned immediately, deferring delivery to a PTY wrapper that does not exist on Windows. The 120s poll loop never ran, so woken messages were dropped. tool_extra_env set HCOM_PTY_MODE=1 for claude unconditionally. That is right on Unix (claude runs under `hcom pty claude`) but wrong on Windows, where the tool is launched directly. Gate it with !cfg!(windows) so the Windows Stop hook runs the non-PTY poll loop that blocks on the notify socket and delivers messages. Verified: macOS build + full suite (1839 passed) + clippy clean; Windows (x86_64-pc-windows-gnu) full build links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 / M2 step 1. The pty module was entirely #[cfg(unix)]; to add a Windows ConPTY proxy the reusable pieces (ScreenTracker, InjectServer, shared PtyTarget/ProxyConfig types) must compile on Windows while the nix-based Unix Proxy stays gated. - main.rs: drop the cfg(unix) gate on `mod pty`. - pty/mod.rs: gate every nix/libc/os-fd import and the Unix Proxy (struct + impl + Drop), the byte/OSC parsing helpers, signal handlers, and nix IO helpers with #[cfg(unix)]; keep PtyTarget and ProxyConfig shared. The Unix proxy code is byte-identical, just gated. - pty/inject.rs: InjectServer is now portable — the two raw-fd accessors are #[cfg(unix)] and a portable client_count() (cfg(any(test,windows))) backs the tests and the future Windows loop. - Add portable-pty 0.9 as a Windows dependency (used by the ConPTY proxy in the next step); confirmed it cross-builds to windows-gnu. run_pty on Windows still returns the 'not supported' stub; the ConPTY proxy that uses this foundation lands next. Unix behavior unchanged. Verified: macOS fmt/clippy clean + full suite (1839 passed); Windows (x86_64-pc-windows-gnu) bin builds and bin unit-tests compile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 / M2 step 2. Implement the Windows side of the PTY wrapper so an idle agent can be woken (the M1 limitation) and `hcom pty` works on Windows. pty/win.rs: a ConPTY-backed Proxy (portable-pty) mirroring the Unix proxy's public surface (spawn/run). It drives the ConPTY with blocking IO threads instead of nix::poll: - reader thread: PTY output -> our stdout, feeding ScreenTracker and the shared ScreenState the delivery loop reads (update_screen_state is a trimmed Windows counterpart of the Unix update_delivery_state). - stdin thread: our stdin -> ConPTY input. - inject thread: InjectServer -> ConPTY input (text injection + term query stub). - delivery thread: run_delivery_loop, which injects <hcom> on message arrival — this is what wakes an idle agent. A RawConsoleGuard puts our console into raw + VT passthrough (restored on drop) so the tool's TUI renders. main.rs: run_pty is now cross-platform (drops the Windows 'not supported' stub); pty::Proxy is ConPTY on Windows, openpty on Unix. The launcher is intentionally unchanged: `hcom <tool>` still uses the M1 direct-launch path (verified working), so this adds `hcom pty <tool>` on Windows for runtime validation without regressing the working path. The launcher flip to ConPTY is a follow-up once verified on a real host. Verified: macOS fmt/clippy clean + full suite (1839 passed); Windows (x86_64-pc-windows-gnu) bin builds and bin unit-tests compile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 / M2 step 3. Now that hcom pty works on Windows (ConPTY proxy), flip the Windows launcher to use it so `hcom claude` is driven by the delivery loop and idle agents can be woken — the M1 limitation this whole phase targets. - create_runner_script_windows: the .ps1 now calls `hcom pty <tool>` instead of running the tool directly. The tool's dir stays on PATH so the wrapper can resolve it. - tool_extra_env: restore HCOM_PTY_MODE=1 for claude on all platforms; with the ConPTY wrapper present, the Stop hook should again defer delivery to the wrapper (reverts the M1c Windows-only exception). This supersedes the M1b/M1c direct-launch path for claude on Windows. Messaging still flows; idle-wake now goes through ConPTY injection instead of being unavailable. Verified: macOS fmt/clippy clean + full suite (1839 passed); Windows (x86_64-pc-windows-gnu) bin builds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the 'Starting Claude Code...' hang (diagnosed from the byte logs): claude emits ESC[?9001h (Win32 Input Mode) + ESC[?1004h (focus reporting) then ESC[6n (cursor-position DSR). The proxy passed ESC[?9001h straight to the *outer* terminal, which then encoded its DSR reply as Win32 input records instead of a plain ESC[15;1R. claude waits forever for a reply it can parse -> hang. Add OutputModeFilter: a stateful CSI parser on the child->stdout path that drops complete CSI ? 9001/1004 h|l sequences (handling splits across reads) and passes everything else through unchanged. With the outer terminal kept in normal VT mode, the DSR reply round-trips correctly and the child proceeds. Mirrors the Unix side's focus/OSC filtering. Replaces the temporary first-read diagnostic logging in the reader thread. Filter unit tests live in the Windows-only module. Verified: macOS clippy clean + full suite (1839 passed, win.rs not built there); Windows (x86_64-pc-windows-gnu) builds and test-compiles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Windows has no POSIX process groups, so the Phase-1 kill_group/terminate_group only terminated the single recorded PID. For background agents that PID is the `powershell` host and the real agent (claude) runs as its child, so killing the recorded PID orphaned the agent and its subprocesses. Two complementary mechanisms now implement "kill the agent and its children": - kill_group/terminate_group/kill_child_group walk a process snapshot (CreateToolhelp32Snapshot) to collect the target PID's full descendant set, then TerminateProcess each (deepest first). The set is frozen before any kill so a dead parent can't strand a child behind a stale PID. This is the race-free path used by `hcom kill` and headless stop from an unrelated process. - The ConPTY proxy assigns its child to a KILL_ON_JOB_CLOSE job object, so the child's whole tree is reaped even if the proxy dies abnormally and Drop never runs. Drop additionally does the snapshot walk for clean exits. Adds Win32_Security + Win32_System_Diagnostics_ToolHelp windows-sys features. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promote the windows-build job from `cargo test --no-run --bins` to `cargo test --bins`, so the bin unit tests actually execute on windows-latest now that the native launch / ConPTY / kill-group paths are validated. The integration tests under tests/ stay excluded (Unix-only: process groups, real-tool lifecycle, std::os::unix), so the suite is scoped to --bins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The promoted windows-build job (running --bins) surfaced 15 failures. One is a
real Windows product bug, the rest are Unix-only test assumptions.
Real fix: ensure_schema's archive_db_at deleted the DB file while self.conn
still held it open. Unix unlinks an open file fine; Windows returns os error 32
("being used by another process"). Release the handle (swap self.conn to an
in-memory connection) before archiving, then reopen the fresh DB.
Gated #[cfg(unix)] (test-only POSIX assumptions, product behavior is correct on
a real Windows host):
- config/{default_uses_home,dir_overrides_home,dir_absolute_stays_absolute}:
$HOME + POSIX absolute paths
- runtime_env tests: $HOME resolution + \\?\ canonicalization
- commands::status::test_is_in_path: is_in_path splits PATH on ':' (Unix-only)
- shell_env resolver test: drives a POSIX shell
- terminal warp tests: Warp is macOS-only, pins POSIX paths
- launcher runner-script test: asserts the bash runner; Windows emits PowerShell
- cursor_preprocessing slug test: slugifies POSIX absolute paths
- antigravity/cursor hook tests: redirect home via $HOME, which dirs::home_dir()
ignores on Windows (reads USERPROFILE)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
replacen("{tool_cmd} ", ...) never matches when the command has no
arguments because there is no trailing space. Add a fallback that
replaces the bare tool name so the generated script always invokes
the full path regardless of whether arguments are present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Windows there are no signals, so the Unix SIGTERM handler that sets EXIT_WAS_KILLED never fires. The delivery thread then always records exit:closed instead of exit:killed, silencing kill-triggered hooks. Fix: terminate_win now uses exit code 130 (the SIGTERM-kill convention) as a cross-process sentinel. pty/win.rs::run() detects exit code 130 after child.wait() and sets EXIT_WAS_KILLED before joining the delivery thread, so the delivery loop records exit:killed correctly. Also set EXIT_WAS_KILLED in Proxy::Drop for the error path where run() exits early via ? and leaves the delivery thread still running. Race fix: reader thread no longer sets running=false on EOF. run() is the sole writer of running, ensuring EXIT_WAS_KILLED is committed before the delivery thread sees running=false and enters cleanup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tree_win Under high load CreateToolhelp32Snapshot can fail transiently. When it does, kill_tree_win falls back to killing only the root process, leaving descendants alive until the job object closes. A single immediate retry reduces the chance of hitting the partial-kill fallback path in CI. Also improves the fallback comment to note that surviving descendants are reaped by the job object (KillOnDropJob) when the proxy exits. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Windows terminate() mapped to TerminateProcess, an immediate hard kill, while daemon_stop printed "Sent SIGTERM" and then ran a pointless wait + SIGKILL-escalation loop against an already-dead process. This diverged from Unix, where terminate() = SIGTERM only *requests* exit and the caller escalates to kill() if the process does not comply. Bring Windows to parity: terminate() now sends CTRL_BREAK_EVENT to the target's process group (a process group exists because detach_session uses CREATE_NEW_PROCESS_GROUP), which sets the shutdown flag in a process that registered a handler via sys::signal::register_term. Delivery is best-effort — it only reaches a process sharing the caller's console — so the existing wait + kill() fallback in daemon_stop / stop_relay_worker_quiet still guarantees termination, exactly as SIGTERM→SIGKILL does on Unix. stop_relay_worker's return value is ignored by all callers, so the best-effort delivery (which returns false when the worker is on another console) does not regress shutdown — the callers' kill() fallback covers it. Also fix the misleading daemon_stop output: SIGTERM/SIGKILL wording is Unix-only jargon and was wrong on Windows. Note: this does not change the pre-existing 30s watchdog latency that makes graceful completion within the 5s wait window unlikely on both platforms — that is an orthogonal, cross-platform issue out of scope here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed the fixes for everything I committed to in my previous reply — merged into LandedP0
Delivery/PTY correctness
Lower priority
Cleanup (your suggested fix, adopted as-is)
Partial adoption (as described previously)
Decisions section — "Git Bash required, fail loudly"
One thing I added beyond the original list
Bugs my own fixes introduced, caught in review before landingRan a review pass on the three branches before merging and found four real regressions in my own work, all fixed before this merge:
Still held on your answersNothing has changed here since my last reply — still waiting on you for:
Ready for another pass whenever you have time. |
…d relay worker spawn - Retry a transient delivery-init error on the next tick instead of aborting the session immediately; bound the wait for a timed-out initializer's eventual result so a permanently stuck init can't hang the coordinator. - Preserve FIFO order when draining the inject-server client queue (so `hcom term inject --enter`'s text+Enter frames can't be reordered), but stop enforcing that order once the head client stalls past a timeout so later, independent clients (e.g. `hcom term` screen queries) aren't starved indefinitely. - Spawn the relay worker via `spawn_detached` instead of a plain `spawn`, so it doesn't inherit the parent's stdio handles on Windows (which could otherwise wedge a caller piping hcom's output). - Accept Copilot's SessionStart hook binding as authoritative launch-ready evidence, independent of screen-scraped footer text that can vary across Copilot versions. - Adjust the ConPTY inject-server's text→Enter delay for Windows timing and keep the Unix proxy's `DeliveryStart::Pending` handling in sync with its new timeout-carrying signature.
…m.exe in dev_root_binary
- Add child_process_path() to strip the \?\ / \?\UNC\ verbatim-path prefix
Path::canonicalize() adds on Windows, which cmd.exe and .cmd shims don't
accept as a working directory. Apply it to the canonicalized --dir passed
to launch/resume so the child process (and its trust pre-seeding) gets a
normal path.
- dev_root_binary() now looks for hcom{EXE_SUFFIX} instead of a bare "hcom",
so worktree binary resolution finds hcom.exe on Windows.
…hed spawns - which_bin(): fall through to well-known-location fallbacks even when PATH is entirely unset (not just missing the target name); add an %LOCALAPPDATA% fallback for agy. - Add executable_command() and route codex's hook subprocess spawns (version probe, app-server) through it so npm's `codex.cmd` shim gets wrapped in `cmd.exe /d /c` instead of failing to exec directly. - Add resolve_windows_tool_launcher() to bypass the codex.cmd shim entirely for the interactive ConPTY launch, invoking its Node entrypoint directly — the shim's cmd.exe `%*` expansion corrupts quote-bearing config values like the multiline developer_instructions bootstrap. Encode that value as a real TOML string (was a raw multiline literal) so quotes/backslashes/newlines survive the round trip on every platform. - Force a UTF-8 BOM on generated PowerShell scripts so stock Windows PowerShell 5.1 doesn't mis-decode non-ASCII content via the active ANSI code page. - Detach wezterm's own launch with DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP and route background-terminal spawns through spawn_detached() instead of a plain spawn, avoiding stdio-handle inheritance into the long-lived runner. - Reintroduce a cross-platform is_in_path() (PATH scan tolerant of dangling symlinks) for `hcom status`'s tool-installed check, since which_bin()'s is_file() requirement doesn't count a symlink mid-upgrade as installed.
… preset validation - zellij's preset now has a Windows open command (PowerShell -File) alongside the existing bash one, gated to the Darwin/Linux/Windows platform set. - `hcom config terminal` lists presets with an "(unavailable on this platform)" marker and rejects setting a built-in preset that isn't available on the current platform. - A malformed user-defined preset override (invalid `open`/`close` TOML) no longer silently exempts a same-named built-in from that platform gate — config validation now surfaces the TOML error instead of treating the malformed override as a valid user preset.
hook_sh_cmd() previously always emitted a POSIX `sh -c '...'` invocation.
Add a cmd.exe branch (`where {bin} >nul 2>nul && (...) || (...)`) so the
Antigravity hook fires on Windows without depending on a POSIX shell being
on PATH.
…irs::home_dir kimi_config_dir() now goes through crate::runtime_env::tool_config_root(), matching the other tool adapters' project-local-unless-overridden resolution (and respecting KIMI_CODE_HOME) instead of always falling back to dirs::home_dir()/.kimi-code. Adds KIMI_CODE_HOME to the hooks test harness's saved/restored env vars.
…ution
- resolve_git_bash() replaces a bare which_bin("bash") check with an actual
`bash --version` probe, so the Windows-only WSL launcher stub (which
reports "found" but isn't a working Bash) is caught with a clear error
instead of failing confusingly when a .sh script runs.
- python_command() tries `python`, then the Python launcher (`py -3`), each
verified with a --version probe, before falling back to an error; PYTHON
env var still takes precedence on every platform.
- get_update_cmd() now detects Windows-style install paths (uv tool / site-packages / venv under a Windows path) case-insensitively, and falls back to a PowerShell one-liner (irm ... | iex) instead of the POSIX curl installer when no known install method matches. - cmd_update() runs the PowerShell installer command directly on Windows instead of refusing to auto-update, while still refusing an unexpected POSIX shell-pipe command there.
…oring them archive_and_clear_db() used to fs::remove_file the db/-wal/-shm files with errors discarded (`let _ = ...`), so a locked file (e.g. another hcom process still holding it open, common under Windows' mandatory file locking) looked like a successful reset. remove_database_files() now removes sidecars first and the primary db last, and returns an error naming the file and telling the user to stop other hcom processes and retry, instead of claiming the reset succeeded.
…results Transcript search treated any non-success rg/grep invocation (missing binary, crash, bad args) the same as "no matches", silently returning an empty result. run_search_tool() now distinguishes a real no-match exit (status 1) from a missing binary or other failure, and search reports an error in the latter cases instead of pretending nothing matched.
`hcom listen --sql stopped:<name>` expands to the underlying life-event SQL condition (type='life' AND instance=<name> AND action='stopped'), sparing callers from hand-writing the json_extract condition to wait for a specific agent to stop.
… regex When an alternate hcom command is configured, get_bootstrap() rewrites bare "hcom" word-boundary matches to that command via regex. The bootstrap text's own [hcom:...] identity marker and <hcom>/</hcom> tags contain "hcom" too, so without protecting them first the rewrite corrupted those literal markers. Sentinel-protect them (alongside the existing command sentinel) before the regex pass and restore them after.
…isolation - apply_isolated_env() preserves the Windows OS-component env vars (SystemRoot/WINDIR/COMSPEC/PATHEXT) needed for powershell.exe and npm .cmd shims to run, and points USERPROFILE/APPDATA/LOCALAPPDATA/TEMP/TMP at the test's isolated home; sets HCOM_DEV_ROOT so test-spawned hcom processes resolve back to this worktree's binary. - external_cmd() resolves .cmd/.bat shims (npm-installed tool binaries) via cmd.exe on Windows instead of trying to exec them directly. - Add shell_hcom_command()/bash_hcom_command() so tests can build exact binary invocations correctly quoted for PowerShell vs. Git Bash. - Add start_listening_with_process_id(): a genuinely live listener (real heartbeat, not a bare `hcom start` identity that goes stale after 30s) so delivery to an active recipient can be asserted against its captured output instead of a one-shot `listen --timeout` poll; track spawned child processes for cleanup alongside tracked PIDs. - Preserve a failed real-tool test's working directory instead of cleaning it up, and trace test-spawned hcom invocations when HCOM_TEST_TRACE_COMMANDS is set. - test_relay_roundtrip.rs: capture child output through temp files with a bounded wait instead of `Command::output()` (a detached relay worker can inherit the parent's anonymous pipe handles on Windows, so `output()` may never observe EOF); migrate the sqlite3-CLI tag assertion to rusqlite; replace the Unix-only pgrep-based orphan-daemon cleanup with a Windows no-op (relies on RelayGuard's PID-file-owned cleanup instead); use the platform temp directory instead of a hardcoded /tmp for remote launch. - codex_mock.rs: advertise a platform-appropriate shell function name to the pinned Codex build and write files via `node -e` instead of shell `echo` so the mocked write command works under both bash and PowerShell.
…ow scripts - Add a windows-latest wheel target (x86_64-pc-windows-msvc) to build-wheels.yml and dist-workspace.toml, a powershell installer to dist-workspace.toml, and a Windows classifier to pyproject.toml. - ci.yml: add a native windows-latest fast gate (fmt/clippy/test/release build + packaged-binary smoke test) alongside the existing Ubuntu job, and extend the real-tool-tests matrix to run on windows-latest too (with a pwsh variant of the pinned-tool install/PATH/test steps). - Justfile: add ci-windows, mock-tools-windows, real-tool-tests-windows, and package-smoke-windows recipes mirroring the Unix `ci`/`mock-tools` gate for native PowerShell use. package-smoke-windows moves (not copies) the built release binary into target/package-smoke, since real-tool-tests-windows runs next and every test-spawned hcom process sets HCOM_DEV_ROOT — leaving a freshly-built target/release/hcom.exe behind would make dev_root_binary() prefer it (newer mtime) over the debug binary cargo test just built, silently testing the wrong binary. - Add scripts/install-mock-tools.ps1, the PowerShell counterpart of install-mock-tools.sh, for installing pinned mock CLI tools in Windows CI and local `just ci-windows` runs. - Document the PowerShell installer and `just ci-windows` in README.md.
hcom term inject --enter blindly slept 300ms between writing text and writing Enter, hoping the TUI had rendered by then. Under contention that margin isn't reliable, causing real_tool_codex to flake ~50% of the time on Windows CI (text typed but never submitted). Poll the screen and send Enter once input_text actually matches, same exclusive-ownership check the delivery state machine already uses. Also fix an unrelated pre-existing flake in test_relay_roundtrip: it asserted a killed instance's inject port fails immediately, but the PTY manager process can legitimately outlive its child by up to ~2s (bounded ConPTY pipe join in win.rs) before tearing down. Poll instead of asserting on the first check, matching every other post-kill check in the same test.
…iden codex CI timeouts assert_tool_pinned used Command::new(tool) directly, which cannot execute npm's Windows .cmd shims (CreateProcess needs an explicit .cmd/.bat extension); the CI mock claude install is shim-only, so Phase 7 failed with "program not found" while the actual hcom-launched claude worked fine. Also widen the process-bound/PTY-ready wait timeouts from 40s to 90s: the Windows codex real-tool tests landed just past the 40s deadline on CI's shared runners (codex's launcher bypasses the npm shim to invoke node directly, adding cold-start latency Unix/claude don't hit).
… mode get_claude_input_text only matched the styled `❯` prompt glyph. On the CI- pinned claude-code@2.1.185 build, `--permission-mode bypassPermissions` renders a plain `>` in the same bordered input box instead, so prompt_empty never went true and drive_claude_startup (relay test Phase 7) timed out waiting for it forever — confirmed reproducible, not a timing flake. Accept either glyph within the existing border + dim-detection checks (no loosening of the anti-false-positive heuristics). Mirror the same tolerance in the relay test's own prompt-marker checks, keeping them line-start aware (not a bare substring match) so an unrelated `>` in tips/output/diffs can't produce a false positive. Verified against the actual CI-pinned mock claude binary locally: Phase 7-9 of test_relay_roundtrip now pass (previously stuck at Phase 7 indefinitely).
Same root cause as the earlier codex launch/PTY-ready timeouts: the fork relaunch's "forked process + hook bindings" wait was still hardcoded at 40s and missed in that pass, failing on Windows CI at exactly the deadline. Also replace the bare poll_until timeout panic for the relay test's Phase 10 PONG round-trip with one that captures Device B's live screen and recent events, so a future CI failure there is diagnosable without another round-trip.
Phase 10 only called ensure_relay_worker(&path_a) after waiting for Device B's claude to finish processing — but B's `hcom send ... PONG` reply is published as part of that same turn. If A's worker was still down (or auto-exits mid-wait, watchdog ~60s idle) when B publishes, a non-retained MQTT publish with no subscriber present is simply lost, and the later poll waits forever for an event that already came and went. Re-arm before the wait starts and keep re-arming every poll iteration through it. Also watermark Device B's own status-event scan by id (previously unwatermarked), so a stale delivery->listening cycle already sitting in the last-20 window can't satisfy the check by coincidence, and add relay status + mock request diagnostics to the Phase 10 timeout panic.
…s Bash call Root cause (confirmed directly from a completed CI job log): the mock's scripted Bash tool call ran bare `hcom send @bigboss --intent inform -- PONG`. Claude's Bash tool runs under Git Bash on Windows, and its PATH does not reliably carry the test binary's directory through relay-worker -> ConPTY-child -> Bash-tool process inheritance, so the call failed with `hcom: command not found` (exit 127) — the turn still completed (fast, since the failure was immediate), but no PONG event was ever sent. This explains why Step 1 (delivery -> listening) passed quickly while Step 2 (PONG receipt) hung forever. Reference the exact CARGO_BIN_EXE_hcom binary via a Git-Bash-safe quoted path instead, mirroring the existing support::Hcom::bash_hcom_command pattern. Also revert the previous commit's relay-worker re-arm changes and their watchdog-race narrative: auto_exit_watchdog only fires when relay is *not* enabled in config, and both test devices enable it in Phases 1/3, so that was never the actual mechanism. Keeps the still-valid parts: the Device B status-event id watermark (prevents a stale delivery->listening cycle from false-matching) and a shared phase10_diagnostics() helper used by both Step 1 and Step 2 timeouts, so a future CI failure here carries relay status, live screen, recent events, and the mock's actual recorded requests instead of a bare "timed out".
|
Nice, made some fixes |
# Conflicts: # .github/workflows/ci.yml # Justfile
|
Thank you for the thorough review throughout this PR, and for taking it the rest of the way to merge — it made the Windows support much more solid than it would have been otherwise. One nice touch: a couple of your follow-on commits resolved items I'd deliberately left as deferred follow-ups (the delivery-init retry behavior, and probing Thanks again for the collaboration — happy to help with anything that comes up down the line. |
Related to #51
Summary
Adds native Windows support to hcom and includes Windows release packaging: an x86_64 MSVC standalone archive, a PowerShell installer, and a
win_amd64Python wheel.The implementation replaces POSIX-only assumptions with a platform abstraction layer and a Windows ConPTY backend while sharing path resolution, delivery state, terminal presets, and screen tracking across platforms.
What changed
src/sys/: platform abstraction for process kill/signals, file permissions, sockets, stdin liveness. Windows process-tree kill re-scans for stragglers spawned mid-kill.src/pty/win.rs: ConPTY-backed PTY proxy for I/O, resize, titles, approval/delivery scraping, sharing cross-platform behavior throughsrc/pty/shared.rs..cmd/.batnpm shims throughcmd.exe; graceful terminate and process-tree escalation; bounded child waits.x86_64-pc-windows-msvccargo-dist artifact, PowerShell installer, and Windows maturin wheel.Known limitations / deferred
hcom termhook-orphan detection and bash workflow scripts (confess,debate,fatcow) remain limited on Windows.Testing