2026-09-19. Host: Windows Server 2022 (ksen@34.133.160.141, 16 cores, 32 GB), Python 3.13.15, Node 24.21, Git 2.55. Baseline: origin/main at b38a7fa0e. Linux control: this repository's usual build host (Python 3.14).
The repository had never been run on Windows. At the baseline it could not even be cloned there (a tracked file name contains < and >), and once checked out the whole kiss package failed to import (import fcntl in kiss.core.models.model_info). After those two fixes the first complete run produced roughly 700 failing tests, three pytest processes killed by a stray Ctrl+C, and two hung processes. Four fix-and-rerun rounds later the suite stands at 10,061 tests: 0 failures apart from documented load flakes, 1,128 skipped on Windows, and all 333 JavaScript suites of the VS Code extension pass there. The Linux suite stays green (10,111 tests), with the same three load-sensitive tests as before this work.
The box was empty: no Git, Python, Node or package manager. Bring-up followed the shape of ./rsorcar (copy the SSH identity, clone origin/main, ship ~/.kiss/api_keys.env, uv sync --all-extras, npm ci, npm run compile) but in a Windows flavour, because rsorcar itself is bash + systemd. Three Windows-specific environment steps were needed:
greenlet (and therefore Playwright) fails with "DLL load failed" and 30 test modules cannot be collected;playwright install chromium;claude and codex CLIs (npm i -g), which the live-CLI tests need; their .cmd shims live in %APPDATA%\npm.Two harness lessons cost time. Processes started from an SSH session are killed when the session ends (Windows OpenSSH puts them in the session's job object); launching each split through Win32_Process.Create from a .cmd wrapper made them survive. And the default SSH shell is PowerShell 5.1, whose quoting and stderr handling differ from bash; generating a .cmd file per invocation and running it with cmd /c avoided both problems. Fix-and-verify cycles used a private git worktree per area on the Windows box (C:\Users\ksen\kiss-wt\<area>) that receives the local git diff HEAD before every run and reuses the shared virtualenv via PYTHONPATH.
Each failure was classified as one of three kinds, and fixed accordingly:
chmod-based permission denial, bash scripts, NTFS-illegal file names) or asserts a POSIX artefact (\n line endings, $! being an OS pid). Fixed in the test, usually by a narrowly scoped, reason-bearing skip or by making the fixture platform-neutral.Shared gates were added to src/kiss/tests/conftest.py so that every skip states its mechanism: posix_only(reason), requires_unix_sockets, is_root() (instead of os.geteuid() == 0, which raises at import time on Windows), plus fixture helpers install_cli_script and install_fake_cloudflared that produce a runnable fake CLI on both platforms (a #! script cannot be executed directly on Windows).
| Bug | What happened on Windows | Fix |
|---|---|---|
Tracked file with < > in its namepapers/kisssorcar/ablation/artifacts/…/<__main__.BytesPath object at 0x104bf7a10>.1 | git clone aborts the checkout: NTFS forbids those characters. The file is a 2-byte junk artefact written by an experiment. | Removed from the repository. |
Unconditional import fcntl (5 modules) | kiss.core.base imports model_info, so every kiss.* module failed to import; nothing could run. | One cross-platform primitive, kiss.core.file_lock (lock_exclusive, unlock, exclusive_file_lock; fcntl.flock on POSIX, msvcrt.locking on Windows). All lock users migrated; the sorcar persistence and worktree locks, previously no-ops on Windows, now really lock. scripts/remote_config.py, which is shipped alone to remote hosts, keeps a self-contained copy. |
os.kill(pid, 0) as a liveness probe | On Windows signal 0 is CTRL_C_EVENT: the probe delivered Ctrl+C to every process on the console. This is what killed three pytest processes with KeyboardInterrupt, and in production it would interrupt the daemon whenever a worktree or bridge liveness check ran. | kiss.core.processes.pid_alive (OpenProcess/GetExitCodeProcess on Windows). All probes, including 31 in tests, use it. |
Process-tree termination: os.killpg, start_new_session, signal.SIGKILL | None exist on Windows; the Bash tool, cron command jobs, CLI children, the WhatsApp bridge and the Signal linker could not be stopped. A first port to taskkill /F /T then proved insufficient: Git bash's fork+exec leaves background children with an already-exited parent, which taskkill's tree walk cannot follow (verified with a probe: sleep.exe survived with a dead ParentProcessId). | popen_process_group() starts the child and, on Windows, enrols it in a Job Object; kill_process_group() terminates the job (every descendant, however re-parented), falling back to taskkill, and raises OSError when it cannot, so callers fall back to proc.kill() as they did after a failed os.killpg. Eight spawn sites migrated. |
| Owner-liveness by pid alone (worktree reclaim) | Windows recycles pids within seconds under load. A dead worktree owner's pid was reused by another process, so reclaim_orphaned_worktrees treated the orphan as live and left it behind. | save_owner_pid also stamps kiss-owner-identity (process_identity(): start time + executable, prefixed with the pid); _owner_alive() requires both. Legacy pid-only stamps keep the old behaviour. |
| Daemon startup with Unix-domain sockets | CPython on Windows has no AF_UNIX; asyncio.start_unix_server does not exist. Startup logged a traceback warning and left partial state. | _setup_server checks _unix_sockets_supported(), logs one INFO line and serves WSS only; the UDS liveness probe, stale-socket unlink and shutdown-signal set (_SHUTDOWN_SIGNALS, no SIGHUP) are guarded. daemon_client.run raises a clear ConnectionError naming the path. |
Update button runs install.sh with shellPath: '/bin/bash' (VS Code extension) | There is no /bin/bash; the update terminal died with ENOENT. The JS test had skipped the execution on win32 and hidden this. | updateShellPath() resolves Git for Windows bash.exe (Program Files, per-user install, the extension's own ~/.local/git, then PATH minus the WSL launcher in System32) and shows an error toast when none exists. The test now executes the real update command on Windows too. |
Files opened without encoding="utf-8" | Windows' default code page (cp1252) raised UnicodeDecodeError reading the README, model tables, tokens and logs. | Explicit UTF-8 at every affected read (README parsing in update_models, memory pages, channel tokens, subprocess output). |
| Replacing or deleting a file another process has open | Windows raises PermissionError; atomic publishes of MY_MODELS.json, tokens, tab registry and memory pages failed intermittently under readers. | atomic_write_text retries the rename briefly on Windows (replace_waiting_for_readers, also used by the web server's _atomic_publish); read_bytes_waiting_for_writer for the readers; write_private_file delegates to atomic_write_text(mode=0o600). |
GNU grep in fs_actions.find_in_folder | No grep on Windows. | git grep --no-index --no-color --no-column --no-full-name: same path:line:text output, honours .gitignore, present wherever Git is. |
Chromium profile lock and leaked-browser identification (web_use_tool) | The POSIX SingletonLock symlink and ps-based identity check do not exist; the unresponsive-page test hung forever. | Windows probes the profile's lockfile; process_identity() reads creation time and image path via kernel32. |
| Assorted | Bash tool and cron command jobs shelled out with shell=True (cmd.exe); diff_merge matched .gitignore against backslash paths; stream_abort could not shut a socket down; signal_agent could not find signal-cli.bat; ThreadedHTTPServer.allow_reuse_address allowed two servers on one port; commands._restart_kiss_web_daemon claimed a restart it never did; the extension's file-link regex and explorer ignored drive letters; the parent-repo path guard was case-sensitive on a case-insensitive filesystem. | Each fixed at the root: Git bash for shell commands (PowerShell fallback), as_posix() matching, socket.close() on Windows, shutil.which-based CLI lookup, reuse-address off on Windows, honest "no managed daemon" message, drive-letter aware link/explorer paths, Unicode case-insensitive guard on Windows only. |
289 test files changed. The largest groups:
start_unix_server/open_unix_connection carry requires_unix_sockets. Marks were kept narrow: tests that only exercise the "daemon unreachable" error path, the jsdom toggle test and persisted-config tests run on Windows.install.sh, sync-task-db.sh, collect-github-auth.sh…), pty, resource rlimits, chmod 000 denial, systemd/cgroup shims, NTFS-illegal names (a quote or trailing space in a file name), SQLite files replaced while open. Each gets posix_only("<mechanism>").os.geteuid() in decorators (23 files); os.kill(pid, 0) probes; fake CLIs as #! scripts (now install_cli_script); a test double FakePopen without pid; CRLF vs LF comparisons; $! from Git bash being the MSYS pid rather than the Windows pid (mapped through /proc/<pid>/winpid).os.kill(pid, 9) terminated an unrelated pytest process (that was run 3's "killed split", exit code 9). Cleanup now only signals a pid that is still alive.socketserver.shutdown() made safe.| Run | Tests | Failed | Skipped | JS suites | Notes |
|---|---|---|---|---|---|
| Windows run 1 (fcntl fixed only) | 7,581* | 701* | 199 | 7 failed / 167 in the half that finished | 3 splits killed by Ctrl+C, 2 hung |
| Windows run 2 | 10,059 | 134 | 1,057 | 333 / 333 | no hangs |
| Windows run 3 | 9,343* | 2 | 1,011 | 333 / 333 | 1 split killed by a test's stale-pid kill |
| Windows run 4 | 10,061 | 4 | 1,128 | 333 / 333 | all fixed afterwards (see below) |
| Windows run 5 (final) | 10,063 | 2 | 1,128 | 333 / 333 | all 14 splits completed; both failures pass when re-run (see below) |
| Linux (final) | 10,111 | 0-1 | ~90 | 333 / 333 | only the pre-existing load flakes listed below |
Run 4's four failures: a thread-safety hole in the new Job Object registry (fixed with a lock); the recycled-owner-pid case (fixed with the identity stamp); a fixed 3 s sleep in the periodic-flush test (now polled); and one wedge of the _RWLock injection stress test, which passed 3/3 isolated re-runs on Windows and whose lock documents a deliberate residual two-injection window, so it is treated as that documented probabilistic case.
Run 5's two failures are load flakes of the 14-way parallel run, not regressions: test_openai_compat_agent::test_chat_completions_requires_bearer_token (the client saw ConnectionAbortedError 10053 while the box was saturated) and test_history_continuation_context::test_followup_with_distinct_tab_id_keeps_context (a worker thread had not yet exited at the moment of the is_alive() assertion). Re-running both modules alone on the same Windows box immediately afterwards gave 24 passed in 14.09 s.
Linux load flakes seen once each across three full runs and passing alone: test_content_tab_file_links::test_out_of_range_line_clamps_to_last_line (Playwright 30 s wait), test_remote_model_dropdown_mobile::test_model_pill_truncates_from_start, test_audit0902_fix_vscode_install_lock::test_eight_simultaneous_contenders_admit_exactly_one_installer (loser reports a previous trial's winner pid), and one live-network stall in test_update_models_decisions (SSL read to a model provider). None touch code changed here.
uv run check --full (ruff, mypy, pyright, extension typecheck and lint) passes.
A read-only gpt-5.6-sol review of every changed project file and of the test diff found seven demonstrated issues, all fixed: kill_process_group swallowed taskkill failures; remote_config.py used a bare os.replace; the parent-repo guard used ASCII-only case folding and treated \ as a separator on POSIX; git grep output shape depended on the user's git config; the hard-coded /bin/bash above; over-broad UDS skips on eight files. Two test names cited by the test reviewer did not exist and were ignored. The framework's cap on review rounds was reached before the round-2 changes could be reviewed by the second model; those (Job Objects, owner identity, helper deduplication, the polling fix) were verified directly on both platforms instead.
The VS Code extension talks to the daemon over a Unix-domain socket only (AgentClient, sorcarSockPath()), isDaemonRunning() returns false on win32 and restartKissWebDaemon() is a no-op there, and install.sh/rsorcar are bash. So on Windows today: the Python package, CLI agents, the Bash tool, cron command jobs, worktrees and the web app over WSS work; the extension's local daemon channel does not. The natural follow-up is a Windows named-pipe transport (\\.\pipe\…; Node's net accepts the same {path} API, Python needs loop.start_serving_pipe) plus a Windows daemon launcher without systemd. The 335 UDS-gated tests and 234 Muse-auth tests are the coverage that would come back with it.
# on the Windows box (PowerShell), after the tooling in §2 is installed
cd $env:USERPROFILE\kiss
uv run pytest -q -p no:cacheprovider --timeout=900 --timeout-method=thread src/kiss/tests
cd src\kiss\agents\vscode; npm run compile; node test\run-all.js
Working notes, per-run logs and the triage guide the area agents followed are in the task's tmp/ directory (PROGRESS.md, WINDOWS_TRIAGE_GUIDE.md, run1…run5).