heliograph

Windows

Two different questions get confused here, so this page separates them.

1. Can a Windows machine host the station? Yes, today, if it has Git for Windows. 2. Can a step be written in PowerShell? Yes, today, on any host.

A third - can the station itself be pure PowerShell, with no bash at all - is designed and not built. It is at the bottom of this page.

Hosting the loop on Windows

.\station.ps1                 # preflight, then run the station
.\station.ps1 --check         # preflight only, change nothing
.\station.ps1 -- --once       # everything after -- goes to station.sh

station.ps1 is a launcher, not a port. It finds the bash that Git for Windows installed and hands over to start.sh. It re-implements nothing.

That is deliberate. The capture pattern has exactly one implementation, and a PowerShell copy of it would be a second one that drifts - in the least visible way possible, because a buffered port gives every line the same timestamp, which reads like a working log while destroying the only property the log is for.

Git for Windows ships bash 4.4+ with GNU coreutils. Git is the transport, so a Windows host without git cannot participate anyway: the dependency is already paid for.

Finding bash, and the one that must not be found

Order matters: an explicit HELIOGRAPH_BASH override first, then the registry (authoritative for where Git for Windows actually landed), then the usual install paths, then PATH.

PATH is last on purpose. WSL puts a bash.exe in System32 that is not Git bash - it launches a Linux distribution, or fails with an install prompt if none exists. A lookup finding that first would fail with a confusing WSL message that says nothing about heliograph, so it is explicitly ignored.

$env:HELIOGRAPH_BASH = 'D:\tools\Git\bin\bash.exe'   # if git is somewhere unusual

Surviving a logout

service.ps1 registers a scheduled task. It does not look for bash itself - it starts station.ps1, so the registry lookup stays in one place.

Steps written in PowerShell

A step is an argv array, so the runner does not care what language it is in. A Windows question wants Get-WinEvent, not a bash reimplementation of it.

winev)  ps_step ./steps/win-events.ps1 ;;

ps_step exists so the four things that make PowerShell output unreadable in a captured log are fixed once, rather than in every step by every author who remembers. Every number below was measured on Windows Server 2022 with PowerShell 5.1.20348 and on Linux with pwsh 7.6.5.

Line endings. powershell.exe -File emits CRLF - measured CR=8, LF=8 for an eight-line script, whether redirected to a file or through a pipe. A stray CR on every line is invisible in a terminal, wrong in the file, and quietly breaks any later grep anchored with $. Rewriting each line with an explicit LF drops CR to 0.

Hyperlinks, not colour. The capture already strips ANSI ESC[ sequences, which covers everything pwsh 7 emits for Format-Table: 8 ESC bytes in, 0 out. The gap is OSC 8 hyperlinks - ESC]8;;<url> - which that pattern does not match. Measured 4 ESC bytes surviving for a step calling $PSStyle.FormatHyperlink, and 0 with NO_COLOR set. NO_COLOR is what earns its place here.

Encoding. Forcing UTF-8 is about the OEM codepage mangling non-ASCII, not about UTF-16: the redirected stream measured NUL=0, so the widely repeated "PowerShell redirects as UTF-16" does not apply to this path.

Exit codes, and this one would have shipped a wrong answer. Running the step in-process as & './step.ps1' leaves $LASTEXITCODE holding whatever the last native command inside it returned. The shipped Windows snapshot ends by probing git config --get core.autocrlf, which exits 1 when the key is unset - so a perfectly good snapshot reported failure. Invoking the step as a child with -File fixes it: measured 0 for a clean run, 3 for a genuine exit 3, and 0 for a run whose internal native command exited 7.

--mode is answered before any interpreter is looked for, so a step's declaration is readable on a machine with no PowerShell on it at all.

Line endings, and what they really break

The station ships a gitattributes file that pins the transport repo to LF.

The point is not to protect Windows from itself. Git for Windows' bash strips CR and runs a CRLF checkout perfectly well - measured on Server 2022 with Git 2.55. The point is that CRLF which gets committed breaks every Linux clone afterwards, and breaks it silently when a file is sourced: on Linux, caplib.sh reports set: pipefail: invalid option name and then carries on with neither -u nor pipefail applied.

So start.sh probes the bash it is actually running under rather than assuming, and reports one of three things: LF throughout, a CR-tolerant bash with no .gitattributes (a warning - it runs here and would break a Linux clone), or a CR-intolerant bash with CRLF files (a failure, with the fix).

If you get it wrong: git config --global core.autocrlf false and clone again. The checkout is disposable and anything already pushed is safe.

A pure PowerShell station

For a Windows Server estate with no Git for Windows and no permission to install it. "We support Windows, provided you first install Git for Windows" is a weak claim on exactly the estates this targets.

Designed, specced, and not built. The design settles on Windows PowerShell 5.1 as the floor - in-box on Server 2016 and later, so it needs no install - carrying the git, file share and relay transports.

It is permitted only on one condition, which is the condition the whole argument turns on: a second implementation of the capture is allowed only while it passes the conformance suite. An unproven port is forbidden, because the failure mode is a log that reads perfectly and is useless.

The design is in docs/specs/2026-09-08-powershell-station-and-full-documentation-design.md. This page will describe it when it exists, and not before.