Skip to content

feat: Windows support (full parity) via build-tag platform seams#83

Closed
nachiket0310 wants to merge 4 commits into
mainfrom
feat/windows-support
Closed

feat: Windows support (full parity) via build-tag platform seams#83
nachiket0310 wants to merge 4 commits into
mainfrom
feat/windows-support

Conversation

@nachiket0310

Copy link
Copy Markdown

Summary

flow was macOS-only by design — no build tags, no runtime.GOOS branching, and an entirely AppleScript-based session-spawn layer. This PR brings it to full parity on macOS, Windows, and Linux with Claude Code, isolating every OS-specific behavior behind //go:build seams so the Unix code paths are byte-identical to before.

📊 Interactive report (port design, Mac/Linux-intact proof, full Windows test results): https://claude.ai/code/artifact/74c7c8f6-89ae-4e5b-b1c5-de85150c4c78 (Claude Code artifact)

Closes #81.

Why

We want flow usable on Windows. The architecture was already portable (pure-Go SQLite/no CGO, filepath.Join + os.UserHomeDir() throughout, a clean harness.Harness interface, and FLOW_TERM=bg background mode that bypasses terminals). What blocked Windows was two hard compile errors (syscall.Setsid) plus a handful of Unix runtime assumptions (ps, AppleScript backends, the iTerm fallback, POSIX shell quoting, /-only path encoding).

How — platform seams

Standard Go build-tag pattern. Unix files contain the original logic, just relocated; the Windows siblings compile only under GOOS=windows. The overridable test seams (processAlive, claude.PSRunner, backend Runner vars) are preserved, so the existing test suite is untouched.

Concern Unix (!windows) Windows
Detached child + liveness proc_unix.goSetsid, signal-0 proc_windows.goCREATE_NEW_PROCESS_GROUP|DETACHED_PROCESS, OpenProcess/GetExitCodeProcess
Process-table scan (LiveSessionIDs) ps_unix.gops -axo ps_windows.goGet-CimInstance Win32_Process
Launch-command quoting shellquote_unix.go — POSIX shellquote_windows.go — PowerShell
Interactive backend iterm/terminal/warp/ghostty/zellij/kitty internal/wintermwt.exe + PowerShell -EncodedCommand
EncodeCwd / . _- (unchanged) also \ :- (runtime.GOOS-gated)
$EDITOR fallback vi notepad

spawner.Detect() defaults to Windows Terminal on Windows (never iTerm); winterm passes the command via base64/UTF-16LE -EncodedCommand to sidestep wt.exe/PowerShell quoting and newline pitfalls.

hookCommand is UNCHANGED (flow hook session-start) — no existing installs are orphaned (per CONTRIBUTING's callout).

Mac & Linux unchanged

The Unix paths are the same logic, only relocated. One latent regression was caught and fixed: the first cut of EncodeCwd mapped :/\- unconditionally, which would have re-encoded Linux cwds containing a : (a legal filename char) and silently broken flow transcript/flow do --here for existing Linux users. It's now gated to Windows, with a test pinning that : is preserved on Unix.

Target Build Vet Tests
🍎 macOS (native) ✅ full suite
🐧 Linux (amd64 + arm64) same !windows files as macOS
🪟 Windows (amd64 + arm64) ✅ on hardware

Test plan

  • make test, go vet ./..., and all 5 cross-compile targets (darwin/linux/windows × arches) green locally; CI now gates the same matrix (windows-latest job + cross-compile job).
  • Verified end-to-end on Windows 10 Pro (19045, amd64) hardware, zero defects:
Path Code exercised Result
flow init skill + SessionStart hook + ~/.flow ✅ existing settings.json preserved (backup written)
EncodeCwd ~/.claude/projects naming ✅ matches Claude Code (C:\…C--Users-…), confirmed via flow transcript
flow hook session-start hook handler ✅ valid hookSpecificOutput JSON
flow do --auto DETACHED_PROCESS, OpenProcess liveness ✅ running → self-flow done → completed
flow done headless close-out sweep ✅ ran; KB untouched for a trivial task
flow do (interactive) winterm.SpawnTab, -EncodedCommand wt.exe tab + claude --session-id launched
2nd flow do ps_windows.go CIM scan ✅ live-session guard refused ("running elsewhere… --force")
flow do --here binding guard ✅ refused on a done task with an actionable message
Full CLI surface add/list/show/update/tags/workdir/stats/playbooks/owners/archive/edit ✅ all exit 0

Known follow-ups (not blockers)

  • winterm tab focus returns (false, nil)wt.exe exposes no per-tab process query, so a duplicate flow do surfaces "running elsewhere" instead of focusing the existing tab. Tracked for a follow-up.
  • Live interactive transcript: Claude Code doesn't flush an interactive session's .jsonl until a graceful /exit, so flow transcript on a live interactive session returns nothing (the --auto transcript works because that session has exited). This is Claude Code flush behavior, not flow.

See docs/windows-support-plan.md for the full phased plan and open questions.

🤖 Generated with Claude Code

nachiket0310 and others added 4 commits June 24, 2026 15:59
flow was macOS-only by design — no build tags, no runtime.GOOS
branching, and an entirely AppleScript-based session-spawn layer. This
brings it to full parity on macOS, Windows, and Linux with Claude Code,
isolating every OS-specific behavior behind //go:build seams so the
Unix code paths are byte-identical to before.

Platform seams (Unix files = original logic relocated; Windows files
compile only under GOOS=windows):
- internal/app/proc_{unix,windows}.go — setDetached() + processAlive()
  (Setsid + signal-0 on Unix; CREATE_NEW_PROCESS_GROUP|DETACHED_PROCESS
  + OpenProcess/GetExitCodeProcess on Windows).
- internal/harness/claude/ps_{unix,windows}.go — runPS() feeds
  LiveSessionIDs (ps -axo vs. Get-CimInstance Win32_Process).
- internal/spawner/shellquote_{unix,windows}.go — POSIX vs. PowerShell
  quoting for the launch command.
- internal/winterm — Windows Terminal (wt.exe) backend; passes the
  command via PowerShell -EncodedCommand (base64/UTF-16LE) to avoid
  wt/PowerShell quoting and newline pitfalls. spawner.Detect() defaults
  to it on Windows (never iTerm).
- EncodeCwd: Windows path chars (\, :) mapped to '-', gated on
  runtime.GOOS so Unix encoding is unchanged (':' is a legal Linux
  filename char — mapping it everywhere would break transcript
  resolution for existing Unix users).
- edit.go: $EDITOR falls back to notepad on Windows, vi elsewhere.

Distribution, CI, docs:
- CI: windows-latest test job + a 5-target GOOS cross-compile gate.
- Release: build flow-windows-{amd64,arm64}.exe + checksums.
- Makefile build-windows target; golang.org/x/sys promoted to direct.
- Skill: Windows Task Scheduler guidance for the owner scheduler.
- Scope statements updated in README, CONTRIBUTING, CLAUDE.md;
  docs/windows-support-plan.md added.

hookCommand is UNCHANGED ("flow hook session-start") — no existing
installs are orphaned.

Verified: go test ./..., go vet, and all 5 cross-compile targets green;
the full headless + interactive surface exercised on Windows 10 Pro
(19045, amd64) hardware with zero defects.

Refs #81

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AUX (like CON, PRN, NUL, COM1-9, LPT1-9) is a reserved device name on
Windows, so git cannot check out a file named aux.go onto a Windows
filesystem — `git clone` and the windows-latest CI job both fail at
checkout with "invalid path". A Mac-side GOOS=windows cross-compile
can't catch this (it only reads the file); only a real Windows checkout
does, which the new windows-latest job did immediately.

Renamed internal/app/aux.go -> auxfiles.go and aux_test.go ->
auxfiles_test.go. Transparent to Go (package app, no filename
references); build, vet, and tests unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The windows-latest CI job surfaced three internal/app test failures
that are test-harness assumptions, not flow bugs (the Windows binary
passed the same paths on real hardware):

- TestCmdDoAutoLaunchesDetached asserted the auto-run log path contains
  'tasks/auto-task/auto-runs/' with forward slashes; filepath.Join
  yields backslashes on Windows. Now normalizes separators — still runs
  on Windows.
- TestCmdDoPropagatesFlowRootEnv and TestCmdDoWithFile pin the iTerm
  backend and inspect its osascript spawn string. iTerm's AppleScript
  escaping doubles the backslashes in Windows temp paths, so the
  assertions miss. That backend is never selected on Windows (Detect ->
  winterm); these now skip on Windows. FLOW_ROOT propagation on Windows
  is covered by internal/winterm TestSpawnTabArgvAndScript; --with-file
  injection is backend-agnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The windows-latest job exposed ~25 test-harness assumptions (the
Windows binary itself passed these paths on real hardware). Root causes:

- HOME isolation: tests set $HOME for a temp home, but os.UserHomeDir()
  reads %USERPROFILE% on Windows (ignoring $HOME), so skill/hook/
  transcript reads and writes escaped to the real profile. Added a
  shared setTestHome() that sets both; routed withTempHome,
  initTempFlowRoot, and the e2e/transcript sites through it. Fixes the
  skill/init/version/transcript/e2e batch (~20 tests).
- spawner: TestDetectFromEnv, TestDetectFlowTermInvalidFallsThrough, and
  TestShellQuoteParity assert macOS detection / POSIX quote parity; both
  are GOOS-specific now. Skipped on Windows and added
  TestDetectDefaultsToWinTermOnWindows for positive Windows coverage.
- EncodeCwd: the Unix colon-preservation case is asserted off-Windows
  only (on Windows ':' maps to '-').
- edit: TestCmdEditPlaybook used /usr/bin/true; now a cmd no-op on Windows.

Also fixed one genuine latent bug: TestMigrationHarnessSurvivesSession-
InvariantRebuild left a *sql.Rows open, which on Windows kept flow.db
locked and failed t.TempDir cleanup. Now closes the rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Windows support (full parity) — discussion + tracking

1 participant