Add a Windows Counterpart to the Linux Bootstrap Loader - #682
Conversation
Windows carried no equivalent of bootstrap.sh, by a documented decision: "the Windows form of that problem has no one-liner anybody here has run, and an unverified loader is worse than none." This supersedes that decision rather than ignoring it. bootstrap.ps1 runs under Windows PowerShell 5.1, the one shell a fresh Windows host guarantees, finds or installs PowerShell 7 through winget, hands the rest of the run to itself under pwsh, then drives upgrade-host.ps1, install-tools.ps1 and setup-github.ps1 in the same fixed order bootstrap.sh drives their Linux peers, plus a new -Wsl action reporting the WSL platform, which has no Linux side to mirror. It sits beside bootstrap.sh at host-setup/ rather than inside host-setup/windows/, the same one-concern-two-platforms shape agent-safety/ already uses for install.sh and install.ps1. Neither loader has ever run against a genuinely fresh host, only dry runs and reads against an already-configured checkout. TODO.md tracks that as its own item, since closing it needs a human on a real VM image rather than more code here.
There was a problem hiding this comment.
Pull request overview
Adds a Windows bootstrap loader (host-setup/bootstrap.ps1) that mirrors the existing Linux loader (bootstrap.sh), enabling “copy/paste from a stock host” setup on native Windows. The PR also updates docs and tests to reflect/support having two platform bootstraps.
Changes:
- Add
host-setup/bootstrap.ps1(PowerShell 5.1 entrypoint that installs/locates PowerShell 7, fetches/extracts a tarball, and driveshost-setup/windows/*scripts). - Extend
scripts/test_bootstrap.pyto assert the loader invariants for both bootstraps and to validatebootstrap.ps1presence/no-shebang. - Update host setup documentation and TODO tracking to describe the new Windows bootstrap path and record the still-unverified “fresh host” runs.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| TODO.md | Adds a tracked work cluster for verifying both bootstraps on truly fresh images; adds link refs for the two loaders. |
| scripts/test_bootstrap.py | Refactors loader-invariant assertions to run against both bootstrap.sh and bootstrap.ps1; adds a bootstrap.ps1 presence/no-shebang test. |
| host-setup/windows/README.md | Replaces the “no bootstrap.ps1” rationale with usage/behavior notes for the new Windows loader and updates examples/status list. |
| host-setup/README.md | Documents the new Windows one-liner and explains the PS 5.1 → PS 7 handoff behavior. |
| host-setup/bootstrap.ps1 | New Windows bootstrap loader implementing ref resolution, tarball fetch/extract with ownership marker, and fixed-order tool execution (plus report-only WSL action). |
| docs/host-setup.md | Updates the host-setup contract narrative to include the Windows loader and its “no Python” implications. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
$DIR = '' at top level was the same variable as the -Dir parameter $Dir, since PowerShell variable names are case-insensitive, and it reset that parameter to empty before Resolve-Directory ever read it, so -Dir has been ignored since the file was added. Found by GitHub Copilot's review of the previous commit. Verified fixed: bootstrap.ps1 -Dir <path> -DryRun -Keep now extracts under the named path rather than the default. Also converts the file to CRLF, matching the other host-setup/windows scripts and the .editorconfig [*] default (end_of_line = crlf), which editorconfig-checker enforces in CI. An earlier grep-based check of the sibling files' line endings gave a false negative; a byte-level read shows they were CRLF all along.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
host-setup/bootstrap.ps1:137
Resolve-Pwshunconditionally callsJoin-Path ${env:ProgramFiles(x86)} .... On systems whereProgramFiles(x86)is unset (e.g., 32-bit Windows / unusual environments),Join-Pathwill throw (and$ErrorActionPreference = 'Stop'makes it terminating), preventing the loader from running.
foreach ($candidate in @(
(Join-Path $env:ProgramFiles 'PowerShell\7\pwsh.exe')
(Join-Path ${env:ProgramFiles(x86)} 'PowerShell\7\pwsh.exe')
)) {
if (Test-Path $candidate) { return $candidate }
host-setup/bootstrap.ps1:150
Install-Pwshdoesn't checkwinget's exit code. Ifwinget installfails, the script still continues and then errors with a misleading message aboutwinget reported installing PowerShell 7.
step 'Installing PowerShell 7'
& winget install --id Microsoft.PowerShell --exact --source winget --accept-source-agreements --accept-package-agreements --silent --disable-interactivity | Out-Host
$found = Resolve-Pwsh
if (-not $found) {
die 'winget reported installing PowerShell 7, but pwsh could still not be found. Close this console and paste the setup lines again, or install it from https://aka.ms/PSWindows.'
Resolve-Pwsh unconditionally Join-Path'd ${env:ProgramFiles(x86)},
which is unset on a host with no WOW64 layer, and Join-Path on a null
path is a terminating error under Set-StrictMode rather than a
candidate that fails to match. Skip that candidate when the variable
carries nothing.
Install-Pwsh now names winget's own exit code in the die message when
pwsh still can't be found afterward, rather than only "reported
installing", so a genuine winget failure reads as one.
Both found by GitHub Copilot's low-confidence findings on the previous
round; the inline finding on that round (-UseBasicParsing failing
under pwsh 7) was checked against a live pwsh 7.6.4 process and
disproven, replied to in the PR thread.
|
Answering the 2 suppressed (low-confidence) findings from the review round on host-setup/bootstrap.ps1:137 — host-setup/bootstrap.ps1:150 — |
…Python CI Gates (#718) Thirty-one squashes, `56f4d7d..d54862a`. 115 files, +20436/-5298. **Merge with a merge commit, never a squash, and never with `--delete-branch`.** This pull request's head is `develop` itself. ## What lands **Fleet Skills.** The `.agents/skills/` source tree, the generated `.claude-plugin/` distribution, `scripts/build_dist.py` with its `--check` gate, and `scripts/skills_install.py` with its host stamp (#676). Packaged as skills on top of the scaffold: PR review conduct and Copilot instructions upkeep (#677), comment and doc style (#678), resync-a-repo and fleet-conformance-check (#679), the per-language codestyles (#680), git commit conventions and operational vs release workflow (#681), stand up a repo (#683), and repo-worktree (#717). Coverage gaps closed in three passes (#690, #691, #692) plus the P4 sentence-length opt-in (#697). **Host setup.** The Windows host-setup tooling and its PowerShell gate (#674), the Windows bootstrap loader (#682), Docker install and upgrade on Linux and Windows with a version floor (#701, #705), a `uv` floor in `spec/host-tools.json` (#698), self-healing of a shadowing `uv`, `jq`, or `git-restore-mtime` copy (#689), node's real winget package id (#696), and a README for the Linux host-setup nuances (#710). **Python and CI.** Python tooling in CI with the script tests moved to `scripts/tests` (#704), `ruff format` adopted and gated (#709), and the PSScriptAnalyzer claim conditioned on repos that carry `.ps1` files (#686). **Conduct rules.** Triage-order and scope guardrails in pr-review-conduct (#684), `pr_review.py wait` requesting a review rather than only polling for one (#685), a tech-agnostic signed-commit verification (#708), execution rather than analogy to verify platform-specific code (#715), and a unique worktree for every task (#717). **Docs.** The fleet map and gap register with peer messaging declared (#687), mermaid flow diagrams in the kept-authority docs (#702), and the map pointed at the shipped diagrams and current tooling (#703). ## Issues this promotion closes Each landed on `develop` on its own pull request. The keyword fires only on a merge into `main`, so it sits here rather than on the feature pull requests. Closes #700 Closes #707 Closes #711 Closes #712 Closes #714 Closes #688 #699 stays open on purpose: #717 shipped the layout convention and the skill, and the physical migration of existing checkouts is still tracked there. ## Review record Every squash closed its own Copilot loop on its own pull request before merging to `develop`. This promotion carries no new content of its own, so its review is the merged tree as a whole. ## Consequence worth stating The `GOVERNANCE.md` and `AGENTS.md` sections these squashes changed become the canonical the moment this reaches `main`, and every carrying repository reads as drifted from that point until it resyncs. That is the ordinary consequence of a canonical moving rather than a defect. The Skills installer added here is also how a machine picks the new skills up, so a session that keeps restating a rule already packaged as a skill is the signal to run it.
What
Adds
host-setup/bootstrap.ps1, a Windows counterpart tohost-setup/bootstrap.sh, so a Windows host with nothing can be stood up from a copy-paste console snippet the same way a Linux host already can.Why
Windows previously carried no loader, by a documented decision (
host-setup/README.md,docs/host-setup.md,host-setup/windows/README.md's "Why There Is No bootstrap.ps1"): "the Windows form of that problem has no one-liner anybody here has run, and an unverified loader is worse than none." This PR supersedes that decision rather than ignoring it — the reasoning is kept and repurposed as the reasonbootstrap.ps1does more defensive work than its Linux peer (TLS pinning, a tree-ownership marker, execution-policy handling, self-installing its own prerequisite) rather than as the reason none exists.How
bootstrap.ps1runs under Windows PowerShell 5.1, the one shell a fresh Windows host guarantees. It finds or installs PowerShell 7 viawinget, then hands the rest of the run to itself underpwsh.bootstrap.sh: resolves the ref to a commit, fetches/extracts the tarball with an ownership marker guarding cleanup, and drivesupgrade-host.ps1→install-tools.ps1→setup-github.ps1in the same fixed order (packages first, GitHub last since it's the only step waiting on a human in a browser).-Wslaction (report-only —setup-wsl.ps1has no Linux peer, and no flag here collects a distribution name to install one unattended).bootstrap.shathost-setup/rather than insidehost-setup/windows/, the same one-concern-two-platforms shapeagent-safety/install.sh+install.ps1already uses.host-setup/README.md,docs/host-setup.md,host-setup/windows/README.md) updated to describe the new loader and its Windows one-liner, replacing the "no bootstrap.ps1" sections.scripts/test_bootstrap.pyextended: the loader-invariant tests (fetched-tree-path, no-Python) now run against both loaders, plus a new presence/no-shebang test forbootstrap.ps1.What this does not verify
Neither loader (the existing
bootstrap.shnor this newbootstrap.ps1) has ever been run against a genuinely fresh host — only dry runs and reads against an already-configured checkout.TODO.mdnow tracks that as its own work cluster ("Neither Host Bootstrap Has Run Against a Truly Fresh Host"), since closing it needs a human watching a real run on a real fresh VM image, not more code.Verification performed
PSScriptAnalyzer(pinned 1.23.0, matching CI): clean.scripts/test_bootstrap.py: passing.scripts/prose_lint.py . --diff HEADandscripts/repo_gate.py --check eol: clean (the local pre-commit gate).-Helpunder actual Windows PowerShell 5.1.-DryRunruns againstdevelopunderpwshfor-Report,-Host -Yes,-Wsl, and the PS5.1→pwsh7 reinvocation handoff forced viapowershell.exe— all completed cleanly, correctly drove the real tool scripts, and left no orphaned trees afterward.