Setup a New Development Computer
August 1, 2026 · View on GitHub
Canonical. The companion NewComputerSetupUsingAnsible.md is retained only for its deeper BIOS / OS-install / Ansible-bootstrap notes; where the two overlap, THIS document wins (cross-link added 2026-07-06, Task 12.45.e).
Purpose
This document bootstraps a new Windows 11 developer workstation so it can participate in ATAP development, sprint worktrees, local SQL Server work, ProGet package hosting, BuildMaster promotion workflows, and offline development.
The end state is:
- The machine has the expected stable and sprint worktrees under
C:\Dropbox\whertzing\GitHub, and the active overview assigns each developer to every approved host rather than deduplicating by username. - PowerShell 7 profiles are rendered from the canonical ATAP.IAC templates into the profile paths PowerShell actually loads; profiled remoting passes a bounded identity probe.
- Third-party software is installed and configured:
- SQL Server with the base instances
Production,QA, andIntegration - ProGet using the
ProductionSQL instance — full step-by-step procedure: see ProGet-Install-Runbook.md - BuildMaster using the
ProductionSQL instance — full step-by-step procedure: see BuildMaster-Install-Runbook.md
- SQL Server with the base instances
- Service accounts have managed profiles, while only the explicitly approved identities
receive Bitwarden Secrets Manager (
bws) access. Interactive Password Manager (bw/BW_SESSION) use remains a separate user-only path. - Classified, verified SQL backups exist outside Git and Dropbox; independent Inedo databases are never copied or merged between hosts.
- Stable-branch builds and tests complete successfully, and Class A/off-LAN plus bounded return certification is recorded with metadata-only evidence.
Important Conventions
- Parity journal required: Before a step in this runbook changes state on
utat022orutat01, append a secret-safe declaration withAdd-ParityChangeEntryon the host being changed. Include the category, item, old/new state, peer host, and a peer action; do not include any secret value. After the peer applies its corresponding action, acknowledge it from that peer withConfirm-ParityChangeApplied. - Use PowerShell 7 (
pwsh) for all commands in this document. - The historical phrase
SQL Server Community Editionappears in older notes, but for a developer workstation that needs SQL Server Agent you should install SQL Server 2022 Developer media. SQL Server Express does not include SQL Server Agent. - Keep SQL Server instance names under 16 characters. The base instances are
Production,QA, andIntegration. Sprint and feature-branch instances use a short tier prefix such asDevorExpplus a shortened branch or user token. - Prefer the newest
Overview.code-workspaceandOverview.Sprint.NNNN.code-workspacefiles atC:\Dropbox\whertzing\GitHubas the current branch/worktree matrix when they are present.
Current architecture and deferred gates
Apply the guide in this dependency order. A later gate does not excuse a failed earlier one.
- Settle Dropbox, inventory mutable application state, and prove exact repository integrity before changing Git or worktree state. Never grant Git trust to the whole GitHub parent.
- Join an existing sprint with the machine-local boundary retarget procedure, not a second
SprintStart. Only
.vscodeis a designed junction; AI adapter surfaces are concrete renders. - Resolve the active
(developer, host)assignment and the profile paths PowerShell actually loads. Profile templates come from ATAP.IAC, not a retired ATAP.Utilities profile target. - Keep personal
bw/BW_SESSIONuse distinct from project-scopedbwsautomation. Provision no new BWS identity until the Tasks 13.40–13.43 senior decision and release gates. - Declare one package manager as owner for every executable. The Java vendor/version/update
decision remains deferred to
SC-0286; measured Java 21/Flyway success is not that decision. - Verify the five SQL roles, classified backup boundaries, ProGet on port
50000, and BuildMaster on port50017before package or application readiness claims. - Finish with mobile-host security, Class A off-LAN proof, and a bounded return drill.
The profiled-remoting source/runtime-state fix (Task 13.20.e), the BWS identity
decision (Tasks 13.40–13.43), the Java decision (SC-0286), the production-data policy
(Task 13.60), and the BuildMaster helper release (Task 13.20.j) remain explicit gates. Do not
turn a successful operator workaround into a claim that these product changes are deployed.
OS Image Sources
Before beginning, decide which OS image you are starting from:
Option A — Direct from Microsoft (OEM image) The PC ships with a manufacturer-specific Windows image that includes OEM additions (drivers, utilities, bloatware). This is the most common starting point for a new machine. OOBE runs from this image. Bloatware should be removed during or after setup.
Option B — Custom organization image
A pre-configured Windows image built and maintained in the ATAP.IAC repository. It skips
or automates much of the manual setup described in this document.
See:
ATAP.IACfor details on the custom image pipeline. A dedicated explainer — Creating a Custom Windows 11 Organization Image — is not yet written. When available it will be linked here.
The steps below assume Option A (OEM image). Where a custom image eliminates a step, a note says so.
Phase 1: Windows and Developer Baseline
Step 0: Validate Memory Before Building (MemTest86)
Run this before installing Windows on a new machine, and on any existing host that shows unexplained bugchecks. Memory faults corrupt builds, databases, and Dropbox state silently; validating first avoids attributing hardware failures to software.
Why this is Step 0. MemTest86 boots from USB and does not need an OS. On a new build it costs one overnight run before any time is invested in the image. On an existing host it is the cheapest way to separate a hardware fault from a driver fault.
0.1 When to run
- Every new workstation, before Step 1.
- After any RAM change (added, replaced, or reseated DIMMs).
- After enabling or changing an EXPO/XMP memory profile.
- Whenever a host records more than one bugcheck in a week, especially with
differing bugcheck codes. A single repeated code usually indicates a driver; a
spread of unrelated codes (
0x1A,0x3B,0x50,0xBE,0x124) indicates memory or another hardware fault.
Check a host's bugcheck history before deciding:
# Bugcheck codes recorded in the last 30 days
Get-WinEvent -FilterHashtable @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-WER-SystemErrorReporting'
Id = 1001
StartTime = (Get-Date).AddDays(-30)
} -ErrorAction SilentlyContinue |
Sort-Object TimeCreated |
ForEach-Object {
$code = if ($_.Message -match 'bugcheck was:\s*(0x[0-9a-fA-F]+)') { $Matches[1] } else { '?' }
'{0} {1}' -f $_.TimeCreated.ToString('MM-dd HH:mm:ss'), $code
}
# WHEA entries are hardware-reported and cannot be caused by software
Get-WinEvent -FilterHashtable @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-WHEA-Logger'
StartTime = (Get-Date).AddDays(-30)
} -ErrorAction SilentlyContinue |
Select-Object TimeCreated, Id, LevelDisplayName
0.2 Build the bootable USB
MemTest86 (PassMark) Free Edition is sufficient; the paid editions add reporting features this runbook does not require. Do not substitute the built-in Windows Memory Diagnostic — it is far weaker and misses marginal faults.
- Download the MemTest86 Free Edition — Image for creating bootable USB Drive
from
https://www.memtest86.com/download.htmon a known-good machine. - Extract the ZIP. It contains
imageUSB.exeand thememtest86-usb.imgimage. - Insert a USB stick of 1 GB or larger. Its contents are destroyed.
- Run
imageUSB.exeelevated, select the USB drive, choose Write image to USB drive, and confirm.
# Identify the USB stick before writing, so the correct disk is selected in imageUSB
Get-Disk | Where-Object BusType -eq 'USB' |
Select-Object Number, FriendlyName, @{ n = 'GB'; e = { [math]::Round($_.Size / 1GB, 1) } }
Keep the stick. Label it and store it with the build media. It is reused for every host, so this step is only performed once per site.
0.3 Boot the target machine from the USB
- Insert the stick and power on, opening the firmware boot menu (commonly
F12,F11,F8, orEsc— vendor-specific). - Select the USB device, preferring the UEFI entry when both are listed.
- If the stick will not boot, disable Secure Boot in firmware, run the test, then re-enable it afterward.
0.4 Run the test
Accept the default test suite and let it complete at least four full passes. Expect roughly 30–60 minutes per pass for 32 GB, so schedule an overnight run.
- MemTest86 starts automatically after a short countdown.
- Errors are reported in red and counted per test; the run need not be stopped, but any error at all is a failure.
- At the end, choose to save the report. It is written to the USB stick as
MemTest86.logand an HTML report.
Pass criteria: zero errors across four or more consecutive passes. One error is a failure. Memory faults are frequently intermittent, so a single clean pass proves nothing.
0.5 Test at the memory profile you intend to run
An enabled EXPO/XMP profile is a factory overclock, not a JEDEC-guaranteed speed. It is a common source of exactly the mixed-bugcheck pattern described in 0.1. Test in the order below and record which configuration passed:
| Order | Firmware setting | Interpretation of a failure |
|---|---|---|
| 1 | EXPO/XMP enabled (rated speed) | The profile is unstable on this silicon; continue to test 2. |
| 2 | EXPO/XMP disabled (JEDEC, e.g. DDR5-4800) | The DIMMs themselves are faulty; RMA them. |
If the host passes at JEDEC but fails at the rated profile, either run it at JEDEC permanently or step the profile down (for example 6000 → 5600) and re-validate with a full four-pass run before trusting the machine.
Record the memory configuration under test:
Get-CimInstance Win32_PhysicalMemory |
Select-Object DeviceLocator, Manufacturer, PartNumber,
@{ n = 'GB'; e = { [math]::Round($_.Capacity / 1GB) } },
@{ n = 'Speed'; e = { $_.Speed } }
0.6 If the test fails
- Re-run with a single DIMM installed, repeating for each DIMM, to identify the failing module. Use the same slot each time so a faulty slot is not mistaken for a faulty DIMM.
- If every DIMM fails individually in the same slot, test a known-good DIMM in a different slot to implicate the slot or the memory controller.
- RMA the failing module. Do not continue with the build; a host that fails MemTest86 must not be used for ATAP development, database, or Dropbox work.
0.7 Record the result in the parity journal
Per the parity-journal convention in Important Conventions, declare the outcome on the host that was tested so the peer host can be scheduled for the same validation:
Import-Module ATAP.Utilities.SystemParityMonitor.PowerShell
Add-ParityChangeEntry `
-Category OS `
-Item 'MemTest86 memory validation' `
-OldValue 'not validated' `
-NewValue 'PASS - 4 passes, EXPO disabled (JEDEC DDR5-4800)' `
-PeerHostName 'utat01' `
-PeerActionKind Document `
-PeerAction 'Run MemTest86 per NewComputerSetup.md Step 0; record pass/fail and the memory profile tested.' `
-Reason 'Baseline hardware validation gate for all ATAP hosts.'
Adjust -NewValue to the configuration actually tested. After the peer host runs its
own validation, acknowledge the entry there with Confirm-ParityChangeApplied.
Step 1: Install Windows and Record Machine Identity
- Install Windows 11.
- Assign and record the final computer name.
- Set the timezone.
- Join the network as a private network and enable discovery and file sharing.
Verify the final machine name:
$env:COMPUTERNAME
1.1 First login — create a local user account
This setup uses a local account rather than a Microsoft account because:
- These machines are domain-less workstations on a private LAN.
- Services (SQL Server, ProGet, BuildMaster, Cobian) run under local service accounts.
- A Microsoft account ties the local profile to cloud sync in ways that can interfere with predictable configuration management.
TBD: Whether to document a Microsoft Live account as the primary user (for Windows Hello, OneDrive, or AAD/Intune policy) is still under consideration. This document covers the local-account path only until that decision is made.
Windows 11 Pro path (local account during OOBE):
- Power on the PC and complete the Language, Region, and Keyboard layout screens.
- On the "Let's connect you to a network" screen, click "I don't have internet" (bottom-left) → "Continue with limited setup". If that option is not visible, use the workaround below.
Fallback workaround (Home, or if the option is not visible on Pro):
-
On the network screen, press Shift + F10 to open a command prompt.
-
Run:
oobe\bypassnro -
The PC reboots and re-enters OOBE. "I don't have internet" now appears — click it → "Continue with limited setup".
Note:
oobe\bypassnrois an official Microsoft-supported bypass. It sets a registry flag that skips the network requirement for the current OOBE session.
After choosing limited/offline setup, enter the account details:
- Who's going to use this PC? — Enter the local username. Use the same username as on
other machines in the environment (e.g.,
whertzing) to keep paths, profile directories, and scripts consistent. - Create a super memorable password — Enter a strong password. Store it in Bitwarden under the entry for this machine (Bitwarden is installed in Step 2 / Step 5). Until then, record it securely offline.
- Security questions — Windows 11 requires three for local accounts. Use memorably wrong answers and store them in Bitwarden alongside the password.
Turn off all optional telemetry and advertising features on the privacy/diagnostics consent screens.
1.2 Verify the account is an Administrator
When OOBE creates the first local account, Windows automatically adds it to the local Administrators group. Verify after the desktop appears:
# Confirm current user's group membership
whoami /groups | Select-String Administrators
# Or list the Administrators group members explicitly
net localgroup Administrators
If the account lacks Administrator privileges — or you are adding a second admin account — run (as Administrator):
# Replace 'whertzing' with the actual username
Add-LocalGroupMember -Group 'Administrators' -Member 'whertzing'
Get-LocalGroupMember -Group 'Administrators'
1.3 (Optional) Enable the built-in Administrator account
The built-in Administrator account is disabled by default. For emergency console
access (recovery when the main account is locked out) it can be enabled:
Enable-LocalUser -Name 'Administrator'
# Leave blank for console-only access (the default when the account has no password)
Set-LocalUser -Name 'Administrator' -Password ([System.Security.SecureString]::new())
Security note: If this machine is reachable from a network, set a strong password on the built-in
Administratoraccount and store it in Bitwarden.
1.4 Windows Update
Run Windows Update immediately after first login to reach the current patch level before installing any software. This may require several reboot cycles. Ensure the machine reaches Windows 11 24H2 build 26200 or later before proceeding to Step 2.
Step 2: Install Core Tools
Install and verify these tools before continuing:
- PowerShell 7
- Git
- Visual Studio Code
- Dropbox
- Bitwarden desktop and the
bwPassword-Manager CLI (the Secrets Manager CLIbwsis installed machine-wide separately in Step 4.6) - .NET SDKs required by the repos
- Python if the workstation will run Manim or Copilot code execution
- Sysinternals Suite at
C:\Program Files\SysinternalsSuite
Useful checks:
pwsh --version
git --version
code --version
dotnet --list-sdks
bw --version
Install Dropbox first and manually (download from
dropbox.com/install). Chocolatey is not yet available at
this stage, and organization scripts, PowerShell modules, and the Chocolatey package
manifest all live under C:\Dropbox\whertzing\ once Dropbox syncs (Step 3). The remaining
tools are installed with Chocolatey below.
2.1 Install Chocolatey
Chocolatey is the organization's standard Windows package manager and a prerequisite for installing most tools in the approved package list.
Custom image note: If the machine was provisioned from the
ATAP.IACcustom image, Chocolatey may already be installed. Runchoco --versionto check before proceeding.
Open PowerShell 7 (or Windows PowerShell) as Administrator and run:
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# Verify
choco --version # Expected: 2.x or later
# Enable confirmation bypass for non-interactive installs and list sources
choco feature enable -n allowGlobalConfirmation
choco source list
If the organization runs a local Chocolatey feed through ProGet (Step 9), add it as a source once ProGet is up:
choco source add --name='proget-choco' --source='http://<proget-host>/nuget/chocolatey/'
2.2 Approved Chocolatey packages
The authoritative list of approved packages is maintained in the ATAP.IAC repository under
the Infrastructure-as-Code data for this machine class.
See:
ATAP.IAC→ organization IAC data → Chocolatey package manifest forutat022-class workstations. (Exact file path TBD — link here once the IAC data is formalized.)
The following categories are expected; exact package names and versions are in ATAP.IAC:
| Category | Key Packages |
|---|---|
| Shell & terminal | pwsh (PowerShell 7), git, git-credential-manager |
| Editor & IDE | vscode |
| Compression | 7zip |
| File sync | dropbox (installed manually above; Chocolatey manages future updates) |
| Password management | bitwarden |
| Package management | nuget.commandline |
| Diff / merge | winmerge or similar |
| Database tools | ssms (SQL Server Management Studio) |
| Runtimes | dotnet-sdk (.NET 8+), nodejs (if needed) |
Once the approved list exists as a packages.config file in ATAP.IAC:
choco install .\packages.config --yes
Or install individually as needed during setup. After installing VS Code, open a new
terminal so the code command is on PATH, then verify with code --version. (The nbgv
.NET global tool is installed machine-wide separately in Step 4.4.)
2.3 Install Sysinternals Suite
Install the pinned Sysinternals Suite release with WinGet. The suite is part of the
UTAT workstation parity baseline: it was installed on utat01 on 2026-07-28 and must
be installed to the same location on utat022 during the return procedure.
Run from an elevated PowerShell session:
winget install -e --id Microsoft.Sysinternals.Suite --version 2026-07-09
Record the installed version and resolved executable path in the parity journal. On
utat01, the existing direct-download installation is at
C:\Program Files\SysinternalsSuite; verify the WinGet installation on utat022
resolves to the same path before closing the return action.
2.4 Install Python (for Manim or Copilot code execution)
Install Python only if the workstation will run Manim (see the optional Manim section near the end of this document) or Copilot code execution.
⚠ Python version constraint — use 3.11, not 3.14 (as of 2026-04-04)
Python 3.14 is too new for several Manim Community dependencies. Pre-built binary wheels for
pycairo,manimpango, andscipyare not yet published forcp314on Windows, sopipfalls back to building from source (requires a full C/C++ toolchain and often fails). This was observed during setup ofutat022. Use Python 3.11 until the Manim dependency ecosystem catches up.
# Install Python 3.11 explicitly — adds python.exe and pip to PATH automatically
choco install python311 --yes
Chocolatey installs Python to C:\Python311\ and adds both the install root and its
Scripts\ subfolder to the system PATH. Verify:
# Reload PATH in the current shell
$env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' +
[System.Environment]::GetEnvironmentVariable('Path','User')
python --version # Expected: Python 3.11.x
pip --version # Expected: pip 24.x from C:\Python311\...
If multiple versions are installed, the
py.exelauncher (C:\Windows\py.exe) lets you target one explicitly:py -3.11,py -3.14. Confirm which version the barepythoncommand resolves to after the PATH reload and adjust if needed.
Disable the Windows Store Python stub. Windows 11 ships a python.exe stub in
%LOCALAPPDATA%\Microsoft\WindowsApps\ that redirects to the Microsoft Store and can
silently shadow the real installation. Disable it via Settings → Apps → Advanced app
settings → App execution aliases (toggle off python.exe and python3.exe), or:
$aliasPath = "$env:LOCALAPPDATA\Microsoft\WindowsApps"
Remove-Item "$aliasPath\python.exe" -ErrorAction SilentlyContinue
Remove-Item "$aliasPath\python3.exe" -ErrorAction SilentlyContinue
VS Code Python extensions:
choco install vscode-python --yes
choco install vscode-pylance --yes
| Chocolatey Package | Extension ID | Purpose |
|---|---|---|
vscode-python | ms-python.python | Core Python language support: IntelliSense, linting, debugging |
vscode-pylance | ms-python.pylance | Fast type-checking language server |
After installing, choose C:\Python311\python.exe when VS Code prompts to select an
interpreter.
Step 3: Sync the Repository Tree
Before allowing Dropbox to synchronize application state, inventory host-local mutable
directories such as databases, package stores, logs, caches, sessions, histories, captures,
and credential stores. Keep them out of Dropbox/Git unless a reviewed procedure explicitly
classifies a safe declarative subset. Wait for Dropbox to report Up to date, scan for
*conflicted copy* files and unexpected reparse points, then verify the stable repos exist:
$gitHubRoot = 'C:\Dropbox\whertzing\GitHub'
$stableRepos = @('_Planning', 'Ace', 'AceCommander', 'ATAP.Utilities', 'ATAP.IAC', 'SharedVSCode')
foreach ($repo in $stableRepos) {
$path = Join-Path $gitHubRoot $repo
if (-not (Test-Path $path)) {
throw "Missing repository: $path"
}
}
'Stable repositories are present.'
If the current sprint already exists, verify the sprint worktrees are present as well:
Get-ChildItem $gitHubRoot -Directory -Filter '*-wt-*-Sprint-*-work-items' |
Select-Object FullName
Do not run SprintStart a second time merely because this host is joining an active sprint.
Run the current machine-local boundary retarget, then verify clean Git state and exact-path
ownership/trust for each repository and worktree. Trusting C:\Dropbox\whertzing\GitHub as a
single parent path is prohibited. Confirm .vscode is the intended junction and that .agents,
.claude, .codex, and .gemini are concrete rendered surfaces.
Also confirm the shell-folder mapping script is present on disk before running it below:
C:\Dropbox\whertzing\GitHub\ATAP.Utilities\src\ATAP.Utilities.IAC.Ansible.Powershell\public\MapUserShellFoldersToDropBox.ps1
3.1 Map user shell folders to Dropbox
Once sync is complete, redirect the Windows shell folders (Documents, Downloads, Favorites, Music, Photos, Videos) to Dropbox-backed locations so all user data is backed up and consistent across machines. Run as Administrator:
& 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities\src\ATAP.Utilities.IAC.Ansible.Powershell\public\MapUserShellFoldersToDropBox.ps1'
The script sets the following registry paths under
HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders:
| Shell Folder | Registry Key | Target Path |
|---|---|---|
| Documents | Personal | C:\Dropbox\whertzing\ |
| Favorites | Favorites | C:\Dropbox\whertzing Favorites |
| Music | My Music | C:\Dropbox\Music |
| Photos | My Pictures | C:\Dropbox\Photos |
| Videos | My Video | C:\Dropbox\Videos |
| Downloads | {374DE290-123F-4565-9164-39C4925E467B} | C:\Dropbox\Downloads |
The script first displays the current vs. desired value for each folder and only applies changes where a difference is detected. After running it, sign out and sign back in (or reboot) so Windows Explorer picks up the changed shell-folder locations.
Step 4: Install PowerShell Profiles and the Login Script
The canonical developer and service-account templates come from ATAP.IAC. Deploy managed
copies through BuildTooling and HostSettings; do not create a profile symlink or revive the
retired ATAP.Utilities profile target. Discover the paths PowerShell actually loads from
$PROFILE in a fresh shell, including redirected known folders.
4.1 Verify the machine-wide PowerShell 7 profile source
Use the current ATAP.IAC profile template selected through HostSettings. In both a direct
fresh shell and ATAP.PS7.Profiled, record the loaded $PROFILE paths, source/target
existence, parser/import results, and current identity. The verified bounded workaround is
to register the endpoint with an explicit WithProfiles.pssc path. Default deployed-module
resolution and machine-state placement remain deferred to Task 13.20.e; do not present the
workaround as the installed fix.
4.2 Provision managed current-user profiles
Use Set-UserScopeProfile instead of copying or linking an individual profile.
The developer template dot-sources the canonical core profile; service-account
templates are deliberately minimal and never start Bitwarden/browser
authentication or resolve secrets. The command refuses to replace a profile
without the managed-header marker unless -Force is supplied. Each live change
adds an Add-ParityChangeEntry record for the peer machine.
Import-Module ATAP.Utilities.BuildTooling.PowerShell
$iacRoot = Join-Path $gitHubRoot 'ATAP.IAC'
Set-UserScopeProfile -AccountName 'whertzing' -AccountClass Developer `
-ATAPIACRoot $iacRoot -ATAPUtilitiesRoot $atapRoot -Confirm:$false
# Run this only after the local service account exists. Use an elevated shell
# because a different user's profile directory is being written.
Set-UserScopeProfile -AccountName 'SvcBuildmaster' -AccountClass ServiceAccount `
-ATAPIACRoot $iacRoot -ATAPUtilitiesRoot $atapRoot -Confirm:$false
For the complete service-account and BWS-machine-token procedure, see Runbook-BitwardenServiceAccounts.md.
4.3 Install required PowerShell Gallery modules
Install the PowerShell Gallery dependencies before later steps import
ATAP.Utilities.BuildTooling.PowerShell. Use -Scope AllUsers (not
CurrentUser) so the BuildMaster service account (SvcBuildmaster on
utat022) and any other local service identity can resolve these modules.
A CurrentUser install only writes under the interactive developer profile
and is invisible to service accounts — BuildMaster packing will fail
validating the module manifest when it cannot resolve powershell-yaml.
This step must be run from an elevated PowerShell 7 session so the
machine-scoped Program Files\PowerShell\Modules write succeeds.
$requiredModules = @('PSFramework', 'powershell-yaml')
foreach ($moduleName in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $moduleName)) {
Install-Module -Name $moduleName -Repository PSGallery -Scope AllUsers -Force
}
}
Get-Module -ListAvailable PSFramework, powershell-yaml |
Select-Object Name, Version, ModuleBase
The ModuleBase column should report a path under
C:\Program Files\PowerShell\Modules\..., not under
$env:USERPROFILE\Documents\PowerShell\Modules. If it reports a per-user
path, the install ran un-elevated or with the wrong scope — uninstall the
per-user copy and re-run elevated with -Scope AllUsers.
If PSGallery is not already trusted on the workstation, run this once first:
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
4.4 Install NBGV (Nerdbank.GitVersioning) machine-wide
BuildMaster runs as a service account (SvcBuildmaster on utat022) and
shells out to the nbgv CLI through Get-BuildContext during the
Experimental stage. If nbgv is installed only as a per-user dotnet tool
(for example, at C:\Users\<dev>\.dotnet\tools\nbgv.exe), the service
account cannot see it — even when pwsh is launched without -NoProfile —
and BuildMaster fails with:
The 'nbgv' CLI was not found on PATH
Install nbgv to a machine-wide dotnet tool location so every local
account (developer logins and service accounts) can resolve it. The
convention used by the ATAP profile is C:\ProgramData\dotnet\tools.
# Elevated PowerShell 7
$machineToolPath = 'C:\ProgramData\dotnet\tools'
New-Item -ItemType Directory -Path $machineToolPath -Force | Out-Null
dotnet tool install --tool-path $machineToolPath nbgv
# Verify
& (Join-Path $machineToolPath 'nbgv.exe') --version
AllUsersAllHostsV7CoreProfile.ps1 (installed at
$PSHome\profile.ps1 in step 4.1) prepends C:\ProgramData\dotnet\tools
to the process-scope PATH for every PowerShell 7 session — including the
non-interactive session BuildMaster spawns under SvcBuildmaster. After
opening a new pwsh window, both of these must succeed:
pwsh -NoProfile -Command "Get-Command nbgv -ErrorAction SilentlyContinue" # machine PATH only
pwsh -Command "Get-Command nbgv -ErrorAction SilentlyContinue" # machine PATH + profile
Upgrade later with:
dotnet tool update --tool-path 'C:\ProgramData\dotnet\tools' nbgv
4.5 Register the Bitwarden login script at startup
Import-Module ATAP.Utilities.PowerShell
$loginScript = Join-Path $profileSource 'LoginScript.ps1'
$credential = Get-BitWardenCredential
Register-StartupScheduledTask `
-TaskName 'ATAPLoginScript' `
-ScriptPath $loginScript `
-Description 'Unlock Bitwarden and populate user-scope environment variables at sign-in' `
-Credential $credential
Open a fresh PowerShell 7 console and verify that the profile and login script are active:
$PROFILE | Format-List *
[System.Environment]::GetEnvironmentVariable('BW_SESSION', 'User')
4.6 Install the Bitwarden Secrets Manager CLI (bws) machine-wide
Get-SecretATAP with the BitwardenSecretsManager provider shells out to the bws
CLI to read runtime secrets (Step 9.4.10). The Inedo products run as SvcBuildmaster
and SvcProGet, and those service / scheduled-task processes start with -NoProfile,
so bws must resolve from the machine PATH — not from a per-user winget install
under one developer's AppData, and not only from the ATAP profile's injected PATH (which
-NoProfile sessions and Windows services never load).
This is the same machine-wide-resolution requirement Step 4.4 documents for nbgv.
Install bws.exe into a shared Program Files location and add that folder to the
system PATH so every local account — SvcBuildmaster, SvcProGet, and every
interactive developer — resolves the same binary.
The bws CLI ships from the bitwarden/sdk-sm repository (it is not the same
package as the bw Password-Manager CLI, and there is no reliable machine-scope winget
package for it). Releases are tagged bws-v<version>.
Run from an elevated PowerShell 7 session.
# Pin the bws release. Bump these lines to upgrade.
$bwsVersion = '2.1.0'
$bwsAsset = "bws-x86_64-pc-windows-msvc-$bwsVersion.zip"
$bwsUrl = "https://github.com/bitwarden/sdk-sm/releases/download/bws-v$bwsVersion/$bwsAsset"
$installDir = 'C:\Program Files\Bitwarden\bws'
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
$zipPath = Join-Path $env:TEMP $bwsAsset
Invoke-WebRequest -Uri $bwsUrl -OutFile $zipPath
# The zip contains bws.exe at its root; extract it into the machine-wide install dir.
Expand-Archive -LiteralPath $zipPath -DestinationPath $installDir -Force
Remove-Item $zipPath -ErrorAction SilentlyContinue
# Confirm the binary landed.
& (Join-Path $installDir 'bws.exe') --version
Add the install dir to the Machine PATH (idempotent — only appends if missing). A
machine-scope PATH entry is inherited by Windows services and by scheduled tasks
launched with -NoProfile, which is why it is used here instead of the profile PATH
injection used for nbgv:
$installDir = 'C:\Program Files\Bitwarden\bws'
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$entries = $machinePath -split ';' | Where-Object { $_ }
if ($entries -notcontains $installDir) {
$newPath = ($entries + $installDir) -join ';'
[Environment]::SetEnvironmentVariable('Path', $newPath, 'Machine')
Write-Host "Added $installDir to Machine PATH."
} else {
Write-Host "$installDir already on Machine PATH."
}
A Machine PATH change is composed into a process's environment only at process
creation, and a child process inherits its parent's in-memory environment block —
not a freshly rebuilt one. The elevated session you just ran the install in still holds
the old PATH, and any pwsh you launch from it (including the verification lines
below) inherits that stale block and will report bws as missing. Open a brand-new
pwsh window from the Start menu / Explorer (not from the install session), then
verify that bws resolves both with and without a profile:
pwsh -NoProfile -Command "(Get-Command bws -ErrorAction SilentlyContinue).Source" # machine PATH only
pwsh -Command "(Get-Command bws -ErrorAction SilentlyContinue).Source" # machine PATH + profile
bws --version
If you must verify without opening a new window, refresh the current session's PATH
from the registry first (this is what a fresh window does automatically):
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' +
[Environment]::GetEnvironmentVariable('Path','User')
(Get-Command bws).Source
Windows services and scheduled tasks likewise inherit the new PATH only after a
service restart (or a reboot).
Both must print a path under C:\Program Files\Bitwarden\bws and the version. The
-NoProfile line is the important one: it proves the binary is visible to the
-NoProfile service-account contexts described in Step 9.4.10, independent of any
PowerShell profile.
A Machine PATH entry is account-independent, so the -NoProfile check above
already proves the binary is resolvable for every account that starts after the change —
including SvcBuildmaster and SvcProGet once their services are restarted. The
following is an optional belt-and-suspenders confirmation that opens a one-shot
-NoProfile shell as the service account and prints the resolved path.
This check is only possible after the service accounts exist (Step 5); if you are
running Step 4.6 in document order, the accounts do not exist yet — skip this and rely
on the -NoProfile check above, or return here after Step 5.
Prompt for the service account's Windows password with Get-Credential rather than
fetching it through Get-SecretATAP. The verification's only goal is to prove bws is
on the service account's PATH; routing it through the Password-Manager bw CLI would
couple this check to an unrelated dependency (a live BW_SESSION and bw's network/TLS
state) that can fail for reasons having nothing to do with bws.
$svc = 'SvcBuildmaster' # repeat with 'SvcProGet'
$cred = Get-Credential -UserName "$env:COMPUTERNAME\$svc" `
-Message "Windows password for $svc (PATH check only)"
# Write the captured output somewhere the service account can also reach. Do NOT use
# your own $env:TEMP (under your profile) — the spawned service-account process cannot
# use another user's profile Temp as its working directory and emits a harmless
# "drive root ... does not exist" warning. C:\Windows\Temp is readable by all accounts.
$out = "C:\Windows\Temp\bws-pathcheck-$svc.txt"
# Keep the Start-Process call on one physical line. A backtick continuation here breaks
# when the block is pasted into an interactive console line-by-line (the -ArgumentList
# arguments end up on a separate, separately-evaluated line). -WorkingDirectory points the
# child at a path the service account can access.
$argList = @('-NoProfile', '-Command', '(Get-Command bws -ErrorAction SilentlyContinue).Source; bws --version')
Start-Process pwsh -Credential $cred -Wait -WorkingDirectory 'C:\Windows\Temp' -RedirectStandardOutput $out -ArgumentList $argList
Get-Content $out
Remove-Item $out -ErrorAction SilentlyContinue
Expected — the resolved path under C:\Program Files\Bitwarden\bws and the bws
version, printed from the service account's own -NoProfile context.
Upgrading. Bump
$bwsVersionand re-run the download/extract block; the PATH and verification steps are idempotent and do not need to change. Overwritingbws.exein place is safe because every account shares the one binary by path rather than copying it per profile.
Why not
winget install Bitwarden.SecretsManager? Awingetper-user install placesbwsunder the installing developer's profile, where service accounts cannot see it. If a verified machine-scope package becomes available you may use it, but you must still pass the-NoProfileand service-account resolution checks above.
4.7 Gate Java, Flyway, and package-manager ownership
Inventory Java from every package manager and every resolved path before installing or
removing a runtime. A successful UTAT01 repair removed Oracle Java 8 and proved Java 21 with
Flyway, but the canonical vendor, architecture, version, update owner, rollback, JAVA_HOME,
and PATH-order decision remains deferred to SC-0286. Do not generalize that measured repair
into an organization-wide Java selection.
Apply the same ownership gate to Chocolatey, pip, npm, NuGet/.NET tools, and manually installed binaries. Every overlap needs one declared owner even when versions match. Record only version, path, architecture, package owner, and test outcome. Flyway validation must run with the exact Java executable selected by the gate.
Phase 2: Third-Party Software
Step 5: Create the Bitwarden Secrets and Local Service Accounts
Create these Bitwarden items in the ComputerLogins collection before installing any
third-party service. Each item must contain a username and password field.
| Bitwarden item name | Local Windows account | Used by |
|---|---|---|
<COMPUTERNAME>-SQLServerSrvAcct-Production | SQLServerSrvAcct | SQL Server Database Engine and SQL Server Agent |
<COMPUTERNAME>-SvcProGet-Production | SvcProGet | ProGet service |
<COMPUTERNAME>-SvcBuildmaster-Production | SvcBuildmaster | BuildMaster service |
If this workstation must be brought online before an Ansible controller exists,
use the manual bootstrap procedure in
ServiceAccountsAndBitwarden.md under
Setting Up Credentials for Services without Ansible after the local service accounts
are created and before installing services that need Bitwarden access.
Then provision the local accounts:
Import-Module ATAP.Utilities.PowerShell
$serviceAccounts = @(
@{
SecretName = "SQLServerSrvAcct.$($env:COMPUTERNAME.ToLowerInvariant())"
AccountName = 'SQLServerSrvAcct'
FullName = 'SQL Server Service Identity'
Description = 'Local service account for SQL Server Database Engine and Agent'
},
@{
SecretName = "SvcProGet.$($env:COMPUTERNAME.ToLowerInvariant())"
AccountName = 'SvcProGet'
FullName = 'ProGet Service Identity'
Description = 'Local service account for the Inedo ProGet service'
},
@{
SecretName = "SvcBuildmaster.$($env:COMPUTERNAME.ToLowerInvariant())"
AccountName = 'SvcBuildmaster'
FullName = 'BuildMaster Service Identity'
Description = 'Local service account for Inedo BuildMaster service'
}
)
foreach ($entry in $serviceAccounts) {
New-LocalServiceAccount `
-AccountName $entry.AccountName `
-FullName $entry.FullName `
-Description $entry.Description `
-SecretNameServiceAccountLoginCredentials $entry.SecretName `
-GrantSeServiceLogonRight
}
Step 6: Install SQL Server and Record the Setup Media Path
6.1 Preserve the setup media location
Keep the extracted SQL Server media on disk because setup.exe is required to add or
remove named instances later.
Recommended location:
$sqlSetupRoot = 'D:\Temp\SQLExpr\extracted'
$sqlSetupExe = Join-Path $sqlSetupRoot 'Setup.exe'
If you do not remember where the media was extracted, try the helper that looks in the registry and the standard staging folders:
Import-Module ATAP.Utilities.BuildTooling.PowerShell
Find-SqlServerSetupExe
Record the returned path in the workstation notes or in the relevant Overview workspace
file for that sprint.
6.2 Install the base instances
Install or verify these permanent instances:
ProductionQAIntegration
All SQL Server named instances use TCP with fixed, high-range ports; do not use dynamic ports. This is the intended configuration on every host:
| Instance | TCP port |
|---|---|
Production | 50020 |
QA | 50025 |
Integration | 50030 |
DevWhertzing | 50035 |
ExpWhertzing | 50040 |
Use the database-management helper:
Import-Module ATAP.Utilities.DatabaseManagement.Powershell
$setupExe = Find-SqlServerSetupExe
$setupRoot = Split-Path $setupExe -Parent
$hostName = $env:COMPUTERNAME.ToLowerInvariant()
$topology = $global:settings[$global:configRootKeys['SqlInstanceTopologyConfigRootKey']]
$hosts = $topology[$global:configRootKeys['SqlInstanceTopologyHostsConfigRootKey']]
$instances = $hosts[$hostName][$global:configRootKeys['SqlInstanceTopologyInstancesConfigRootKey']]
$instanceNameKey = $global:configRootKeys['SqlInstanceTopologyInstanceNameConfigRootKey']
$tcpPortKey = $global:configRootKeys['SqlInstanceTopologyTcpPortConfigRootKey']
foreach ($role in @('PRODUCTION', 'QA', 'INTEGRATION')) {
$instance = $instances[$role]
Install-SqlServerInstance `
-DatabaseHost 'localhost' `
-SqlInstance $instance[$instanceNameKey] `
-ConnectionMethod 'tcp' `
-Port $instance[$tcpPortKey] `
-AuthenticationMode 'Windows' `
-IntegratedSecurity `
-Version '2022' `
-SqlServerSetupPath $setupRoot
}
6.3 During the SQL Server setup UI
When the SQL installer prompts for service accounts:
- Configure the Database Engine to run as
SQLServerSrvAcct. - Configure SQL Server Agent to run as
SQLServerSrvAcct. - Set both services to automatic startup.
- Keep Windows authentication enabled.
6.4 Verify the instances and SQL Server Agent
@('Production', 'QA', 'Integration') | ForEach-Object {
Get-Service -Name "MSSQL`$$_", "SQLAgent`$$_" -ErrorAction SilentlyContinue |
Select-Object Name, Status, StartType
}
6.5 Enable TCP/IP and set backup paths
For each named instance, configure TCP/IP with the fixed port in the preceding table and clear its dynamic-port setting:
- Open SQL Server Configuration Manager.
- Enable TCP/IP for the instance.
- Assign the intended static port.
- Restart the instance.
- Verify that the data, log, and backup directories match the current host's
SQL topology row in
$global:settings.
Display the authoritative values for the current host:
$hostName = $env:COMPUTERNAME.ToLowerInvariant()
$topology = $global:settings[$global:configRootKeys['SqlInstanceTopologyConfigRootKey']]
$hosts = $topology[$global:configRootKeys['SqlInstanceTopologyHostsConfigRootKey']]
$instances = $hosts[$hostName][$global:configRootKeys['SqlInstanceTopologyInstancesConfigRootKey']]
$instances.Values | Select-Object InstanceName, DataPath, LogPath, BackupPath, TcpPort
Every instance follows this settings-backed convention:
- Data:
C:\LocalDBs\<INSTANCE_NAME>\Data\ - Logs:
C:\LocalDBs\<INSTANCE_NAME>\Log\ - Backups:
C:\LocalDBs\<INSTANCE_NAME>\Backup\
Install-SqlServerInstance resolves these values from $global:settings and passes
them to dbatools. Do not reproduce the paths as independent literals in setup scripts.
Verify TCP connectivity:
sqlcmd -S 'localhost\Production' -E -Q 'SELECT @@SERVERNAME, @@VERSION' -C
sqlcmd -S 'localhost\QA' -E -Q 'SELECT @@SERVERNAME' -C
sqlcmd -S 'localhost\Integration' -E -Q 'SELECT @@SERVERNAME' -C
6.6 Cap max server memory on every instance
SQL Server ships with max server memory (MB) set to 2147483647, which means
unlimited. A workstation runs several instances alongside builds, AI agent
processes, and Dropbox, so uncapped instances compete with each other and with the
OS until every instance raises error 701 at once. Cap every instance immediately
after creating it.
The standing allocation for a 32 GB host:
| Instance | Cap (MB) | Rationale |
|---|---|---|
Production | 7936 | Hosts the ProGet and BuildMaster databases; the only instance under sustained real load. |
QA | 1792 | Promotion target; intermittent load. |
Integration | 1024 | Integration runs only. |
Dev<user> | 768 | Per-developer scratch. |
Exp<user> | 768 | Per-developer experimental. |
| Total | 12288 | 12 GB of 32 GB, leaving headroom for the OS, builds, and agent processes. |
Scale proportionally on hosts with different physical memory, keeping the total at
roughly one third of RAM and leaving Production the clear majority.
Apply the caps:
$caps = [ordered]@{
Production = 7936
QA = 1792
Integration = 1024
"Dev$($env:USERNAME)" = 768
"Exp$($env:USERNAME)" = 768
}
foreach ($instance in $caps.Keys) {
$mb = $caps[$instance]
$tsql = @"
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'max server memory (MB)', $mb; RECONFIGURE;
"@
sqlcmd -S "localhost\$instance" -E -C -Q $tsql
Write-Output " $instance capped at $mb MB"
}
No restart required.
max server memoryis a dynamic setting (sys.configurations.is_dynamic = 1); it takes effect immediately.
Verify the applied values and the host-wide total:
$rows = foreach ($instance in @('Production','QA','Integration',"Dev$($env:USERNAME)","Exp$($env:USERNAME)")) {
$c = New-Object System.Data.SqlClient.SqlConnection "Server=localhost\$instance;Integrated Security=True;Connect Timeout=10;TrustServerCertificate=True"
$c.Open()
$cmd = $c.CreateCommand()
$cmd.CommandText = @'
SELECT MaxMB = (SELECT CONVERT(bigint, value_in_use) FROM sys.configurations WHERE name = 'max server memory (MB)'),
UsedMB = (SELECT physical_memory_in_use_kb / 1024 FROM sys.dm_os_process_memory)
'@
$r = $cmd.ExecuteReader()
while ($r.Read()) { [pscustomobject] @{ Instance = $instance; MaxMB = $r['MaxMB']; UsedMB = $r['UsedMB'] } }
$c.Close()
}
$rows | Format-Table -AutoSize
'TOTAL capped: {0} MB' -f ($rows.MaxMB | Measure-Object -Sum).Sum
A value of 2147483647 in the MaxMB column means that instance was missed.
If Production later shows sustained memory pressure — error 701, or a page life
expectancy trending toward single digits — raise its cap before any other
instance's:
$c = New-Object System.Data.SqlClient.SqlConnection 'Server=localhost\Production;Integrated Security=True;TrustServerCertificate=True'
$c.Open()
$cmd = $c.CreateCommand()
$cmd.CommandText = @'
SELECT counter_name, cntr_value FROM sys.dm_os_performance_counters
WHERE counter_name IN ('Page life expectancy', 'Total Server Memory (KB)', 'Target Server Memory (KB)')
'@
$r = $cmd.ExecuteReader()
while ($r.Read()) { '{0,-28} {1}' -f $r[0].Trim(), $r[1] }
$c.Close()
Declare the caps in the parity journal so the peer host receives the same allocation:
Import-Module ATAP.Utilities.SystemParityMonitor.PowerShell
Add-ParityChangeEntry `
-Category SQL `
-Item 'max server memory (MB) - all local instances' `
-OldValue 'uncapped (2147483647 MB default)' `
-NewValue 'capped, 12288 MB total: Production 7936, QA 1792, Integration 1024, Dev 768, Exp 768' `
-PeerHostName 'utat01' `
-PeerActionKind ConfigureService `
-PeerAction 'Apply the same max server memory caps to the corresponding local SQL instances. Setting is dynamic; no restart required.' `
-Reason 'Uncapped instances compete for RAM and raise error 701 together under memory pressure.'
Step 7: Create Sprint and Feature-Branch Developer Instances
The permanent instances are Production, QA, and Integration. Developer and
experimental instances are per-user or per-feature and are created separately.
Every new instance must be capped. Instances created here arrive at the SQL default of unlimited. A single uncapped sprint instance can consume the headroom reserved for all the others. Apply the 1024 MB default cap in 7.1 as part of creating the instance, not as a later cleanup step.
For the active developer:
Import-Module ATAP.Utilities.BuildTooling.PowerShell
New-SprintSqlServerInstances `
-InstanceNames @("Dev$($env:USERNAME)", "Exp$($env:USERNAME)") `
-Databases @('ATAPUtilities', 'AceCommander') `
-DatabaseHost 'localhost' `
-ConnectionMethod 'tcp'
7.1 Apply the default memory cap to every new instance
Default cap for any instance created in this step: 1024 MB. Run this
immediately after New-SprintSqlServerInstances returns, and after creating any
ad-hoc sprint or feature-branch instance.
# Default cap for newly created sprint / feature-branch instances
$DefaultInstanceCapMB = 1024
$newInstances = @("Dev$($env:USERNAME)", "Exp$($env:USERNAME)") # add feature-branch instances here
foreach ($instance in $newInstances) {
$tsql = @"
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'max server memory (MB)', $DefaultInstanceCapMB; RECONFIGURE;
"@
sqlcmd -S "localhost\$instance" -E -C -Q $tsql
Write-Output " $instance capped at $DefaultInstanceCapMB MB"
}
Sweep for any instance still at the unlimited default — this catches instances created outside this runbook:
Get-Service -Name 'MSSQL$*' |
Where-Object Status -eq 'Running' |
ForEach-Object {
$instance = $_.Name -replace '^MSSQL\$', ''
try {
$c = New-Object System.Data.SqlClient.SqlConnection "Server=localhost\$instance;Integrated Security=True;Connect Timeout=10;TrustServerCertificate=True"
$c.Open()
$cmd = $c.CreateCommand()
$cmd.CommandText = "SELECT CONVERT(bigint, value_in_use) FROM sys.configurations WHERE name = 'max server memory (MB)'"
$maxMb = $cmd.ExecuteScalar()
$c.Close()
[pscustomobject] @{
Instance = $instance
MaxMB = $maxMb
Status = if ($maxMb -eq 2147483647) { 'UNCAPPED - fix' } else { 'ok' }
}
} catch {
[pscustomobject] @{ Instance = $instance; MaxMB = 'unreachable'; Status = $_.Exception.Message }
}
} | Format-Table -AutoSize
Raise a sprint instance above 1024 MB only when a specific workload requires it,
and subtract the increase from the host's remaining headroom rather than letting the
total climb past the budget in 6.6.
Notes:
New-SprintSqlServerInstancescreates only theDev...andExp...instances. It does not createProduction,QA, orIntegration.- For long-lived multi-sprint feature branches, combine a short feature token with the tier prefix and keep the full instance name under 16 characters.
- Use the current
Overview.code-workspaceandOverview.Sprint.NNNN.code-workspacefiles as the branch matrix when deciding which extra instances are still required. - Sprint instances are torn down at sprint end. Removing them returns their capped memory to the host budget; no cap adjustment is needed elsewhere when they go away.
Step 8: Build the Databases on All Instances
Classify the five roles before running any database command:
DevwhertzingandExpwhertzingare permanent developer instances managed by the sprint lifecycle and may be rebuilt from Flyway under that contract.IntegrationandQAare ecosystem tiers with their own verification and reset policy.Productionis protected data. Do not rebuild, seed, copy, restore, or reset it merely because a developer instance was created. Task 13.60 owns the protection/seed policy.
Run the database rebuild script only for roles whose current policy explicitly authorizes a rebuild:
Push-Location 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities'
.\Database\Powershell\public\Rebuild-All-AllInstances.ps1
Pop-Location
This script should be treated as a temporary bootstrap script until it is converted into a module cmdlet. Review its resolved target list before execution. It is not authorization to build or seed Production, and it must not collapse the five roles into "two instances total."
Step 9: Install and Configure ProGet and BuildMaster
Group all tooling setup under this section. ProGet and BuildMaster both use the local
Production SQL Server instance and Windows integrated security.
The two Inedo products are independent per host. ProGet is canonical on port 50000 and
BuildMaster on port 50017; port 8600 is obsolete. Never copy or merge an Inedo database
between utat01 and utat022. All service placement and derived URLs come from the single
ServicePlacementMap; do not edit derived endpoint keys individually.
ProGet readiness includes the per-host license state, Storage.PackagesRootPath readback as
C:\ProgramData\ProGet\Packages, SvcProGet ACLs, the canonical 15-feed inventory, explicit
connector-free mapping, and a harmless publish/download/restore proof. The approved local HTTP
NuGet source may set allowInsecureConnections=true; public sources remain HTTPS. Resolve the
host-specific administrative SecretName only at the authenticated-operation boundary and never
record the value.
BuildMaster readiness includes the host-specific administrative SecretName, the approved
ATAP.Utilities-CSharp and ATAP.Utilities-PowerShell application definitions, required
non-secret variables, default raft, and full Assert-BuildMasterReady outcome. The supported
server ArtifactUsage values are FileSystem, AssetDirectory, and None; do not send
Default. The direct API fallback is a verified workaround only. Optional/null-field handling,
response detail, idempotent create/update tests, release, and installation remain Task 13.20.j.
9.1 Install ProGet and BuildMaster from Inedo Hub
- Download and run Inedo Hub.
- Install ProGet first.
- Install BuildMaster second.
- For both products, use a connection string targeting
localhost\Productionwith integrated security.
9.2 Grant the service accounts database rights
Import-Module ATAP.Utilities.PowerShell
Initialize-SqlServiceLogin `
-SqlInstance 'localhost\Production' `
-DatabaseName 'ProGet' `
-ServiceAccount "$env:COMPUTERNAME\SvcProGet" `
-Encrypt Optional `
-TrustServerCertificate
Initialize-SqlServiceLogin `
-SqlInstance 'localhost\Production' `
-DatabaseName 'BuildMaster' `
-ServiceAccount "$env:COMPUTERNAME\SvcBuildmaster" `
-Encrypt Optional `
-TrustServerCertificate
9.2.1 Grant SvcBuildMaster database-package deployment rights
The preceding BuildMaster product-database grant is not sufficient for database
package deployment. The BuildMaster service identity also performs tier rehearsal,
pre-migration backup, and Flyway DDL/DML apply operations. Grant the local
SvcBuildMaster identity db_owner on the ATAPUtilities database in every
authorized tier instance.
The logical Experimental tier targets Exp<DeveloperName>. Never create or grant
against a generic permanent Experimental instance.
Run this idempotent procedure in an elevated, profile-loaded PowerShell session after the five databases exist:
Import-Module ATAP.Utilities.PowerShell
$developerName = $env:USERNAME
$serviceAccount = "$env:COMPUTERNAME\SvcBuildMaster"
$databaseTierInstances = @(
"Exp$developerName"
"Dev$developerName"
'Integration'
'QA'
'Production'
)
foreach ($instanceName in $databaseTierInstances) {
Initialize-SqlServiceLogin `
-SqlInstance "localhost\$instanceName" `
-DatabaseName 'ATAPUtilities' `
-ServiceAccount $serviceAccount `
-Encrypt Optional `
-TrustServerCertificate
}
Verify the server login, database user, and db_owner membership independently:
$principalLiteral = $serviceAccount.Replace("'", "''")
$verificationQuery = @"
DECLARE @principal sysname = N'$principalLiteral';
SELECT
CAST(SERVERPROPERTY('MachineName') AS nvarchar(128)) AS MachineName,
CAST(SERVERPROPERTY('InstanceName') AS nvarchar(128)) AS InstanceName,
DB_NAME() AS DatabaseName,
@principal AS AccountName,
IIF(SUSER_ID(@principal) IS NULL, 0, 1) AS ServerLoginExists,
IIF(USER_ID(@principal) IS NULL, 0, 1) AS DatabaseUserExists,
ISNULL(IS_ROLEMEMBER(N'db_owner', @principal), 0) AS IsDbOwner;
"@
$grantAudit = foreach ($instanceName in $databaseTierInstances) {
Invoke-Sqlcmd `
-ServerInstance "localhost\$instanceName" `
-Database 'ATAPUtilities' `
-Query $verificationQuery `
-TrustServerCertificate
}
$grantAudit |
Select-Object MachineName, InstanceName, DatabaseName, AccountName,
ServerLoginExists, DatabaseUserExists, IsDbOwner
if ($grantAudit.Where({
$_.ServerLoginExists -ne 1 -or
$_.DatabaseUserExists -ne 1 -or
$_.IsDbOwner -ne 1
}).Count -gt 0) {
throw 'SvcBuildMaster ATAPUtilities database permission verification failed.'
}
Record this machine-state grant with Add-ParityChangeEntry. The peer action must
repeat the same idempotent procedure using the peer-local
<PeerHost>\SvcBuildMaster identity, then acknowledge the entry only after the
independent query passes on all five authorized tier databases. Do not copy or
restore an application or Inedo database to establish parity.
9.3 Reconfigure the Windows services to use the dedicated accounts
Import-Module ATAP.Utilities.PowerShell
$proGetPassword = Get-SecretATAP -SecretName "$env:COMPUTERNAME-SvcProGet-Production" -SecretField 'password'
$bmPassword = Get-SecretATAP -SecretName "$env:COMPUTERNAME-SvcBuildmaster-Production" -SecretField 'password'
$proGetCredential = New-Object System.Management.Automation.PSCredential(
"$env:COMPUTERNAME\SvcProGet",
(ConvertTo-SecureString $proGetPassword -AsPlainText -Force)
)
$buildMasterCredential = New-Object System.Management.Automation.PSCredential(
"$env:COMPUTERNAME\SvcBuildmaster",
(ConvertTo-SecureString $bmPassword -AsPlainText -Force)
)
Set-ServiceLogonAccount -ServiceName 'INEDOPROGETSVC' -Credential $proGetCredential
Set-ServiceLogonAccount -ServiceName 'INEDOBMSVC' -Credential $buildMasterCredential
Set-InedoServicesDependency
Restart-Service INEDOPROGETSVC, INEDOBMSVC
Get-Service INEDOPROGETSVC, INEDOBMSVC | Select-Object Name, Status, StartType
Both services must depend on MSSQL$PRODUCTION so SQL Server is fully available before
either Inedo product starts.
9.3.1 Grant SvcProGet full control over the ProGet package store
When ProGet runs as the custom local service account SvcProGet (rather than the default
NetworkService identity), the Inedo Hub installer does not automatically grant that
account write access to C:\ProgramData\ProGet\Packages\. ProGet UI operations that delete
or write package files (e.g., bulk feed delete) then return HTTP 500 with:
Access to the path 'C:\ProgramData\ProGet\Packages\...' is denied.
Grant SvcProGet Full Control (object-inherit + container-inherit) over the package
directory. Run in an elevated shell:
icacls "C:\ProgramData\ProGet\Packages" /grant "SvcProGet:(OI)(CI)F" /T
Automation:
Invoke-ProvisionInedoServiceAccounts.ps1performs this step automatically after creating theSvcProGetaccount. Run it instead of the manual command when provisioning a new machine.
9.4 Provision approved Bitwarden Secrets Manager ReadOnly identities
This is the only supported non-interactive secret bootstrap. Password Manager
bw/BW_SESSION is interactive-user-only and must never be provisioned to or inherited by
a Windows service. The retired service-account Password Manager procedure is available in
Git history only and is non-executable.
The approved automation allowlist is exact: SvcBuildMaster, SvcProGet, and
SvcSQLServer, each scoped to project CI-Shared with purpose ReadOnly. SvcSeq,
SvcParityAudit, and ansibleAdmin receive no BWS token. ReadWrite is approved only for
utat022\whertzing and is not a fallback for any service account. Creating accounts,
certificates, Bitwarden grants, tokens, logon rights, or scheduled tasks is a separate HITL
operation under Tasks 13.40–13.43.
9.4.1 Preconditions and dry run
- Install BuildTooling 0.1.44 or later from
powershellget-stableat AllUsers scope and verify a fresh shell imports that exact version fromC:\Program Files\PowerShell\Modules. - Confirm the service account, its Password logon credential, its document-encryption
certificate/private key, and its
CI-SharedReadOnly machine-account grant were created under explicit operator authority. - Resolve the current repository root and host at run time. Do not paste a sprint worktree path or a suffixless infrastructure SecretName into this procedure.
- Run both cmdlets with
-WhatIf. RequireProjectName=CI-Shared,TokenPurpose=ReadOnly, the intended account, and no state change.
$repositoryRoot = git rev-parse --show-toplevel
Import-Module (Join-Path $repositoryRoot 'src\ATAP.Utilities.BuildTooling.PowerShell\ATAP.Utilities.BuildTooling.PowerShell.psd1') -Force
$accountName = '.\SvcProGet' # repeat separately for each approved account
$token = Read-Host 'CI-Shared ReadOnly BWS token' -AsSecureString
$envelope = Join-Path $env:TEMP 'ATAP-BWS-ReadOnly-bootstrap.cms'
$certificatePath = Join-Path $env:TEMP 'SvcProGet.cer'
New-BWSReadOnlyBootstrapEnvelope `
-AccountName $accountName `
-AccessToken $token `
-RecipientCertificatePath $certificatePath `
-OutputPath $envelope `
-WhatIf
The public .cer must have document-encryption EKU and a simple name exactly matching the
approved account. The envelope contains no plaintext token and is bound to that account's
certificate. Never put a token in an argument, environment variable, transcript, log, or
evidence artifact.
9.4.2 Execute the bounded bootstrap
After reviewing the dry run, create the CMS envelope and invoke the bounded one-shot
Password-logon scheduled task. The service logon credential identity must exactly match
-AccountName.
$serviceLogonCredential = Get-Credential -UserName $accountName
$envelopeResult = New-BWSReadOnlyBootstrapEnvelope `
-AccountName $accountName `
-AccessToken $token `
-RecipientCertificatePath $certificatePath `
-OutputPath $envelope
$bootstrap = Invoke-BWSReadOnlyTokenBootstrap `
-AccountName $accountName `
-ServiceLogonCredential $serviceLogonCredential `
-EnvelopePath $envelopeResult.EnvelopePath `
-CertificateThumbprint $envelopeResult.CertificateThumbprint
Remove-Item -LiteralPath $envelopeResult.EnvelopePath -Force
$serviceLogonCredential = $null
$token = $null
The automation uses the fixed \ATAP\ task path, waits for bounded completion, verifies the
canonical account-bound DPAPI ReadOnly file, and removes the temporary task. Re-running
without -Force returns the existing-state result without overwriting it; use -Force only
for an operator-reviewed repair. A failure stops and verifies task removal before deleting
the envelope; a cleanup failure is an operator gate, not permission to continue.
9.4.3 Validate without exposing secret material
Run validation as the owning account in a fresh profile-enabled shell:
$credential = Get-BWSAccessToken -TokenPurpose ReadOnly
if ($credential.UserName -ne 'BWS_ACCESS_TOKEN') {
throw 'The canonical ReadOnly DPAPI slot did not return the expected credential shape.'
}
Get-SecretATAP -SecretName '<approved-CI-Shared-secret-name>' | Out-Null
Record only account, host, project, purpose, module version/path, operation ID, redacted status, and the DPAPI file path/hash. Do not record the credential value, BWS output, CMS contents, task XML, runtime environment, transcript, cache, or history. Repeat independently for all three approved accounts and both hosts; DPAPI files and envelopes are never copied between accounts or hosts.
9.5 Bootstrap exact Git trust for the BuildMaster service account
When a BuildMaster build agent runs as the BuildMaster service account
(SvcBuildmaster on utat022) against a worktree under
C:\Dropbox\whertzing\GitHub\, git refuses to operate on the directory
because it was created by a different user (the interactive developer
account that owns the Dropbox sync). Symptom:
fatal: detected dubious ownership in repository at 'C:/Dropbox/whertzing/GitHub/...'
This breaks NBGV height computation, sourcelink commit-id resolution, and
every Get-BuildContext call that shells out to git. It typically
surfaces first during dotnet restore because of the
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> block at
Directory.Build.props:36, which triggers an NBGV evaluation that needs
git.
Run as SvcBuildmaster itself, not as the interactive developer. Add only the resolved
repository and worktree paths that this identity is authorized to build. Task 13.5 owns the
idempotent ownership/trust automation; until that automation is deployed, review each exact
path before adding it.
# Elevated pwsh running as SvcBuildmaster; repeat for each reviewed exact path.
$authorizedWorktree = 'C:/Dropbox/whertzing/GitHub/ATAP.Utilities'
git config --global --add safe.directory $authorizedWorktree
Verify:
git config --global --get-all safe.directory
Critical notes:
- Run as
SvcBuildmaster, not as your interactive login. This entry lives in the running user's~/.gitconfig. Running it from your developer account writes to the wrong user's gitconfig and the dubious ownership error persists. - Never trust the parent GitHub directory. Parent-wide trust silently authorizes current and future repositories that were not reviewed.
- Wildcards are not supported or acceptable here. Add each authorized repository or worktree as an exact canonical path, and remove obsolete entries during return cleanup.
A common way to obtain a SvcBuildmaster shell from an admin login:
$bmPassword = Get-SecretATAP -SecretName "$env:COMPUTERNAME-SvcBuildmaster-Production" -SecretField 'password'
$bmCred = New-Object System.Management.Automation.PSCredential(
"$env:COMPUTERNAME\SvcBuildmaster",
(ConvertTo-SecureString $bmPassword -AsPlainText -Force)
)
Start-Process pwsh -Credential $bmCred -ArgumentList '-NoExit', '-Command',
'git config --global --add safe.directory C:/Dropbox/whertzing/GitHub/ATAP.Utilities; git config --global --get-all safe.directory'
Acceptance: the exact authorized paths appear in safe.directory, no parent-wide entry is
present, and a fresh BuildMaster build under SvcBuildmaster completes dotnet restore and
Get-BuildContext without a dubious ownership error.
9.6 Keep the config files under source control
Use the IAC repo copies of ProGet.config and BuildMaster.config and link them into
C:\ProgramData\Inedo\SharedConfig when that machine is the authoritative host.
9.7 Assign the Git raft to each BuildMaster application
When configuring each BuildMaster application to read plans/monitors/scripts from Git, the raft is assigned in this UI location:
- Application → Settings → Advanced → Artifact & Component Hosting
In that dialog, choose the intended raft in the Raft dropdown and save the application settings.
9.8 Register the ProGet powershellget-stable feed and install ATAP modules
ProGet is now running with the powershellget-stable NuGetV2 feed provisioned. This
step registers that feed as a trusted PSRepository before PSGallery in the
repository priority list, then installs ATAP.Utilities.PowerShell and
ATAP.Utilities.BuildTooling.PowerShell globally (-Scope AllUsers).
Why ordering matters.
Get-PSRepositoryreturns repositories in registration order andFind-Module(without-Repository) searches them in that order. IfPSGalleryappears beforepowershellget-stable, PowerShell will resolve the public registry first and may pick up a stale or wrong version. The step below ensures the internal feed is consulted first by re-registering PSGallery afterpowershellget-stablewhen necessary, or by always passing-Repository powershellget-stableexplicitly.
Run from an elevated PowerShell 7 session. The script is already in the sprint worktree.
# Dot-source the function and invoke it
. 'C:\Dropbox\whertzing\GitHub\<active-ATAP.Utilities-worktree>\src\_AdminRequiresHoldingPen\ATAP.Utilities.PowerShell\public\Install-ATAPModulesFromProGet.ps1'
Install-ATAPModulesFromProGet
Expected output (module base paths confirm -Scope AllUsers):
[HH:mm:ss][Install-ATAPModulesFromProGet] Feed URI: http://localhost:50000/nuget/powershellget-stable/
[HH:mm:ss][Install-ATAPModulesFromProGet] Feed 'powershellget-stable' is reachable at http://localhost:50000/nuget/powershellget-stable/
[HH:mm:ss][Install-ATAPModulesFromProGet] Installed 'ATAP.Utilities.PowerShell' v0.1.0 to 'C:\Program Files\PowerShell\Modules\ATAP.Utilities.Powershell\0.1.0'
[HH:mm:ss][Install-ATAPModulesFromProGet] Installed 'ATAP.Utilities.BuildTooling.PowerShell' v0.1.0 to 'C:\Program Files\PowerShell\Modules\ATAP.Utilities.BuildTooling.PowerShell\0.1.0'
ModuleName VersionInstalled InstallResult
---------- ---------------- -------------
ATAP.Utilities.PowerShell 0.1.0 Installed
ATAP.Utilities.BuildTooling.PowerShell 0.1.0 Installed
Verify the ModuleBase column shows C:\Program Files\PowerShell\Modules\..., not a
per-user path. If it shows a per-user path, the shell was not elevated — uninstall the
per-user copy and re-run elevated.
If powershellget-stable appears after PSGallery in the repository list, the script
logs a warning but still succeeds because it passes -Repository powershellget-stable
explicitly. To fix the ordering permanently, unregister and re-register the repositories
so the internal feed is first:
# Fix repository ordering: internal feed first, PSGallery second.
# Run elevated.
Unregister-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
Unregister-PSRepository -Name powershellget-stable -ErrorAction SilentlyContinue
Register-PSRepository `
-Name 'powershellget-stable' `
-SourceLocation 'http://localhost:50000/nuget/powershellget-stable/' `
-PublishLocation 'http://localhost:50000/nuget/powershellget-stable/' `
-InstallationPolicy Trusted
Register-PSRepository `
-Default `
-InstallationPolicy Trusted
Get-PSRepository | Select-Object Name, SourceLocation, InstallationPolicy
After re-ordering, Get-PSRepository should list powershellget-stable first.
9.9 Register all ProGet PowerShell feeds for the current user
Step 9.8 registers only the powershellget-stable feed needed to install the ATAP modules.
Registering the full set of tiered feeds is a one-time per Windows user profile per
machine consumer setup step. Re-run it only when:
- setting up a new workstation or a new Windows user profile;
- changing the ProGet host, port, or feed names;
- repairing a profile where PowerShellGet/PSResourceGet repository state was reset.
.NET/NuGet feed sources are normally checked into repo-level NuGet.Config files, but
PowerShell module installation keeps repository registrations in the current user's
package-provider state. Register both PowerShell repository stores so either
Install-Module (PowerShellGet v2) or Install-PSResource
(Microsoft.PowerShell.PSResourceGet) can consume the same ProGet feeds.
Run in a normal PowerShell 7 session for the user who will install modules:
$feeds = @(
@{ Name = 'powershellget-experimental'; Uri = 'http://localhost:50000/nuget/powershellget-experimental/' },
@{ Name = 'powershellget-development'; Uri = 'http://localhost:50000/nuget/powershellget-development/' },
@{ Name = 'powershellget-integration'; Uri = 'http://localhost:50000/nuget/powershellget-integration/' },
@{ Name = 'powershellget-qa'; Uri = 'http://localhost:50000/nuget/powershellget-qa/' },
@{ Name = 'powershellget-stable'; Uri = 'http://localhost:50000/nuget/powershellget-stable/' }
)
foreach ($feed in $feeds) {
$repo = Get-PSRepository -Name $feed.Name -ErrorAction SilentlyContinue
if ($null -eq $repo) {
Register-PSRepository `
-Name $feed.Name `
-SourceLocation $feed.Uri `
-PublishLocation $feed.Uri `
-InstallationPolicy Trusted
}
else {
Set-PSRepository `
-Name $feed.Name `
-SourceLocation $feed.Uri `
-PublishLocation $feed.Uri `
-InstallationPolicy Trusted
}
$resourceUri = "$($feed.Uri.TrimEnd('/'))/v2"
$resourceRepo = Get-PSResourceRepository -Name $feed.Name -ErrorAction SilentlyContinue
if ($null -eq $resourceRepo) {
Register-PSResourceRepository -Name $feed.Name -Uri $resourceUri -Trusted
}
else {
Set-PSResourceRepository -Name $feed.Name -Uri $resourceUri -Trusted
}
}
Verify:
Get-PSRepository |
Where-Object Name -like 'powershellget-*' |
Sort-Object Name |
Select-Object Name, SourceLocation, InstallationPolicy
Get-PSResourceRepository |
Where-Object Name -like 'powershellget-*' |
Sort-Object Name |
Select-Object Name, Uri, Trusted
Expected PowerShell feeds: powershellget-experimental, powershellget-development,
powershellget-integration, powershellget-qa, powershellget-stable.
The matching NuGet package feeds for .NET restore (nuget-experimental,
nuget-development, nuget-integration, nuget-qa, nuget-stable) should already appear
in the repo NuGet.Config files. Confirm from a repo root with:
dotnet nuget list source
9.10 Configure SystemParityMonitor on Windows 10 and Windows 11
Parity monitoring is a host-pair service, not merely a module import. The complete installation, registration, first-run, rollback, and troubleshooting procedure is intended to ship with the module at:
$moduleRoot\Documentation\InstallationAndTroubleshooting.md
Version 0.1.1 omitted Documentation\; until SC-0264 is implemented, use the
canonical source copy under
src\ATAP.Utilities.SystemParityMonitor.PowerShell\Documentation.
An administrator must complete this checklist on each participating host:
- Install the exact promoted
ATAP.Utilities.SystemParityMonitor.PowerShellversion for AllUsers. Resolve$moduleRootbelowC:\Program Files\PowerShell\Modules\ATAP.Utilities.SystemParityMonitor.PowerShell. - Verify the installed package contains its
scripts\andDocumentation\folders. A package missing either folder is not deployment-complete. The live0.1.1installation required a temporary manualscripts\copy on both hosts; SC-0264 owns the reproducible package fix, and source documentation remains authoritative until a corrected immutable version ships. - Create and enable the non-administrative
SvcParityAuditaccount. Do not grant local Administrator membership to work around scheduled-task failures. - Create and ACL
C:\ProgramData\ATAP\ParityState, its SMB share, journals, acknowledgements, snapshots, whitelist, and task-results folders as specified by the parity runbook. - Create the
\ATAPTask Scheduler folder. - Grant
SvcParityAuditSeBatchLogonRightwhile preserving every existing right holder. Confirm neither the account nor one of its governing policies assignsSeDenyBatchLogonRight. The guardedseceditpattern in §9.4.6.5 applies; addSvcParityAuditto the SID list. - As
SvcParityAuditon that same host, provision and validate theCommonCIForBitwardenReadOnlyDPAPI token. Never copy the token from another host or account, and never usebw/BW_SESSIONin the scheduled path. - Register
AuditAndCompareon the primary host with Password logon when peer SMB access needs reusable credentials. RegisterAuditOnlyon the peer with S4U and a limited run level. When the elevated administrator and S4U run-as identities differ, supply the run-as credential during registration; Task Scheduler uses it to authorize registration while preserving S4U/no stored password in the task. - Run the peer audit, primary audit, and primary comparison in that order; require successful task-result JSON, fresh snapshots, a drift report, and no secret values in evidence.
- Keep Daily cadence for the first clean month, then re-register as BiWeekly.
Windows 10 can require the inbox ScheduledTasks manifest to be imported by full path
from Windows PowerShell 5.1. The observed utat01 endpoint omitted
C:\Windows\System32\WindowsPowerShell\v1.0\Modules from PSModulePath, which also
prevented normal autoload of Microsoft.PowerShell.Utility and PowerShellGet. Use
WinRM with -ConfigurationName Microsoft.PowerShell and absolute manifest paths for
the current recovery. SC-0266 tracks restoring a complete PSModulePath and adding a
PowerShell 7 (pwsh) endpoint; WinRM profile loading is separate work. Windows 11
normally exposes the cmdlets directly. The module runbook records the exact
compatibility checks and these live findings:
- the registration script must be dot-sourced before calling
Register-ParityScheduledTasks; invoking the file with&intentionally performs no registration; Get-Module -ListAvailablein Windows PowerShell 5.1 may not enumerate a module whose manifest requires PowerShell 7, so discover$moduleRootfrom the installed filesystem path;- a PSFramework success message after
Register-ScheduledTaskreports an error is not proof; verify withGet-ScheduledTaskand force registration errors to stop; - version
0.1.1fixed the historical Highest-run-level and false-success-log defects, but its S4U branch still omits the different run-as account's registration credential.LOGON32_LOGON_BATCHproved the account right is effective; COM and cmdlet registration without a credential returned0x80070005, while credential- backed COM registration succeeded and saved S4U (2) with Limited run level (0); - do not infer that a batch right is absent merely because
seceditexports the local account by name rather than literal SID; use an actual batch-logon test; Find-ModuleandInstall-Modulecan resolve the stable NuGet v2 feed even whenSave-PSResourcereturns 404 against itsFindPackagesByIdrequest. Treat that as a promoted-test-runner/feed-protocol defect, not proof that the package is absent.
9.10a Verify Inedo service reachability by hostname (IPv4 and IPv6)
Run this on every host that runs or calls BuildMaster/ProGet, including utat022.
BuildMaster and ProGet already bind all IPv4 and IPv6 addresses — their
Urls="http://*:PORT/" configuration is correct and must not be changed. The failure this
step catches is a host networking problem: if the machine advertises global IPv6
addresses that are not actually routable, clients resolving the hostname try those first
and hang, producing 30-second timeouts that look like a broken service.
# 1. Confirm BOTH families are listening. -p tcp prints ONLY the IPv4 table, so a service
# can look "IPv4-only" when it is not. Always check tcpv6 as well.
netstat -ano -p tcp | Select-String ':50017|:50000'
netstat -ano -p tcpv6 | Select-String ':50017|:50000' # expect [::]:50017 and [::]:50000
# 2. Connect to every address the hostname resolves to. Any TIMEOUT row is the defect.
$addrs = [System.Net.Dns]::GetHostAddresses($env:COMPUTERNAME)
foreach ($a in $addrs) {
$ip = $a.IPAddressToString
$c = [System.Net.Sockets.TcpClient]::new()
try {
$ok = $c.ConnectAsync($ip, 50017).Wait(4000)
'{0,-46} {1}' -f $ip, $(if ($ok) { 'CONNECT' } else { 'TIMEOUT' })
} catch { '{0,-46} FAILED' -f $ip } finally { $c.Dispose() }
}
# 3. If a global IPv6 address timed out, test whether it works at all:
ping -6 -n 2 <that-address> # "General failure" => the address is dead, not the port
If global IPv6 addresses are dead, fix the host networking — do not edit
BuildMaster.config, ProGet.config, or ServicePlacementMap:
-
Preferred: fix router/ISP DHCPv6 so the advertised prefix is routable, then re-test.
-
If IPv6 is not used on this network, disable IPv6 on the adapter, or prefer IPv4 in address selection (elevated):
netsh interface ipv6 set prefixpolicy ::ffff:0:0/96 60 4 -
Interim: use
localhost/127.0.0.1for host-local calls, and pass-BuildMasterBaseUrl 'http://localhost:50017/'to the release cmdlets.
Do not change
ServicePlacementMap['BuildMaster']tolocalhost. The BuildMaster base URL is derived from it, and so is the host-suffixed admin SecretName (BuildMaster.Admin.API.Key.<service-host>). Repointing the map silently rewrites the SecretName to one that does not exist in Bitwarden, turning a slow timeout into a hard credential failure.
Record the outcome with Add-ParityChangeEntry so the peer host shows a declared change
rather than undeclared drift.
9.11 Junction AI agent memory to the Dropbox store
AI agent memory for the ATAP repositories is stored under Dropbox, outside every git repository, so it survives sprint end and is available on every host:
C:\Dropbox\whertzing\ATAP\AIAgentMemory\<RepoName>\
Claude Code does not read that path directly. It resolves memory under
%USERPROFILE%\.claude\projects\<slug>\memory, where <slug> is the repository path with
the drive letter lowercased and : \ _ . replaced by -. Each host therefore needs
NTFS junctions from the expected slug paths to the Dropbox store. Junctions are
host-local and do not sync, so this step is required on every new computer even though the
memory content arrives via Dropbox.
Two junctions are needed per repository, because Claude Code and the checkpoint tooling resolve different slugs:
- Main-repo slug — Claude Code derives it from
git rev-parse --git-common-dir, so for a worktree it resolves to the main repository. This is where Claude Code reads and writes memory, which is why memory is shared across all worktrees of a repo. - Sprint-worktree slug —
Save-SprintWorkSessionderives its path from the transcript slug, which is the worktree. This is where/checkpointlooks for memory to archive.
# Directory junctions do NOT require elevation.
$projects = Join-Path $env:USERPROFILE '.claude\projects'
$target = 'C:\Dropbox\whertzing\ATAP\AIAgentMemory\ATAP.Utilities'
New-Item -ItemType Directory -Path $target -Force | Out-Null
# Repeat for each repo and active sprint worktree. Replace the symbolic
# worktree path with the current sprint path; never pin a sprint number here.
$repositoryPaths = @(
'C:\Dropbox\whertzing\GitHub\ATAP.Utilities'
'C:\Dropbox\whertzing\GitHub\<active-ATAP.Utilities-worktree>'
)
$slugs = $repositoryPaths | ForEach-Object {
$_ -replace '[:\\_.]', '-'
}
foreach ($slug in $slugs) {
$projDir = Join-Path $projects $slug
New-Item -ItemType Directory -Path $projDir -Force | Out-Null
$mem = Join-Path $projDir 'memory'
if (Test-Path $mem) {
if ((Get-Item $mem).LinkType) { Remove-Item -LiteralPath $mem -Force }
else { throw "Refusing to clobber real directory: $mem" }
}
New-Item -ItemType Junction -Path $mem -Target $target | Out-Null
}
# Verify: both must report LinkType 'Junction' and the same target.
foreach ($slug in $slugs) {
Get-Item (Join-Path $projects "$slug\memory") | Select-Object FullName, LinkType, Target
}
Verify end to end by running /checkpoint and confirming the roster entry reports
MemorySnapshotCreated: true with a non-zero MemoryFileCount.
Two things to know:
- The worktree-slug junction is sprint-scoped, but from SprintLifecycle
0.1.10onward it is created automatically:Set-SprintBoundaryContext -Boundary Startprovisions it for every worktree, and bothNew-SprintStage1andNew-SprintStage2delegate to that function, so_Planningand every downstream repo are covered. The manual commands above are for first-time host setup and for repairing a worktree created before that version. IfDropboxBasePathConfigRootKeyis unresolvable the automatic step skips rather than guessing a path, and records the reason in the per-worktree result asMemoryJunctionError; a memory-junction failure never aborts sprint provisioning. CheckMemoryJunctionCreatedandMemoryStorePathon theSet-SprintBoundaryContextresult to confirm. - A missing junction fails silently —
Save-SprintWorkSessionreportsMemorySnapshotCreated: falsewith reason "Memory directory not found" and still exits successfully, so checkpoints look healthy while archiving zero memory files. - Dropbox sync can produce conflicted copies if two hosts write memory concurrently.
An agent would read a
... (conflicted copy).mdfile as an additional memory. This is accepted deliberately because the files are small and rarely written, but it is worth checking when memory content looks duplicated. Memory also propagates only as fast as Dropbox syncs, so a memory written on one host is not instantly visible on another.
These junctions are host-local configuration in scope for SystemParityMonitor; journal
their creation with Add-ParityChangeEntry so the peer host records a declared change
rather than undeclared drift.
Step 9a: Provision the Elevated Install Broker
Agents and build tooling run unelevated, but an AllUsers module install writes under
Program Files and needs administrator rights. The elevated install broker performs those
installs on their behalf with no UAC interaction, so agents never issue a blind
Start-Process -Verb RunAs retry loop. Without it, build-deploy-module cannot complete
its final install step.
Prerequisites: the SvcAnsibleAdmin account and its Bitwarden secret from Step 5, and the
ProGet feeds from Step 9.
Bootstrap order matters. The broker is what installs modules AllUsers, so it must be provisioned before the module-install machinery works. Run the provisioning function from the git clone, not from an installed module:
# ELEVATED pwsh. Adjust the clone path to the worktree you synced in Step 3.
$proGetModule = 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities\src\ATAP.Utilities.BuildTooling.ProGet.PowerShell'
Import-Module "$proGetModule\ATAP.Utilities.BuildTooling.ProGet.PowerShell.psd1" -Force -DisableNameChecking
Register-ElevationBrokerTask -Verbose
Register-ElevationBrokerTask is idempotent and does all of the following:
- creates
C:\ProgramData\ATAP\ElevationBrokerwith itsbin,requests,results,transcripts, andworksubfolders; - copies the broker payload and the config template out of the module's
Resources\ElevationBroker, never overwriting an existingconfig.jsonunless-ForcePayloadis passed, because that file is the broker's trust anchor; - applies a restrictive, non-inherited ACL to
requests\. The broker refuses to run whenEveryone,Authenticated Users, orUserscan write there, since that would let any local account reach an administrator context; - exports any existing task registration before replacing it;
- registers the
\ATAP\ATAP-ElevatedInstallBrokerscheduled task to run asSvcAnsibleAdminwith highest privileges, reading the password throughGet-SecretATAP.
Then grant the developer account the right to start the task on demand:
Grant-ElevationBrokerStartRights -Verbose
This step is required, not optional. The task has no repeating timer: it is started on
demand the moment a request is staged. Without start rights every request returns
broker-unreachable. The grant confers read + execute only, so the grantee can ask the
broker to drain its queue but cannot alter what the task runs.
Verify the whole path end to end from a normal, non-elevated shell — an elevated shell
would succeed through the Administrators ACE and prove nothing:
$s = New-Object -ComObject Schedule.Service
$s.Connect()
$t = $s.GetFolder('\ATAP').GetTask('ATAP-ElevatedInstallBroker')
$t.Run($null)
Start-Sleep -Seconds 5
"result=$($t.LastTaskResult) lastrun=$($t.LastRunTime)"
Expect result=0 and a current timestamp. Access denied means the grant did not target the
account that actually runs the build tooling.
Notes and gotchas:
- The task deliberately has no repeating time trigger. An earlier one-minute timer fired
roughly 4,644 times to service 4 real requests and measurably contributed to CPU
saturation.
BootTriggerremains as the catch-up drain for requests staged while the broker could not run. MultipleInstancesPolicymust stayQueue. Under-Oncethe broker enumerates the requests folder exactly once, soIgnoreNewwould silently strand a request staged while another instance was running.- The client starts the task through the
Schedule.ServiceCOM API rather thanStart-ScheduledTask, because theScheduledTasksmodule is not present under PowerShell 7 on these hosts.
The broker is host-local configuration in scope for SystemParityMonitor; journal its
provisioning with Add-ParityChangeEntry so the peer host records a declared change rather
than undeclared drift. See Runbook-ElevationBroker-PeerHost.md for the peer-host steps.
Step 10: Create Cobian Backup Jobs for the Tooling Databases
Create separate Cobian jobs for ProGet and BuildMaster. Each Cobian job should call
the SQL backup cmdlet from ATAP.Utilities.DatabaseManagement.Powershell rather than
copying MDF or LDF files directly.
Recommended commands:
pwsh -Command "& 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities\src\ATAP.Utilities.DatabaseManagement.Powershell\public\Invoke-SqlServerBackup.ps1' -DatabaseName 'ProGet' -BackupType Full"
pwsh -Command "& 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities\src\ATAP.Utilities.DatabaseManagement.Powershell\public\Invoke-SqlServerBackup.ps1' -DatabaseName 'BuildMaster' -BackupType Full"
Operational guidance:
- Schedule a weekly full backup for each database.
- Add nightly differential backups after the first full backup exists.
- Before extended DPOM, write classified
COPY_ONLYbackups withCHECKSUMunderC:\LocalDBs\PRODUCTION\Backupand runRESTORE VERIFYONLY. - Keep backup contents outside Git and Dropbox. Evidence contains metadata such as path identity, size, hash, timestamp, and verification outcome, never database contents.
- Never merge or restore ProGet/BuildMaster databases across hosts as workstation state; reconstruct outcomes from Git, packages, and reviewed configuration.
- Retention, recovery objectives, monitoring, and rehearsal remain gated by Task 13.60.
Phase 3: Validate the Development Environment
Step 11: Validate Stable-Branch Builds and Tests
Before calling the computer ready, run the stable branches through the same basic build and test flow expected by the manual CI process.
11.1 ATAP.Utilities stable branch
Push-Location 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities'
dotnet restore .\ATAP.Utilities.sln
dotnet build .\ATAP.Utilities.sln -c Debug
dotnet test .\ATAP.Utilities.sln -c Debug --no-build
Pop-Location
Run the PowerShell module tests as well:
pwsh -Command "Invoke-Pester -Path 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities\src\ATAP.Utilities.BuildTooling.PowerShell\tests' -Output Detailed"
pwsh -Command "Invoke-Pester -Path 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities\src\ATAP.Utilities.PowerShell\tests' -Output Detailed"
11.2 AceCommander stable branch
Push-Location 'C:\Dropbox\whertzing\GitHub\AceCommander'
dotnet restore .\AceCommander.sln
dotnet build .\AceCommander.sln -c Debug
dotnet test .\AceCommander.sln -c Debug --no-build
Pop-Location
11.3 Packaging and feed validation
Validate that the stable machine can build packages and modules for the manual promotion flow documented elsewhere in the repository.
Minimum checks:
- NuGet package builds succeed.
- PowerShell module builds succeed.
- ProGet is reachable.
- BuildMaster is reachable.
On a Sprint branch, pass -p:PackageLifeCycleStage=Sprint consistently to split and
combined restore/build/test flows. Preserve the ATAP5TIER001 guard. Omit the override on
main, where the stable/empty lifecycle label is valid.
Example reachability checks:
Invoke-WebRequest 'http://localhost:50000/' -UseBasicParsing | Select-Object StatusCode
Invoke-WebRequest 'http://localhost:50017/' -UseBasicParsing | Select-Object StatusCode
11.4 GitHub MCP access validation
Configure and test the GitHub MCP token before using Copilot or MCP-driven GitHub automation from the new workstation.
$toolRoot = 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities\src\ATAP.Utilities.BuildTooling.PowerShell\tools'
& (Join-Path $toolRoot 'Setup-GitHubMCP.ps1') -Token '<GitHub PAT>' -Scope User
& (Join-Path $toolRoot 'Test-GitHubMCP.ps1')
Setup-GitHubMCP.ps1 validates the token shape, stores it as GITHUB_TOKEN, and
checks that mcp-server-github is available on PATH. Use -Scope Process when
testing a token without persisting it.
Test-GitHubMCP.ps1 verifies that GITHUB_TOKEN is visible, confirms the MCP
server command is installed, checks the repository .vscode\settings.json MCP
server entry, and performs a GitHub API identity/rate-limit check with the token.
Restart VS Code after setting the token at user scope.
11.5 Data API Builder SQL MCP servers
Install Data API Builder (DAB) as a user-global .NET tool and initialize its one secret-free shared configuration. The canonical AI adapter starts DAB on demand in MCP stdio mode; it does not create a long-lived HTTP listener.
Import-Module ATAP.Utilities.BuildTooling.DotnetBuild.PowerShell
Initialize-DabMcpServer -Version '2.0.9'
Add-DabMcpEntity -EntityName 'Instantiations' -EntitySource 'ATAPUtilities.Instantiation'
Test-DabInstallation
The DAB configuration is stored at
$env:APPDATA\ATAP\DataApiBuilder\ATAPUtilities\dab-config.json. It contains only
the @env('DAB_ATAPUTILITIES_CONNECTION_STRING') reference and the approved entity
allow-list; it never contains a connection string or a .env file.
Codex and Claude Code receive five canonical, user-scope MCP registrations:
dab-ataputilities-production, dab-ataputilities-qa,
dab-ataputilities-integration, dab-ataputilities-dev, and
dab-ataputilities-exp. Each registration launches the same DAB stdio server with an
explicit tier, so a request cannot silently fall through to a different SQL instance.
The registrations invoke the dedicated Mcp/Start-DabMcpServer.ps1 launcher rather
than importing the full DotnetBuild module: an MCP stdio process must reserve stdout
for JSON-RPC, while unrelated module-import warnings corrupt the protocol. DAB starts
Kestrel even in stdio mode, so the registrations also use separate loopback endpoints:
Dev 5101, Exp 5102, Integration 5103, Production 5104, and QA 5105. These
ports must not be used by the normal DAB host or another MCP registration.
At startup, the dedicated MCP launcher derives the local host's BWS SecretName with
Get-DbConnectionStringSecretDescriptor, resolves it through
Get-SecretATAP -SecretStoreType BitwardenSecretsManager, and assigns the result only
to the DAB child-process environment. The resolved value must not be displayed,
persisted, added to agent settings, or written to .env. The secret names follow this
shape:
dbConnectionString.ATAPUtilities.<current-host>.Production
dbConnectionString.ATAPUtilities.<current-host>.QA
dbConnectionString.ATAPUtilities.<current-host>.Integration
dbConnectionString.ATAPUtilities.<current-host>.Dev.<current-user>
dbConnectionString.ATAPUtilities.<current-host>.Exp.<current-user>
After the shared MCP catalog is rendered and the agent application is restarted, open
the MCP status surface and confirm that all five DAB servers appear. Start with the
Exp server. The DAB configuration grants mcp-reader:read only; add an entity only
after reviewing its columns and least-privilege requirement. DAB's stdio mode requires
runtime.mcp.enabled and keeps protocol messages on stdout; use --LogLevel Error as
the canonical launcher does. See DAB stdio transport
and DAB environment substitution.
11.6 Mobile-host and Class A certification
Before a mobile workstation is considered ready for extended DPOM, record metadata-only proof for:
- BitLocker fully encrypted with protection on; anti-malware current; time synchronized.
- Sleep/hibernate, battery-sensitive scheduled tasks, Windows support/EOS, and public-network behavior reviewed.
- NordVPN Internet Kill Switch and the trusted-network rule tested before cloud access.
- Local SQL and Inedo health, one value-discarding SecretName resolution, representative restore/build/test, GitHub authentication, and Dropbox continuity.
- A live off-LAN gate where peer services are unreachable and local Class A work still passes.
- A bounded return drill that reconciles parity journals and path/hash-only AI intent, transfers immutable packages by hash, and reconstructs BuildMaster outcomes without copying Inedo databases.
Class B/full-offline secret use is separately limited and is not implied by a Class A pass.
If a parity-share read encounters Windows double hop, use the documented temporary
credentialed-PSDrive workaround with a credential resolved by SecretName. Do not weaken
remoting globally; hardened transport remains deferred to SC-0242.
Ready State
The new computer is ready for a developer when all of the following are true:
- Dropbox is settled; conflict/reparse and tracked-file integrity checks pass; every Git trust entry is an exact authorized path.
- The multi-host overview, machine-local sprint retarget,
.vscodejunction, and concrete AI adapter renders match the current host assignment. - Developer and service-account profiles load from canonical ATAP.IAC templates in the paths PowerShell actually uses; direct and profiled-remoting identity probes pass or carry an explicit deferred exception.
- The
SecretName/Get-SecretATAP/bwsautomation boundary and separate personalbw/BW_SESSIONboundary are respected. Host settings and callers contain SecretNames only; no raw API-key environment-variable readiness claim is permitted. - The declared Java/package owners and Flyway resolution pass, or the deferred Java decision is recorded without claiming canonicalization.
- SQL Server
Devwhertzing,Expwhertzing,Integration,QA, andProductionare verified under their distinct data classifications. Required local backups pass checksum and restore verification outside Git/Dropbox. - ProGet answers on
50000with its package-root, license, feed inventory, ACL, and package proof complete. BuildMaster answers on50017with supported application definitions and a full readiness assertion. - Stable-branch builds/tests pass; Sprint-branch flows pass with the lifecycle override.
- Mobile security, Class A off-LAN, GitHub/Dropbox continuity, and bounded return evidence pass.
- Every acceptance record distinguishes source, deploy, service, and operator state and contains metadata only. Unresolved HITL/deferred gates remain named rather than silently treated as complete.
At that point the workstation can serve as a fully functional developer machine.
(Optional) Manim Community Animation Tooling
Manim Community is a Python animation engine used to create precise mathematical and technical animations programmatically. Install it only on workstations that render Manim scenes. It depends on Python (Step 2.3), ffmpeg, and optionally MiKTeX for LaTeX-rendered equations.
Install ffmpeg
choco install ffmpeg --yes
ffmpeg -version # Expected: ffmpeg version N-xxxxx ...
Install MiKTeX (optional — for LaTeX equations)
MiKTeX provides the LaTeX distribution Manim uses for MathTex/Tex objects. Skip if you
do not need equation rendering.
choco install miktex --yes
After installation, open the MiKTeX Console and run Check for updates → Update now. Manim downloads additional LaTeX packages on first use.
Create the project folder and virtual environment
The Manim work lives inside the ATAP.Utilities repository rather than in a separate repo, keeping Python scene scripts, C# host code, and shared ATAP.Utilities libraries under one version-control root.
ATAP.Utilities/
├── ManimVideoGenerator/ ← Python project root (its own .gitignore)
│ ├── .gitignore ← excludes .venv/, __pycache__/ (NOT media/)
│ ├── .venv/ ← Python virtual environment (gitignored)
│ ├── scenes/ ← Manim scene .py scripts
│ ├── media/ ← rendered MP4/GIF output (tracked in git)
│ └── requirements.txt ← pinned Python dependencies
└── src/
└── ATAP.Utilities.ManimVideoGenerator/ ← C# facade (.csproj)
├── ATAP.Utilities.ManimVideoGenerator.Interfaces/
├── ATAP.Utilities.ManimVideoGenerator.StringConstants/
├── ATAP.Utilities.ManimVideoGenerator.DefaultSettings/
└── ATAP.Utilities.ManimVideoGenerator.Models/
Rendered output under media/ is tracked in git so it merges into main when the sprint
PR lands and can be consumed by other projects over time.
Set-Location C:\Dropbox\whertzing\GitHub\ATAP.Utilities
New-Item -ItemType Directory -Path ManimVideoGenerator
Set-Location ManimVideoGenerator
python -m venv .venv
.\.venv\Scripts\Activate
The shell prompt shows (.venv) when the environment is active. Create
ManimVideoGenerator\.gitignore with at minimum:
# Python virtual environment
.venv/
# Python caches
__pycache__/
*.pyc
*.pyo
# NOTE: media/ is intentionally NOT ignored — rendered output is tracked in git
# so it can be merged into main and reused by other projects.
Virtual environment path for C#
Process.Start():C:\Dropbox\whertzing\GitHub\ATAP.Utilities\ManimVideoGenerator\.venv\Scripts\manim.exe— passed asProcessStartInfo.FileName, or read from a configuration value bound toIOptions<ManimVideoGeneratorOptions>.
You must activate the virtual environment each time you open a new terminal to work on Manim scripts directly. VS Code activates it automatically (see the Manim Sideview subsection below).
Sprint worktree and venv lifecycle
Git worktrees give every sprint branch its own directory. There are two sprint types:
| Sprint type | Examples | Venv action needed? | MANIM_EXE_PATH changes? |
|---|---|---|---|
| Scene-only | Write or edit .py scene files, add rendering output to media/ | None — reuse the main worktree venv | No — keep pointing at the main worktree venv |
| Manim-upgrade | Install a new Manim version, change requirements.txt, modify C# facade | Create a new .venv in the sprint worktree | Yes — point at the sprint worktree venv |
For the common scene-only case, the .venv in the main worktree is fully reusable; only the
sprint worktree's scenes/ folder changes, tracked normally by git. Because .venv/ is
gitignored, a newly-created sprint worktree contains no virtual environment — irrelevant for
scene-only sprints. Only Manim-upgrade sprints require a fresh venv in the sprint worktree.
| Context | Root path |
|---|---|
| Main worktree | C:\Dropbox\whertzing\GitHub\ATAP.Utilities\ |
| Sprint worktree | C:\Dropbox\whertzing\GitHub\ATAP.Utilities-wt-{N}-Sprint-NNN-work-items\ |
Items holding an absolute path to manim.exe/python.exe that must track the active
worktree (updated when a Manim-affecting sprint starts, reverted when it ends):
| Item | Storage location | Scene-only sprint | Manim-upgrade sprint |
|---|---|---|---|
manim.exe path in IOptions<ManimVideoGeneratorOptions> | DefaultSettings config override or MANIM_EXE_PATH env var | No change — reuse main worktree venv path | Update to sprint worktree venv path |
| VS Code Manim Sideview: Default Manim Path | VS Code user settings (or workspace override) | No change | Update to sprint worktree venv path |
| VS Code Python: Select Interpreter | Workspace settings or command palette selection | No change | Update to sprint worktree venv |
scenes/ content (.py files) | Tracked in git on the sprint branch | Normal git workflow | Normal git workflow |
requirements.txt | Tracked in git on the sprint branch | Normal git workflow | Update, then pip install -r requirements.txt |
media/ rendered output | Tracked in git on the sprint branch | Committed and merged into main with the PR | Committed and merged into main with the PR |
Recommended pattern — MANIM_EXE_PATH environment variable. For scene-only sprints this
variable never changes. For Manim-upgrade sprints, storing the path in a User-scope
environment variable means only that one variable needs updating — no source files are
touched. The C# binding reads it at startup:
options.ManimExecutablePath =
configuration["ManimVideoGenerator:ManimExecutablePath"]
?? Environment.GetEnvironmentVariable("MANIM_EXE_PATH")
?? @"C:\Dropbox\whertzing\GitHub\ATAP.Utilities\ManimVideoGenerator\.venv\Scripts\manim.exe";
Sprint-start agent steps (Manim-affecting sprints only):
$sprintWorktreeRoot = 'C:\Dropbox\whertzing\GitHub\ATAP.Utilities-wt-{N}-Sprint-NNN-work-items'
$venvManimExe = "$sprintWorktreeRoot\ManimVideoGenerator\.venv\Scripts\manim.exe"
New-Item -ItemType Directory -Path "$sprintWorktreeRoot\ManimVideoGenerator" -Force
Set-Location "$sprintWorktreeRoot\ManimVideoGenerator"
python -m venv .venv
.\.venv\Scripts\Activate
pip install manim
# Point MANIM_EXE_PATH at the sprint worktree venv (User scope — survives shell restart)
[System.Environment]::SetEnvironmentVariable('MANIM_EXE_PATH', $venvManimExe, 'User')
# Update the VS Code workspace Python interpreter as well (see UserSettings.jsonc below).
Symlink alternative: the sprint-start agent can instead create an NTFS junction
ManimVideoGenerator-activein a fixed location that always points to the active worktree'sManimVideoGenerator\folder; configuration then references the fixed junction path and only the junction target changes.
Sprint-end agent steps (after PR merge):
- Scene-only sprint: No venv action. Ensure
media/rendered output is staged and committed before the PR merges — it then lands in main automatically. - Manim-upgrade sprint only: revert
MANIM_EXE_PATHto the main-branch venv; the sprint worktree's gitignored.venv\is deleted with the worktree (no git cleanup).
[System.Environment]::SetEnvironmentVariable(
'MANIM_EXE_PATH',
'C:\Dropbox\whertzing\GitHub\ATAP.Utilities\ManimVideoGenerator\.venv\Scripts\manim.exe',
'User')
Install Manim
With the virtual environment active:
pip install manim # 2–5 minutes on first install
manim --version # Expected: Manim Community v0.19.x
manim checkhealth # cairo / ffmpeg / latex / manimpango should PASS
If LaTeX reports FAIL and you did not install MiKTeX, that is expected and acceptable as
long as you do not use MathTex or Tex objects.
VS Code extension — Manim Sideview
Note:
Rickaym.manim-sideviewis not available as a Chocolatey package (confirmed 2026-04-04). Prefer installing from the Extensions panel (Ctrl+Shift+X) → searchManim Sideview→ Install, because runningcode --install-extensionwhile VS Code is open launches additional GUI windows. The CLI command is preserved for automation:
code --install-extension Rickaym.manim-sideview
Then configure the extension and interpreter:
- Settings (
Ctrl+,) → searchmanim sideview→ set Manim Sideview: Default Manim Path toC:\Dropbox\whertzing\GitHub\ATAP.Utilities\ManimVideoGenerator\.venv\Scripts\manim.exe. - Command Palette (
Ctrl+Shift+P) → Python: Select Interpreter → Enter interpreter path →C:\Dropbox\whertzing\GitHub\ATAP.Utilities\ManimVideoGenerator\.venv\Scripts\python.exe.
Sprint-branch adjustments for UserSettings.jsonc. On a Manim-affecting sprint branch,
both keys must point to the sprint worktree venv. These settings live in
SharedVSCode/UserSettings.jsonc (the canonical source — there is no per-repo
.vscode/settings.json).
| Setting key | Purpose |
|---|---|
manim-sideview.defaultManimPath | Path to the manim.exe the Sideview extension invokes |
python.defaultInterpreterPath | Python interpreter VS Code uses for the workspace |
Sprint-start — set to the sprint worktree venv (example sprint <active-sprint-branch>):
// In SharedVSCode/UserSettings.jsonc
"manim-sideview.defaultManimPath": "C:\\Dropbox\\whertzing\\GitHub\\<active-ATAP.Utilities-worktree>\\ManimVideoGenerator\\.venv\\Scripts\\manim.exe",
"python.defaultInterpreterPath": "C:\\Dropbox\\whertzing\\GitHub\\<active-ATAP.Utilities-worktree>\\ManimVideoGenerator\\.venv\\Scripts\\python.exe",
Sprint-end — revert to the main-branch venv after the PR merges and the worktree is removed:
// In SharedVSCode/UserSettings.jsonc
"manim-sideview.defaultManimPath": "C:\\Dropbox\\whertzing\\GitHub\\ATAP.Utilities\\ManimVideoGenerator\\.venv\\Scripts\\manim.exe",
"python.defaultInterpreterPath": "C:\\Dropbox\\whertzing\\GitHub\\ATAP.Utilities\\ManimVideoGenerator\\.venv\\Scripts\\python.exe",
Agent reminder: these two keys must be in the sprint-start and sprint-end checklists for any sprint that creates or upgrades a Manim venv. Scene-only sprints that reuse the main-branch venv do not need these changes.
Render a verification scene
# test_manim.py
from manim import *
class HelloManim(Scene):
def construct(self):
text = Text("Hello, Manim!")
self.play(Write(text))
self.wait(1)
manim -pql test_manim.py HelloManim
-p opens the video after rendering; -q l renders at low quality (fast). Output is saved
to media\videos\test_manim\480p15\HelloManim.mp4 relative to the project folder. If MiKTeX
was installed, optionally verify equation rendering with a MathTex(r"e^{i\pi} + 1 = 0")
scene — MiKTeX downloads required LaTeX packages automatically on first run.
Google Antigravity setup on a new Windows 11 computer
Use the steps in this section to install Google Antigravity on a new workstation and connect it to the current sprint's local repositories.
Install prerequisites
- Install Google Antigravity using the vendor-supported installer or package source used by the team.
- Sign in with the account used for development work.
- Verify Git is installed and available on
PATH. - Verify Visual Studio Code is installed and available on
PATH. - Verify PowerShell 7 is installed and available on
PATH.
Repo discovery model
Antigravity should not be configured with a hard-coded list of repository paths because the workspace file changes every sprint.
Instead, the local configuration should be generated from the current sprint's .code-workspace file, for example:
C:\Dropbox\whertzing\GitHub\Overview.Sprint.0007.code-workspace
Each new sprint creates a new workspace file, so the authoritative source for the repo list is the active sprint workspace file, not a static Antigravity configuration checked in once.
Recommended configuration approach
Configure Antigravity so its list of connected repositories is rebuilt from the current workspace file whenever SprintStartAgent creates a new sprint workspace and whenever SprintEndAgent closes out a sprint.
Recommended pattern:
- Keep a small generated Antigravity config file outside the repo or in a machine-local ignored path.
- Have
SprintStartAgentdetermine the active workspace file for the new sprint. - Parse the
folderscollection in that.code-workspacefile. - Resolve each folder path to an absolute local path.
- Rewrite the Antigravity repo connection file or invoke the Antigravity CLI/API to register those repos.
- Optionally remove repos no longer present in the active sprint workspace.
- Have
SprintEndAgenteither clear the repo list or switch Antigravity to the next active workspace, depending on the sprint workflow.
PowerShell implementation guidance
Add or maintain a PowerShell helper that accepts the current workspace path and emits the repo list in the format expected by Antigravity.
Suggested contract:
Set-AntigravityWorkspaceRepos `
-WorkspacePath 'C:\Dropbox\whertzing\GitHub\Overview.Sprint.0007.code-workspace' `
-AntigravityConfigPath "$env:LOCALAPPDATA\Google\Antigravity\repos.json"
Suggested behavior:
- Read the workspace file as JSON.
- Expand every entry under
folders. - Normalize relative paths against the workspace file directory.
- Preserve only paths that currently exist.
- Write the resolved repo list to the Antigravity configuration store.
- Make the script idempotent so
SprintStartAgentandSprintEndAgentcan run it repeatedly without duplicating entries.
Example PowerShell sketch
function Set-AntigravityWorkspaceRepos {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $WorkspacePath,
[Parameter(Mandatory)]
[string] $AntigravityConfigPath
)
$workspace = Get-Content -Raw -Path $WorkspacePath | ConvertFrom-Json
$workspaceDirectory = Split-Path -Parent $WorkspacePath
$repoPaths = foreach ($folder in $workspace.folders) {
$candidate = if ($folder.path -match '^[A-Za-z]:\\') {
$folder.path
}
else {
[System.IO.Path]::GetFullPath((Join-Path $workspaceDirectory $folder.path))
}
if (Test-Path -LiteralPath $candidate) {
$candidate
}
}
$repoPaths = $repoPaths | Sort-Object -Unique
$payload = [ordered]@{
generatedFromWorkspace = $WorkspacePath
generatedOn = (Get-Date).ToString('s')
repositories = @($repoPaths | ForEach-Object {
[ordered]@{ path = $_ }
})
}
$parent = Split-Path -Parent $AntigravityConfigPath
if (-not (Test-Path -LiteralPath $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
$payload | ConvertTo-Json -Depth 5 | Set-Content -Path $AntigravityConfigPath -Encoding utf8
}
Agent integration points
SprintStartAgent should:
- Create the new sprint workspace file.
- Call
Set-AntigravityWorkspaceReposwith that workspace file. - Launch or refresh Antigravity if needed so it reloads the updated repo list.
SprintEndAgent should:
- Determine whether the sprint is being archived, replaced, or rolled forward.
- Either clear Antigravity's repo list, or repoint it to the next workspace file.
- Call the same helper so there is only one code path for repo synchronization.
Maintenance note
Document the Antigravity integration in terms of an active workspace file and a generated local config, rather than naming a specific sprint file such as Overview.Sprint.0007.code-workspace, because that filename changes every sprint.
(Optional) Headroom — AI Agent Context Compression
Headroom (headroom-ai, Apache-2.0, chopratejas/headroom)
is a context-optimization layer for LLM agents. It compresses large tool outputs, logs,
search results, and traces so coding agents (Claude Code, Codex, Copilot) consume fewer
context tokens. It is delivered three ways, all of which this section installs/enables:
- MCP server — exposes on-demand tools
headroom_compress,headroom_retrieve,headroom_statsto Claude Code and Codex. This is the primary, recommended mode. - Proxy — a local server on port
8787that transparently compresses provider traffic when an agent is launched through it (headroom wrap claude|codex). - Library — the Python compress API the MCP server calls internally.
Install Headroom only on workstations doing heavy agentic development. It is independent of the SQL/ProGet/BuildMaster pipeline and can be skipped without affecting builds.
Compression engine note. Headroom's semantic compression loads two ML models on first use — an
answerdotai/ModernBERT-baseembedding model (viatorch/sentence-transformers) and a quantizedkompress-int8.onnxmodel (viaonnxruntime). On CPU the first compression incurs a multi-minute cold model-load, and steady-state compress latency is CPU-bound. A machine with an NVIDIA GPU should follow Path B below to run these models on the GPU. Choose exactly one path in §H.3.
H.0 Decision: which path?
| Path A — CPU-only | Path B — GPU-accelerated | |
|---|---|---|
| Requires | Any x64 CPU | NVIDIA GPU (e.g. RTX 3080) + recent driver (CUDA 12.x class) |
| Packages | torch (cpu build), onnxruntime | CUDA torch (+cuXXX), onnxruntime-gpu |
| First compress | Multi-minute cold model load | Seconds (after one-time model download) |
| Steady-state compress | CPU-bound (slow on large payloads) | GPU-accelerated |
| Proxy wrap overhead | ~550ms+/request (observed on CPU) | Lower |
| Use when | No NVIDIA GPU present | NVIDIA GPU present — preferred |
Verify GPU presence before choosing:
# Path B is available only if this prints a GPU. Otherwise use Path A.
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
H.1 Prerequisites (both paths)
These were required to build headroom-ai[all] from source on Windows + Python 3.11 (the
package builds native wheels for itself and hnswlib):
-
Python 3.11 — already installed in Step 2.3 at
C:\Python311\(do not use 3.14). -
Rust toolchain — Headroom's native extension bootstraps Rust if absent; install it explicitly first to avoid a mid-install cert failure:
winget install Rustlang.Rustup --source winget # New shell, then verify: rustup --version; rustc --version; cargo --version -
Visual Studio Build Tools 2022 with the C++ workload — provides
link.exe/cl.exeneeded to compile native wheels:winget install Microsoft.VisualStudio.2022.BuildTools ` --override "--quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
H.2 Create the dedicated virtual environment (both paths)
Headroom is cross-workflow tooling, so it gets its own reusable venv — not pipx, and
not a project-scoped venv (e.g. the ManimVideoGenerator venv is intentionally left alone).
& 'C:\Python311\python.exe' -m venv 'C:\Users\whertzing\.venvs\headroom'
$venvPy = 'C:\Users\whertzing\.venvs\headroom\Scripts\python.exe'
# truststore lets pip use the Windows certificate store (required on this network)
& $venvPy -m pip install --upgrade --use-feature=truststore pip setuptools wheel
H.3 Install Headroom — choose ONE path
The native build needs the Visual C++ environment loaded and two network workarounds. This
helper loads VsDevCmd.bat, puts Cargo on PATH, and sets the cert workarounds for the
current shell — run it before the pip install in either path:
$vsdev = 'C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat'
cmd.exe /d /s /c "call `"$vsdev`" -arch=x64 -host_arch=x64 && set" | ForEach-Object {
if ($_ -match '^(.*?)=(.*)$') { Set-Item -Path ('Env:' + $matches[1]) -Value $matches[2] }
}
$env:PATH = 'C:\Users\whertzing\.cargo\bin;' + $env:PATH
$env:CARGO_HTTP_CHECK_REVOKE = 'false' # Cargo hits CRYPT_E_NO_REVOCATION_CHECK against crates.io without this
$venvPy = 'C:\Users\whertzing\.venvs\headroom\Scripts\python.exe'
Path A — CPU-only
& $venvPy -m pip install --use-feature=truststore 'headroom-ai[all]'
# Verify
& 'C:\Users\whertzing\.venvs\headroom\Scripts\headroom.exe' --help
& $venvPy -c "import headroom; print('headroom', headroom.__version__)"
This installs CPU builds (torch ...+cpu, CPU onnxruntime). Compression works but is
CPU-bound — expect a multi-minute first-compress while models load.
Path B — GPU-accelerated (NVIDIA)
Install Headroom the same way, then replace the two CPU ML packages with CUDA builds.
The default headroom-ai[all] pulls torch ...+cpu and CPU-only onnxruntime; neither
touches the GPU until swapped.
# 1. Base install (same as Path A)
& $venvPy -m pip install --use-feature=truststore 'headroom-ai[all]'
# 2. Replace CPU torch with a CUDA build (cu124 shown — match your driver's CUDA level).
# This is a large (~2.5 GB) download.
& $venvPy -m pip uninstall -y torch
& $venvPy -m pip install --use-feature=truststore torch --index-url https://download.pytorch.org/whl/cu124
# 3. Replace CPU onnxruntime with the GPU build so the kompress ONNX model can use CUDA.
& $venvPy -m pip uninstall -y onnxruntime
& $venvPy -m pip install --use-feature=truststore onnxruntime-gpu
# 4. Verify both ML stacks see the GPU
& $venvPy -c "import torch; print('torch', torch.__version__, 'cuda_available', torch.cuda.is_available(), 'device', torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'N/A')"
& $venvPy -c "import onnxruntime as ort; print('ORT', ort.__version__, 'providers', ort.get_available_providers())"
Path B is correct when:
torch.cuda.is_available()printsTrueand names your GPU, andonnxruntime.get_available_providers()includesCUDAExecutionProvider.
If either still reports CPU-only, the CUDA torch wheel or onnxruntime-gpu did not match
the installed CUDA runtime — re-check the driver's CUDA level (nvidia-smi) and pick the
matching cuXXX wheel index. There is no Headroom CLI switch for the GPU; GPU use is
entirely determined by which torch / onnxruntime packages are installed.
H.3.1 ⚠ REQUIRED: pin the onnxruntime DLL (both paths) — prevents a fatal hang
Symptom if skipped: any Headroom compression hangs indefinitely (observed: a single compression ran 24.8 minutes without completing). It looks like a CPU/GPU/model problem but is not.
Root cause: Headroom's Rust extension headroom._core.detect_content_type (the magika
content-type detector that the compression pipeline calls first, before any model) uses
the Rust ort crate, which by default tries to locate/download its own onnxruntime.
On this environment that probe blocks forever — the process sits at 0% CPU, 1 thread, no
completed network connection. It hangs on every input, even an 11-character string.
(Diagnosed with Python faulthandler, which pinpointed content_router.py →
_detect_content → the Rust call.)
Fix: point the ort crate at the onnxruntime DLL already installed in the venv via the
ORT_DYLIB_PATH environment variable. Set it User-scope so every Headroom process
inherits it — the logon proxy task, the MCP servers Claude Code / Codex spawn, and
interactive shells:
$ortDll = 'C:\Users\whertzing\.venvs\headroom\Lib\site-packages\onnxruntime\capi\onnxruntime.dll'
[Environment]::SetEnvironmentVariable('ORT_DYLIB_PATH', $ortDll, 'User')
$env:ORT_DYLIB_PATH = $ortDll # current shell too
# Verify the Rust detector now returns instantly instead of hanging
& 'C:\Users\whertzing\.venvs\headroom\Scripts\python.exe' -c "import time; from headroom._core import detect_content_type as d; t=time.time(); r=d('hello world'); print(f'{time.time()-t:.2f}s', r.content_type)"
# Expected: ~0.1s text (NOT a hang)
After this fix, a 4,162-token build log compresses in ~0.5s cold / ~0s warm (≈86% token reduction) with all critical evidence preserved — versus the 24.8-minute hang without it.
The §H.5 proxy task and §H.6 profile also set
ORT_DYLIB_PATHexplicitly as belt-and-suspenders for-NoProfile/ service contexts. Setting it User-scope here is what makes it apply everywhere.
H.3.2 Disable Headroom telemetry at user scope
Disable Headroom telemetry once at User scope so the setting is inherited by normal
PowerShell sessions, headroom wrap ... launches, MCP server processes, Claude Code,
Codex, and VS Code sessions started after the setting is applied:
[Environment]::SetEnvironmentVariable('HEADROOM_TELEMETRY', 'off', 'User')
$env:HEADROOM_TELEMETRY = 'off' # current shell too
headroom proxy --no-telemetry still appears in the scheduled task below as a
belt-and-suspenders guard. The user-scope environment variable is the setting that prevents
headroom wrap codex / headroom wrap claude from reporting telemetry as enabled when the
wrapper is launched from a fresh shell.
H.4 Configure the Headroom MCP server (both paths) — primary mode
Register the absolute venv path (not a bare headroom, which is PATH-dependent and
fails to connect) as a user-scope stdio MCP server in both agents:
$hrExe = 'C:\Users\whertzing\.venvs\headroom\Scripts\headroom.exe'
# Claude Code (user scope = all projects)
claude mcp add headroom -s user -- $hrExe mcp serve
claude mcp get headroom # expect: Status: Connected
# Codex (global config: C:\Users\whertzing\.codex\config.toml)
& 'C:\Users\whertzing\AppData\Local\OpenAI\Codex\bin\<build-id>\codex.exe' mcp add headroom -- $hrExe mcp serve
Restart any running Claude Code / Codex session so the new server is picked up, then run
/mcp in each to confirm headroom_compress, headroom_retrieve, headroom_stats appear.
H.5 Proxy autostart at logon (both paths) — optional wrap mode
The proxy backs headroom wrap claude|codex|copilot and any editor/app that can be pointed
at an OpenAI-compatible or Anthropic-compatible base URL. Run it as a hidden,
telemetry-disabled logon scheduled task that logs to
C:\Users\whertzing\.headroom\proxy.log.
Headroom writes nothing to a log file on its own — a bare Scheduled Task would silently
discard its stdout/stderr — so the task must redirect output explicitly. A cscript/VBScript
wrapper launches it with no visible console window (a plain cmd/pwsh action flashes
and steals foreground focus). The VBScript writes a one-line batch that sets
HEADROOM_TELEMETRY=off and passes --no-telemetry:
$headroomDir = 'C:\Users\whertzing\.headroom'
New-Item -ItemType Directory -Path $headroomDir -Force | Out-Null
$vbs = Join-Path $headroomDir 'Start-HeadroomProxy.vbs'
@'
Set objShell = CreateObject("WScript.Shell")
Set objFSO = CreateObject("Scripting.FileSystemObject")
batchPath = "C:\Users\whertzing\.headroom\headroom_proxy_run.bat"
Set objFile = objFSO.CreateTextFile(batchPath, True)
objFile.WriteLine("@echo off")
objFile.WriteLine("set HEADROOM_TELEMETRY=off")
objFile.WriteLine("C:\Users\whertzing\.venvs\headroom\Scripts\headroom.exe proxy --port 8787 --no-telemetry >> ""C:\Users\whertzing\.headroom\proxy.log"" 2>&1")
objFile.Close()
objShell.Run batchPath, 0, False ' 0 = hidden window
'@ | Set-Content -LiteralPath $vbs -Encoding ASCII
$action = New-ScheduledTaskAction -Execute 'cscript.exe' -Argument "`"$vbs`""
$trigger = New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Hours 0) `
-RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -DontStopOnIdleEnd
# Replace any old task with the current hidden/telemetry-off definition.
Unregister-ScheduledTask -TaskName 'Headroom Proxy' -Confirm:$false -ErrorAction SilentlyContinue
Register-ScheduledTask -TaskName 'Headroom Proxy' -Action $action -Trigger $trigger `
-Settings $settings -RunLevel Highest `
-Description 'Headroom context compression proxy on port 8787 (hidden, telemetry off)'
# Start now and confirm
Start-ScheduledTask -TaskName 'Headroom Proxy'
Start-Sleep -Seconds 5
if (netstat -ano | Select-String ':8787.*LISTENING') { 'Proxy listening on 8787' }
# Confirm telemetry is disabled
(curl http://127.0.0.1:8787/stats | ConvertFrom-Json).telemetry.enabled # expect: False
Why not a direct
headroom.exe proxytask action? The proxy runs in the foreground until killed; a Scheduled Task whose action is the proxy never reports "completed" and can trip the execution-time limit. The VBScript launches it detached and exits immediately, so the task completes while the proxy keeps running.Scope-creep (SC-0171): the inline VBScript + task registration above should be replaced by a reusable
Register-HeadroomProxyTask.ps1function inATAP.Utilities.BuildTooling.PowerShell/public/; this doc will then call that function.
H.6 PowerShell profile additions (both paths)
CurrentUserAllHostsV7CoreProfile.ps1 adds a headroom function (so the venv CLI resolves
from any shell) and a startup warning when the proxy is not listening:
function headroom { & "C:\Users\whertzing\.venvs\headroom\Scripts\headroom.exe" @args }
if (-not (netstat -ano 2>$null | Select-String ":8787.*LISTENING")) {
Write-Warning "Headroom proxy is NOT running on port 8787. Start with: headroom proxy --port 8787"
}
H.7 Automatically use Headroom with agent CLIs, apps, and VS Code extensions
Headroom can fully auto-wrap CLI processes because the wrapper controls their launch
environment. Desktop apps and VS Code extensions are different: they usually run in their
own host process and often ignore OPENAI_BASE_URL / ANTHROPIC_BASE_URL. For those,
Headroom works only when the app/extension exposes a custom base URL, BYOK provider, or
compatible local-proxy setting.
Recommended baseline:
# Run once per user profile.
$codexBin = "$env:LOCALAPPDATA\OpenAI\Codex\bin"
$headroomBin = "$env:USERPROFILE\.venvs\headroom\Scripts"
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$wanted = @($headroomBin, $codexBin)
$entries = @($userPath -split ';' | Where-Object { $_ })
foreach ($entry in $wanted) {
if ($entries -notcontains $entry) { $userPath = "$entry;$userPath" }
}
[Environment]::SetEnvironmentVariable('Path', $userPath, 'User')
[Environment]::SetEnvironmentVariable('HEADROOM_TELEMETRY', 'off', 'User')
[Environment]::SetEnvironmentVariable('OPENAI_BASE_URL', 'http://127.0.0.1:8787/v1', 'User')
[Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL', 'http://127.0.0.1:8787', 'User')
Open a new PowerShell after setting PATH/environment variables, then verify:
Get-Command headroom
Get-Command codex
curl http://127.0.0.1:8787/stats
CLI launch commands:
# Claude Code CLI
headroom wrap claude --no-serena --no-mcp --no-proxy
# Codex CLI
headroom wrap codex --no-serena --no-mcp --no-proxy
# GitHub Copilot CLI
headroom wrap copilot --no-proxy -- --model claude-sonnet-4-20250514
Use --no-proxy only when the Headroom Proxy scheduled task is already listening on
127.0.0.1:8787. Use --no-mcp for Claude Code / Codex after §H.4 has registered MCP with
the absolute venv path. Use --no-serena unless uvx has been installed and Serena MCP is
intentionally enabled.
Desktop app / VS Code extension handling:
| Client surface | Headroom approach |
|---|---|
| Claude Code CLI | Fully auto-wrap with headroom wrap claude .... |
| Codex CLI | Fully auto-wrap with headroom wrap codex ...; ensure C:\Users\whertzing\AppData\Local\OpenAI\Codex\bin is on PATH. |
| GitHub Copilot CLI | Fully auto-wrap with headroom wrap copilot ...; requires the copilot CLI on PATH and a model/provider supported by Copilot CLI BYOK mode. |
| Codex desktop app | Prefer MCP tools only. Do not keep a global Headroom provider/base URL override in C:\Users\whertzing\.codex\config.toml; it can make the app show only conversations created under model_provider = headroom. Use the wrapped Codex CLI for guaranteed proxy routing. |
| Claude desktop app | Only route through Headroom if the app exposes a custom Anthropic base URL or inherits ANTHROPIC_BASE_URL; otherwise use Claude Code CLI for guaranteed routing. |
| VS Code launched from shell | Launch from a shell that has OPENAI_BASE_URL, ANTHROPIC_BASE_URL, and HEADROOM_TELEMETRY=off: code <workspace>. This only helps extensions that actually inherit and honor those variables. |
| GitHub Copilot VS Code extension | Do not assume it is wrapped. The first-party extension may use GitHub-hosted Copilot endpoints and may ignore OpenAI/Anthropic base URL variables. Use Copilot CLI through headroom wrap copilot for guaranteed Headroom routing, or configure BYOK/custom-model base URL only if the installed extension version exposes that setting. |
| Cline VS Code extension | Run headroom wrap cline --no-proxy, then configure Cline's API Base URL in VS Code settings to http://127.0.0.1:8787/v1 for OpenAI-compatible mode or the matching Anthropic-compatible URL if using Anthropic mode. |
| Continue VS Code / JetBrains extension | Run headroom wrap continue --no-proxy, then set each model's apiBase in .continue/config.json / .continue/config.yaml to the Headroom proxy URL. |
| Cursor app | Run headroom wrap cursor --no-proxy, then set Cursor's OpenAI override base URL to http://127.0.0.1:8787/v1. |
For app/extension surfaces, the acceptance test is not "the app launched" - it is "the proxy saw traffic":
curl http://127.0.0.1:8787/stats
headroom perf --hours 1
If the request counters do not change while using a surface, that surface is not routed through Headroom yet. Prefer the wrapped CLI for guaranteed savings.
H.7.1 Codex desktop conversation visibility incident
On 2026-06-03, after headroom wrap codex was made to work for the Codex CLI, the Codex
desktop app showed No Chats for projects that still had conversation history. The
conversations were not deleted. They were still present in
C:\Users\whertzing\.codex\state_5.sqlite under the normal openai provider. The new
desktop sessions were being created under model_provider = headroom, so the app sidebar
was effectively showing the wrong provider slice.
The risky config injected by the wrapper was at the top of
C:\Users\whertzing\.codex\config.toml:
# --- Headroom proxy (auto-injected by headroom wrap codex) ---
model_provider = "headroom"
openai_base_url = "http://127.0.0.1:8787/v1"
# --- end Headroom ---
The repair was:
- Confirm the old conversations existed in
state_5.sqlite, grouped mostly undermodel_provider = openai. - Back up
C:\Users\whertzing\.codex\config.toml. - Remove only the global Headroom provider/base URL override shown above.
- Keep
[mcp_servers.headroom]intact so the MCP tools still load. - Fully quit and restart the Codex desktop app.
Do not use the Codex desktop app as the primary Headroom proxy surface until Codex exposes a
provider override that does not partition or hide existing conversation history. Use
headroom wrap codex for the CLI and MCP tools for the desktop app.
H.8 Daily usage and verification
- MCP (default): in Claude Code / Codex, run
/mcp, then ask the agent to compress a large log or search result; retrieve the original withheadroom_retrieve <hash>when exact line-level evidence is needed. Compress for triage/summary; retrieve originals before exact edits or claims — do not compress source files you are about to edit. - Wrap (optional):
headroom wrap claude/headroom wrap codex/headroom wrap copilotroutes supported CLI provider traffic through the proxy for automatic compression. - Metrics:
headroom perf --hours 1, or the proxy endpointsGET /statsandGET /stats-historyonhttp://127.0.0.1:8787.
H.9 Troubleshooting
| Symptom | Cause / Fix |
|---|---|
| MCP server shows "failed to connect" | Registered as bare headroom (PATH-dependent). Re-add with the absolute ...\.venvs\headroom\Scripts\headroom.exe path. |
Codex desktop shows No Chats after headroom wrap codex | Check C:\Users\whertzing\.codex\config.toml for a global model_provider = "headroom" / openai_base_url = "http://127.0.0.1:8787/v1" override. Back up the file, remove only that override, keep [mcp_servers.headroom], then fully restart Codex. Confirm history still exists with Python's built-in sqlite3 module by querying C:\Users\whertzing\.codex\state_5.sqlite and grouping threads by model_provider and cwd. |
headroom wrap codex reports telemetry enabled | Set [Environment]::SetEnvironmentVariable('HEADROOM_TELEMETRY', 'off', 'User'), open a fresh shell, and relaunch. |
headroom wrap codex says codex not found | Add C:\Users\whertzing\AppData\Local\OpenAI\Codex\bin to user PATH, open a fresh shell, and verify Get-Command codex. |
| App or VS Code extension shows no proxy traffic | It is not inheriting or honoring the proxy base URL. Configure the extension's custom base URL/BYOK setting if available; otherwise use the wrapped CLI. |
| First compression hangs for minutes | CPU model cold-load (Path A). Expected once per process; switch to Path B for GPU. |
headroom_retrieve returns nothing | Local/proxy retention window expired — reacquire the original content. |
pip cert errors during install | Add --use-feature=truststore (Windows cert store). |
Cargo CRYPT_E_NO_REVOCATION_CHECK | Set CARGO_HTTP_CHECK_REVOKE=false before the install (see §H.3). |
link.exe / cl.exe not found | Load VsDevCmd.bat (§H.3) so the MSVC toolchain is on PATH. |
| Path B still CPU-only | CUDA torch / onnxruntime-gpu mismatch with driver CUDA level; reinstall the matching cuXXX wheel. |
| Proxy window flashes / steals focus | Use the cscript VBScript wrapper (§H.5), not a cmd/pwsh task action. |