# #302 ENLYZEAM operator staging kit — release 3142753a, operator-only

This is the explicitly ordered **manual operator cutover**, not `spt update apply` and not the signed debug-channel rollout. `docs/DEBUG-ROLLOUT.md` normally requires a node-local debug pin and signed metadata. ENLYZEAM has no `identity/release-keys.json`; do not create one or imply that copying files satisfies that runbook. Only the human operator executes the cutover commands below. Authoring used read-only task/process/endpoint queries; no resident mutation occurred.

Known field facts: Windows 10 22H2 19045.6466; installed spt 0.70.0 at `%LOCALAPPDATA%\spt-core\bin\spt.exe`; daemon PID 48932, parent 49172, session 0, `daemon run --detached`; scheduled task `spt-core daemon`; live `ball-b` and `engine-room`, five other suspended endpoints; approximately 15 GB free on C:. These are baseline facts, **not reusable PID authority**.

Main's read-only receipt `.spt/preserved/302/daemon-rehearsal/enlyzeam-task-readonly.json` now supplies the actual XML: principal SID `S-1-5-21-447417125-33635525-555367570-1001`, `InteractiveToken`, one `LogonTrigger`, `MultipleInstancesPolicy=IgnoreNew`, command `C:\Users\decid\AppData\Local\spt-core\bin\spt.exe`, arguments `daemon run --detached`. The XML does **not explicitly contain** RunLevel, RestartOnFailure or AllowHardTerminate; omission is not proof of a particular effective value. It contains battery-start/stop restrictions and idle settings; keep the machine on AC and review effective policy before setting the manifest review gate. The current daemon is owned by `ENLYZEAM\decid`; parent 49172 is no longer present. LastTaskResult `3221225786` is historical, not current readiness or a reason to restart anything automatically.

The existing session-0 daemon and the task's `InteractiveToken` registration are distinct observations, not proof of the next process's session. Initial identity pins the observed session 0. After `/Run`, identify the new daemon by exact executable, parsed `daemon run --detached` arguments, owner SID, fresh creation time, live broker PID and unchanged task action; record its actual session without requiring session 0. An absent interactive logon may make the task launch fail; that is a failed restart, not authority to rewrite the task.

The read-only task-state receipt reports `Ready` (numeric 3) while the detached daemon remains alive. Therefore `/End` is conditional on an actually running task instance; a Ready task is not an error to suppress. The retained, fully identified daemon handle remains the stop authority when the task has already exited. Queued/disabled/unknown task state refuses.

Effective-settings readback (`enlyzeam-task-effective-settings.json`): enabled, `AllowHardTerminate=true`, `RestartCount=0`, no restart interval, `ExecutionTimeLimit=PT72H`, `IgnoreNew`, least-privilege run level, empty working directory. The finite staging/capture window is below that execution limit. AC remains required by both battery restrictions.

Census discrepancy, retained rather than “corrected”: Main's normal status sees `ball-b`, `engine-room`, `mobile-ez` (gateway, alive false/ready true) and `okabe-r` (alive false/ready false). The full endpoint list omits engine-room from its subnet projection and reports the five `chert`, `emphasys`, `hfen-user`, `liam`, `swan` as suspended on **HFENDULEAM**, not as five local ENLYZEAM rows; other remote suspended rows also exist. Preserve both complete surfaces before/after. The supplied “five suspended” requirement remains protection scope, not a license to relocate or mutate those endpoints. No lifecycle command may target anything except the two named local endpoints.

## Pinned release and verification limits

The RELEASE build is source `3142753a8e4eedf1404f75a82eea88bcb3331a32`, opt-level 3, line-tables-only, strip none. Executable: **39,186,944 bytes**, SHA256 `67dbe7113554af073d20b0cb7fd9c8ceb613c70574ed68d9e705f572b78ab140`. Matching PDB: **128,110,592 bytes**, SHA256 `61b41db2473065131932d669ae5f2498b8f322afb344dd4d2b74fcc16f05aa8a`. Both identify GUID `64E2A01E-CD61-4576-B9F5-DA6F8F1AAFB2`, age 1. Producer capsule manifest SHA256 `c5ac46ca9b7bd580b68004f28c86d6ef9af6fed12f788958b2aa690e45ae48db`. Main independently hashed the retained CLI/PDB and verified every packaged member. This kit is not automatic staging approval; no resident cutover has been performed.

Final producer handoff: `.spt/preserved/302/todlando-release/handoff.json`, SHA256 `464351065f85334390d3f9f8cba55fb441f0011459c33ede2c296e4417d8a39f`, reports release proofs complete. Its executable/PDB hashes match this package. The package's smaller producer manifest is the immutable build-time capsule, so its historical “proofs pending” status is not rewritten. Producer fixture evidence remains separate from the actual-daemon rehearsal below and does not establish ENLYZEAM causality.

Required manifest fields (names are the contract used by the commands):

- `build_commit`, `build_profile` (`release+line-tables`), `cli_version` (complete trimmed `--version` output), `broker_version` (the live status version string).
- `exe`: `path`, `sha256`, `bytes`; `pdb`: same. Paths are absolute drive-local, already transferred to ENLYZEAM. Producer must attest the executable CodeView GUID/age matches this PDB; hashes alone do not establish pairing.
- `symbols_source`, `symbols_destination`: absolute drive-local directories, destination a new unique directory below the resident home, never an existing symbol cache. `symbols`: nonempty array of `relative_path`, `sha256`, `bytes` for **only flat OS `*.pdb` files plus `manifest.json`**. Exclude `attempts/` and every subdirectory; do not recursively copy the producer's workspace. The matched release `spt.pdb` is copied separately into this same directory AND beside `bin\spt.exe`. No `..`, rooted paths, reparse points, network paths, or symbol-server URLs.
- `os_build`: exactly `19045.6466`. Producer must supply loaded-image/PDB GUID+age matching evidence for ENLYZEAM, not merely an OS marketing version or HFENDULEAM PDBs.
- Final `.spt/preserved/302/enlyzeam-symbols/manifest.json` SHA256: `6431f18c6e54db4c4834d13a8b66c1c8a43d5c40c4e0fa7797b0960f73221978`. It describes 15 matched OS PDBs plus the matched release application PDB. Its `search_path_environment` remains HFENDULEAM-local provenance: **never use that path on ENLYZEAM**. The package contains only flat OS PDBs and this manifest under `symbols/`; application `release/spt.pdb` is copied into that same destination by section 4. No `attempts/` or producer workspace is transferred.
- `collector_path`, `collector_sha256`: transferred `step1-collect.ps1`, pinned by Main.
- `task_xml_sha256`: SHA256 of UTF-8 bytes **without BOM** of `([xml]$xmlText).OuterXml`, using default `PreserveWhitespace=false`, after parsing the exact `/Query /TN ... /XML` result. Main supplies the identically calculated canonical pin; raw CRLF/CRCRLF capture transport is not task drift. Preserve the raw XML separately as evidence. `task_user_sid`: `S-1-5-21-447417125-33635525-555367570-1001`. `task_process_command_line`: preserve the observed `C:\Users\decid\AppData\Local\spt-core\bin\spt.exe  daemon run --detached` (two spaces before `daemon`) as baseline evidence; runtime identity matching below permits only equivalent quoting/whitespace, not additional arguments. `task_session_id`: `0` for the initial daemon only; a restarted daemon's session is observed, not predicted. `suspended_ids`: the five exact endpoint IDs, excluding the two named live endpoints.
- Main's canonical task pin is now `4fcc291cf528d587b0f5fa7f221dddafeb6534ad50080068c4718af496059dbf`, retained in `.spt/preserved/302/daemon-rehearsal/enlyzeam-task-canonical-pin.json`. Use this exact value for `task_xml_sha256`; do not recompute a pin from unreviewed changed task XML during cutover.
- `task_policy_reviewed`: true only after reviewing action, principal/logon type, working directory, triggers, restart policy, concurrent-instance policy and default resident-home resolution. Require precisely one action: the pinned installed executable plus `daemon run --detached`. No additional actions, periodic restart triggers, or automatic retries that could race this manual window. If the existing task does not meet this, STOP for a separately authorized plan; do not edit it here.
- `symbol_pairing_reviewed`: true only after the producer's executable/PDB and OS-image/PDB matching evidence is attached.

Task identity and effective policy values are grounded in retained read-only receipts. Environment inheritance remains a cutover-time gate. ENLYZEAM payload destination: `C:\Users\decid\AppData\Local\spt-core\preserved\302\payload-3142753a`; new symbols destination: sibling `symbols-3142753a`. Staging manifest SHA256: `3993d26174a186b96a41c1a1498a9887ba05da3e4a41ecc5d9721e0b59c18508`. No files have been transferred.

**Environment caveat:** setting USER variables does not guarantee a session-0 task sees them. `/Run` uses the saved task account/action, not the calling PowerShell environment. Task Scheduler documents cached environment expansion at task-engine launch. This kit never kills `taskeng.exe`, restarts Scheduler, changes task actions, writes SYSTEM environment, or creates a wrapper task. A fresh diagnostic START proves diagnostic activation, not the symbol-directory value. After restart the operator must inspect the **new identified daemon's** Environment page in Process Explorer (if accessible), checking only the two named variables. If inaccessible, stale or wrong: rollback; do not improvise an inheritance workaround. References: [ExecAction](https://learn.microsoft.com/en-us/windows/win32/taskschd/execaction), [schtasks /Run](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-run).

## 0. Operator transfer and extraction — no agent executes this

The finite ZIP is `.spt/preserved/302/302-operator-3142753a.zip`: **187,740,252 bytes**, SHA256 **`03f1d9c755591ba089f5fa9615778900bd4a9ca283a4151f70065a6736e17928`**. Its 21 entries contain the release pair, 15 flat OS PDBs, final symbol manifest, producer manifest, staging manifest and collector. Extraction does not replace installed files. Destination installation copies total 315,826,225 bytes, plus the original backup; section 2 independently enforces 5 GiB free-space reserve.

First, in a human PowerShell on **ENLYZEAM**, refuse an existing transfer or payload:

```powershell
$ErrorActionPreference = 'Stop'
if ($env:COMPUTERNAME -ine 'ENLYZEAM') { throw 'Wrong host' }
$Zip = 'C:\Users\decid\302-operator-3142753a.zip'
$Destination = 'C:\Users\decid\AppData\Local\spt-core\preserved\302\payload-3142753a'
if ((Test-Path -LiteralPath $Zip) -or (Test-Path -LiteralPath $Destination)) { throw 'Transfer/payload path already exists; preserve it, do not overwrite' }
if ([IO.DriveInfo]::new('C:\').AvailableFreeSpace -lt 6GB) { throw 'Insufficient transfer/extraction headroom' }
$cursor = $Destination
while ($cursor) {
    if ((Test-Path -LiteralPath $cursor) -and ((Get-Item -LiteralPath $cursor -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw "Reparse point refused: $cursor" }
    $cursor = Split-Path -Parent $cursor
}
```

Then, in a human PowerShell on **HFENDULEAM**, transfer only that pinned ZIP. SSH authentication must already work; BatchMode refuses a credential prompt. Five-minute transfer cap, owned-client termination only:

```powershell
$ErrorActionPreference = 'Stop'
if ($env:COMPUTERNAME -ine 'HFENDULEAM') { throw 'Wrong host' }
$Source = 'C:\Users\decid\Documents\projects\spt-core\.spt\preserved\302\302-operator-3142753a.zip'
if ((Get-FileHash -LiteralPath $Source -Algorithm SHA256).Hash -ine '03f1d9c755591ba089f5fa9615778900bd4a9ca283a4151f70065a6736e17928') { throw 'ZIP pin mismatch' }
$copy = Start-Process -FilePath (Get-Command scp.exe).Source -ArgumentList @('-o','BatchMode=yes','-o','ConnectTimeout=10',$Source,'decid@enlyzeam:C:/Users/decid/302-operator-3142753a.zip') -NoNewWindow -PassThru
try {
    [void]$copy.Handle
    if (-not $copy.WaitForExit(300000)) {
        $copy.Kill()
        if (-not $copy.WaitForExit(2000)) { throw 'Transfer-client termination unconfirmed' }
        throw 'Transfer timed out; preserve partial ZIP, do not extract'
    }
    if ($copy.ExitCode -ne 0) { throw "Transfer failed: $($copy.ExitCode)" }
} finally { $copy.Dispose() }
```

Back in the same human **ENLYZEAM** shell, verify the complete ZIP before extraction. The child performs only finite local extraction, with a 120-second cap; failure preserves partial payload and stops:

```powershell
if ((Get-Item -LiteralPath $Zip).Length -ne 187740252 -or (Get-FileHash -LiteralPath $Zip -Algorithm SHA256).Hash -ine '03f1d9c755591ba089f5fa9615778900bd4a9ca283a4151f70065a6736e17928') { throw 'Transferred ZIP mismatch' }
if (Test-Path -LiteralPath $Destination) { throw 'Payload destination appeared; refuse overwrite' }
$body = '$ErrorActionPreference="Stop"; try { Expand-Archive -LiteralPath "C:\Users\decid\302-operator-3142753a.zip" -DestinationPath "C:\Users\decid\AppData\Local\spt-core\preserved\302\payload-3142753a"; exit 0 } catch { [Console]::Error.WriteLine($_); exit 1 }'
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($body))
$unpack = Start-Process -FilePath (Join-Path $PSHOME 'powershell.exe') -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand',$encoded) -NoNewWindow -PassThru
try {
    [void]$unpack.Handle
    if (-not $unpack.WaitForExit(120000)) {
        $unpack.Kill()
        if (-not $unpack.WaitForExit(2000)) { throw 'Extraction-client termination unconfirmed' }
        throw 'Extraction timed out; do not stage partial payload'
    }
    if ($unpack.ExitCode -ne 0) { throw "Extraction failed: $($unpack.ExitCode)" }
} finally { $unpack.Dispose() }
if ((Get-FileHash -LiteralPath (Join-Path $Destination 'staging-manifest.json') -Algorithm SHA256).Hash -ine '3993d26174a186b96a41c1a1498a9887ba05da3e4a41ecc5d9721e0b59c18508') { throw 'Extracted staging manifest mismatch' }
```

## 1. Human shell and bounded helpers

Use Windows PowerShell 5.1 in a separate human-owned terminal on ENLYZEAM, under the same user SID as the task principal; elevate that user's shell only if needed to inspect session 0. Never run these blocks inside an endpoint/agent shell, and never clear agent markers to evade that restriction. Keep this one shell open through rollback. Stop at any exception; later blocks are not an automatic continuation. All external invocations below have deadlines and retain only their own client process handle. A timed-out query is not permission to kill the resident.

Use these pinned values; do not replace a failed pin with a locally recomputed one:

```powershell
$ManifestPath = 'C:\Users\decid\AppData\Local\spt-core\preserved\302\payload-3142753a\staging-manifest.json'
$ManifestSha256 = '3993d26174a186b96a41c1a1498a9887ba05da3e4a41ecc5d9721e0b59c18508'
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2
if ($env:COMPUTERNAME -ine 'ENLYZEAM') { throw 'Wrong machine' }
foreach ($n in 'OWL_SESSION_ID','SPT_AGENT_ID','SPT_ENDPOINT_ID') {
    if ([Environment]::GetEnvironmentVariable($n, 'Process')) { throw 'Use a human-owned external shell; do not scrub agent identity' }
}
if ($ManifestSha256 -notmatch '^[0-9a-fA-F]{64}$') { throw 'Unfilled release manifest pin' }
if ((Get-FileHash -LiteralPath $ManifestPath -Algorithm SHA256).Hash -ine $ManifestSha256) { throw 'Manifest hash mismatch' }
$m = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
$HomePath = Join-Path $env:LOCALAPPDATA 'spt-core'
$Binary = Join-Path $HomePath 'bin\spt.exe'
$Pdb = Join-Path $HomePath 'bin\spt.pdb'
$Log = Join-Path $HomePath 'logs\daemon.stderr.log'
$TaskName = 'spt-core daemon'
$Run = Join-Path $HomePath ('preserved\302\operator-' + [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssfffZ'))
if (Test-Path -LiteralPath $Run) { throw 'Run directory already exists' }
[void](New-Item -ItemType Directory -Path $Run)
$script:CallNo = 0
$PowerShell = Join-Path $PSHOME 'powershell.exe'
$SchTasks = Join-Path $env:SystemRoot 'System32\schtasks.exe'
$PriorProcessDiag = $env:SPT_RC_HITCH_DIAG
$PriorProcessHome = $env:SPT_HOME
$env:SPT_HOME = $HomePath
$env:SPT_RC_HITCH_DIAG = $null # normal status shape in this HUMAN shell only

function Quote-Arg([string]$Value) {
    if ($Value.Contains('"') -or $Value.Contains("`r") -or $Value.Contains("`n")) { throw 'Unsupported quote/newline in argument' }
    # All supplied arguments are flags, IDs or file paths, never trailing-backslash directories.
    if ($Value.EndsWith('\')) { throw 'Remove trailing backslash from argument' }
    return '"' + $Value + '"'
}
function Invoke-Bounded([string]$Exe, [string[]]$Argv, [int]$Seconds = 15) {
    if ($Seconds -lt 1 -or $Seconds -gt 210) { throw 'Invalid command deadline' }
    $script:CallNo++
    $out = Join-Path $Run ('call-{0:D3}.stdout.txt' -f $script:CallNo)
    $err = Join-Path $Run ('call-{0:D3}.stderr.txt' -f $script:CallNo)
    $line = ($Argv | ForEach-Object { Quote-Arg $_ }) -join ' '
    $p = Start-Process -FilePath $Exe -ArgumentList $line -PassThru -NoNewWindow -RedirectStandardOutput $out -RedirectStandardError $err
    try {
        [void]$p.Handle # retained owned CLIENT identity, never a PID/tree kill
        if (-not $p.WaitForExit($Seconds * 1000)) {
            $p.Kill()
            if (-not $p.WaitForExit(2000)) { throw "Owned client termination unconfirmed: $out" }
            throw "Command timed out; stop and inspect partial evidence: $out / $err"
        }
        # No parameterless WaitForExit: inherited pipe handles must not hang this shell.
        $exit = $p.ExitCode
        foreach ($f in $out,$err) {
            if ((Get-Item -LiteralPath $f).Length -gt 16MB) { throw "Output too large: $f" }
        }
        $text = [IO.File]::ReadAllText($out)
        $errorText = [IO.File]::ReadAllText($err)
        if ($exit -ne 0) { throw "Command exit ${exit}: $errorText (evidence: $out)" }
        return $text
    } finally { $p.Dispose() }
}
function Invoke-ReadPs([string]$Body) {
    # Read-only queries only. Failure/timeout never grants stop authority.
    $code = '$ErrorActionPreference="Stop"; try { & { ' + $Body + ' }; exit 0 } catch { [Console]::Error.WriteLine($_); exit 1 }'
    $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($code))
    Invoke-Bounded $PowerShell @('-NoProfile','-NonInteractive','-EncodedCommand',$encoded) 15
}
function Read-SharedLog {
    # Share the live writer's existing access; File.ReadAllLines/ReadAllText
    # deny sharing with this daemon log on Windows.
    $s = [IO.FileStream]::new($Log, [IO.FileMode]::Open, [IO.FileAccess]::Read, ([IO.FileShare]::ReadWrite -bor [IO.FileShare]::Delete))
    try {
        $length = $s.Length
        if ($length -gt 16MB) { throw 'Live log exceeds bounded snapshot' }
        $bytes = New-Object byte[] ([int]$length)
        $offset = 0
        while ($offset -lt $length) {
            $n = $s.Read($bytes,$offset,[int]($length-$offset))
            if ($n -eq 0) { throw 'Live log truncated during snapshot' }
            $offset += $n
        }
        [Text.Encoding]::UTF8.GetString($bytes)
    } finally { $s.Dispose() }
}
function Read-HostedSessions([int]$BrokerPid, [string]$PipeName = 'spt-daemon-broker-78bf18a9fd96fdc6.sock') {
    # Pipe name was observed in ENLYZEAM's bounded shared log read.
    # It is a candidate, not authority: verify its server PID before requests.
    if ($PipeName -notmatch '^spt-daemon-broker-[0-9a-f]{16}\.sock$') { throw 'Invalid reviewed broker pipe name' }
    $body = @'
Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class CensusPipeIdentity { [DllImport("kernel32.dll", SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetNamedPipeServerProcessId(IntPtr pipe, out uint pid); }'
$pipe = [IO.Pipes.NamedPipeClientStream]::new('.', $PipeName, [IO.Pipes.PipeDirection]::InOut)
try {
    $pipe.Connect(2000)
    [uint32]$actual = 0
    if (-not [CensusPipeIdentity]::GetNamedPipeServerProcessId($pipe.SafePipeHandle.DangerousGetHandle(), [ref]$actual) -or $actual -ne $BrokerPid) { throw 'Broker pipe PID mismatch; no request sent' }
    foreach ($frame in @(
        @{protocol_version=1;kind='hello';payload=@{protocol_version=1;role='brain'}},
        @{protocol_version=1;kind='sessions';payload=$null}
    )) {
        $bytes = [Text.Encoding]::UTF8.GetBytes(($frame | ConvertTo-Json -Depth 5 -Compress))
        $prefix = [BitConverter]::GetBytes([Net.IPAddress]::HostToNetworkOrder([int]$bytes.Length))
        $pipe.Write($prefix,0,4); $pipe.Write($bytes,0,$bytes.Length); $pipe.Flush()
    }
    $reader = [IO.BinaryReader]::new($pipe)
    $prefix = $reader.ReadBytes(4)
    if ($prefix.Length -ne 4) { throw 'Census prefix EOF' }
    $length = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($prefix,0))
    if ($length -lt 1 -or $length -gt 16MB) { throw 'Census frame outside byte bound' }
    $bytes = $reader.ReadBytes($length)
    if ($bytes.Length -ne $length) { throw 'Census body EOF' }
    $reply = [Text.UTF8Encoding]::new($false,$true).GetString($bytes) | ConvertFrom-Json
    if ($reply.protocol_version -ne 1 -or $reply.kind -ne 'sessions-reply') { throw 'Unexpected census protocol/reply' }
    if ($null -eq $reply.payload.PSObject.Properties['sessions'] -or $reply.payload.sessions -isnot [Array]) { throw 'Missing/non-array session census' }
    $bringingUp = @()
    if ($null -ne $reply.payload.PSObject.Properties['bringing_up']) {
        if ($reply.payload.bringing_up -isnot [Array]) { throw 'Malformed bring-up census' }
        $bringingUp = @($reply.payload.bringing_up)
    }
    [ordered]@{broker_pid=$actual;pipe=$PipeName;observed_utc=[DateTime]::UtcNow.ToString('o');sessions=@($reply.payload.sessions);bringing_up=$bringingUp} | ConvertTo-Json -Depth 20 -Compress
} finally { $pipe.Dispose() }
'@
    Invoke-ReadPs ('$BrokerPid=' + $BrokerPid + ";`n" + '$PipeName=''' + $PipeName + "';`n" + $body) | ConvertFrom-Json
}
function Assert-Hash([string]$Path, [string]$Hash) {
    if ($Hash -notmatch '^[0-9a-fA-F]{64}$' -or (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash -ine $Hash) { throw "SHA256 mismatch: $Path" }
}
function Assert-LocalPath([string]$Path) {
    if ($Path -notmatch '^[A-Za-z]:\\' -or $Path.Contains('..')) { throw "Not an absolute drive-local path: $Path" }
    $cursor = $Path
    while ($cursor) {
        if ((Test-Path -LiteralPath $cursor) -and ((Get-Item -LiteralPath $cursor).Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw "Reparse point refused: $cursor" }
        $parent = Split-Path -Parent $cursor
        if ($parent -eq $cursor) { break }
        $cursor = $parent
    }
}
function Read-Processes {
    $json = Invoke-ReadPs 'Get-CimInstance Win32_Process -OperationTimeoutSec 8 | Select-Object ProcessId,ParentProcessId,SessionId,ExecutablePath,CommandLine,@{n="CreatedUtc";e={$_.CreationDate.ToUniversalTime().ToString("o")}} | ConvertTo-Json -Depth 4 -Compress'
    $rows = @($json | ConvertFrom-Json)
    if ($rows.Count -eq 0) { throw 'Empty process census is not absence proof' }
    return $rows
}
function Resident-Rows {
    @(Read-Processes | Where-Object { $_.ExecutablePath -ieq $Binary -and $_.CommandLine -match '\bdaemon\s+run\b' })
}
function Read-Status {
    Invoke-Bounded $Binary @('daemon','status','--json') 15 | ConvertFrom-Json
}
function Assert-Task {
    $xmlText = Invoke-Bounded $SchTasks @('/Query','/TN',$TaskName,'/XML') 15
    [IO.File]::WriteAllText((Join-Path $Run 'task.xml'), $xmlText)
    [xml]$x = $xmlText # default PreserveWhitespace=false
    $canonicalBytes = [Text.UTF8Encoding]::new($false).GetBytes($x.OuterXml)
    $sha = [Security.Cryptography.SHA256]::Create()
    try { $h = ([BitConverter]::ToString($sha.ComputeHash($canonicalBytes))).Replace('-','') } finally { $sha.Dispose() }
    if ($h -ine $m.task_xml_sha256) { throw 'Task XML drift; no task action authorized' }
    $exec = @($x.SelectNodes('//*[local-name()="Actions"]/*'))
    if ($exec.Count -ne 1 -or $exec[0].LocalName -ne 'Exec' -or [string]$exec[0].Command -ine $Binary -or [string]$exec[0].Arguments -ne 'daemon run --detached') { throw 'Task action mismatch' }
}
function Read-Census {
    Invoke-Bounded $Binary @('endpoint','list','--show-all','--workers','--json') 20 | ConvertFrom-Json
}
function Assert-Preserved($Status, $Census) {
    # Status is only the local surface; do not require remote suspended IDs here.
    $oldLocal = @($Before.local_endpoints | Where-Object { $_.id -notin @('ball-b','engine-room') } | Sort-Object id | Select-Object id,state,address,ready,alive,unbound) | ConvertTo-Json -Depth 8 -Compress
    $newLocal = @($Status.local_endpoints | Where-Object { $_.id -notin @('ball-b','engine-room') } | Sort-Object id | Select-Object id,state,address,ready,alive,unbound) | ConvertTo-Json -Depth 8 -Compress
    if ($oldLocal -cne $newLocal) { throw 'Non-target local status changed or disappeared; escalate, do not repair it' }
    $protected = @($BeforeCensus.subnets | ForEach-Object { $_.endpoints } | Where-Object { $_.status -eq 'Suspended' } | Select-Object node,id,status -Unique)
    $current = @($Census.subnets | ForEach-Object { $_.endpoints } | Select-Object node,id,status -Unique)
    foreach ($old in $protected) {
        $found = @($current | Where-Object { $_.node -eq $old.node -and $_.id -eq $old.id })
        if ($found.Count -ne 1 -or $found[0].status -cne $old.status) { throw ('Protected suspended row missing/drifted: ' + $old.id + '@' + $old.node) }
    }
}
$LastTaskLaunchUtc = $null
function Hold-Resident([int]$ExpectedSession = -1, [DateTime]$BornAfter = [DateTime]::MinValue) {
    $rows = @(Resident-Rows)
    if ($rows.Count -ne 1) { throw 'Expected exactly one identifiable resident daemon' }
    $r = $rows[0]
    $exePattern = [Regex]::Escape($Binary)
    $commandPattern = '^(?:"' + $exePattern + '"|' + $exePattern + ')\s+daemon\s+run\s+--detached\s*$'
    if ($r.CommandLine -notmatch $commandPattern -or ($ExpectedSession -ge 0 -and $r.SessionId -ne $ExpectedSession)) { throw 'Daemon command/initial-session identity drift' }
    if ([DateTime]::Parse($r.CreatedUtc).ToUniversalTime() -lt $BornAfter) { throw 'Daemon is not fresh for the reviewed task launch' }
    $s = Read-Status
    if (-not $s.running -or [string]$s.pid -ne [string]$r.ProcessId) { throw 'Live broker identity missing/mismatched; do not use PID file' }
    $owner = Invoke-ReadPs ('Invoke-CimMethod -InputObject (Get-CimInstance Win32_Process -Filter "ProcessId=' + $r.ProcessId + '" -OperationTimeoutSec 8) -MethodName GetOwnerSid -OperationTimeoutSec 8 | ConvertTo-Json -Compress') | ConvertFrom-Json
    if ($owner.ReturnValue -ne 0 -or $owner.Sid -ne $m.task_user_sid) { throw 'Daemon owner SID mismatch' }
    $p = [Diagnostics.Process]::GetProcessById([int]$r.ProcessId)
    [void]$p.Handle
    # WMI timestamps have microsecond precision; Process.StartTime may retain finer ticks.
    $deltaMs = [Math]::Abs(($p.StartTime.ToUniversalTime() - [DateTime]::Parse($r.CreatedUtc).ToUniversalTime()).TotalMilliseconds)
    if ($p.HasExited -or $p.MainModule.FileName -ine $Binary -or $deltaMs -ge 1) { $p.Dispose(); throw 'Daemon changed during identity acquisition' }
    $r | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Run ('daemon-' + $r.ProcessId + '.json')) -Encoding UTF8
    return $p
}
function Assert-Absent {
    $writers = @(Read-Processes | Where-Object { $_.ExecutablePath -ieq $Binary -and $_.CommandLine -match '\bdaemon\s+(run|brain)\b' })
    if ($writers.Count -ne 0) { throw 'Resident broker/brain still present or respawned; no file mutation/start allowed' }
}
function End-RunningTask {
    $taskState = (Invoke-ReadPs "(Get-ScheduledTask -TaskName 'spt-core daemon').State.ToString()").Trim()
    if ($taskState -eq 'Running') {
        Invoke-Bounded $SchTasks @('/End','/TN',$TaskName) 15 | Out-Host
    } elseif ($taskState -ne 'Ready') {
        throw "Task state cannot be safely ended/restarted: $taskState"
    }
}
function Stop-ExactResident($Held) {
    Assert-Task
    if ($null -eq $Held -or $Held.HasExited) { throw 'Retained daemon identity no longer live; re-inventory, do not guess' }
    $hosted = Read-HostedSessions $Held.Id
    $hosted | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $Run ('hosted-before-stop-' + [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssfffZ') + '.json')) -Encoding UTF8
    if ($Held.HasExited -or @($hosted.sessions).Count -ne 0 -or @($hosted.bringing_up).Count -ne 0) { throw 'Broker has hosted/in-flight sessions or identity changed; no stop authorized' }
    End-RunningTask
    if (-not $Held.WaitForExit(5000)) {
        # Task End may leave its detached daemon. This handle was retained after
        # path, command, creation time, session, owner and live-broker checks.
        # If the task was Ready, only this already-retained daemon handle is terminated.
        # No PID-only kill, parent kill, process-name kill or tree kill.
        $Held.Kill()
        if (-not $Held.WaitForExit(5000)) { throw 'Exact daemon death unconfirmed; halt' }
    }
    # The broker's death is not proof that its brain released the image/log.
    # Read-only bounded wait for normal child exit; never broaden kill authority.
    for ($i=0; $i -lt 3; $i++) {
        $brains = @(Read-Processes | Where-Object { $_.ExecutablePath -ieq $Binary -and $_.CommandLine -match '\bdaemon\s+brain\b' })
        if ($brains.Count -eq 0) { break }
        if ($i -lt 2) { Start-Sleep -Seconds 1 }
    }
    # Recheck the complete writer census below, not only the original PID.
    Assert-Absent
}
```

The helper bounds process invocations/waits. Local hash/copy work is bounded by the finite file list and byte ceiling, not a hard disk-I/O deadline. PowerShell 5.1 helper smoke passed: actual `spt --version` and a complete 1,048,576-character JSON payload from a bounded read-only child; receipt `daemon-rehearsal/kit-helper-proof/stdout-3.txt`. Task stop/start, executable replacement and USER environment writes remain operator-only and were not exercised by that smoke. Storage/permission failure requires absence proof before any rollback copy.

## 2. Preflight and backup before any endpoint/task/binary mutation

Before any mutation, confirm Process Explorer is available and that this human account can inspect the existing daemon's Environment page. Do not proceed hoping access will become available after shutdown. This read-only access check is a prerequisite for proving both variables in the replacement process; it does not authorize a debugger attach.

```powershell
if ($m.build_profile -ne 'release+line-tables' -or $m.os_build -ne '19045.6466' -or -not $m.task_policy_reviewed -or -not $m.symbol_pairing_reviewed) { throw 'Release/task/symbol review incomplete' }
if ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -ne $m.task_user_sid) { throw 'Wrong USER hive for saved task principal' }
if (@($m.suspended_ids).Count -ne 5 -or @($m.suspended_ids | Select-Object -Unique).Count -ne 5 -or @($m.suspended_ids | Where-Object { $_ -in @('ball-b','engine-room') }).Count) { throw 'Invalid five-endpoint preservation set' }
Assert-Task
foreach ($p in @($Binary,$Pdb,$m.exe.path,$m.pdb.path,$m.symbols_source,$m.symbols_destination,$m.collector_path,$Run)) { Assert-LocalPath $p }
if (-not $m.symbols_destination.StartsWith($HomePath + '\', [StringComparison]::OrdinalIgnoreCase) -or (Test-Path -LiteralPath $m.symbols_destination)) { throw 'Symbols destination must be a new resident-home directory' }
Assert-Hash $m.exe.path $m.exe.sha256
Assert-Hash $m.pdb.path $m.pdb.sha256
Assert-Hash $m.collector_path $m.collector_sha256
$Symbols = @($m.symbols)
if ($Symbols.Count -lt 1 -or $Symbols.Count -gt 10000) { throw 'Missing/excessive symbol manifest' }
$Seen = @{}
$PayloadBytes = [long]$m.pdb.bytes # second copy: matched release PDB in symbol directory
foreach ($a in @($m.exe,$m.pdb)) {
    if ((Get-Item -LiteralPath $a.path).Length -ne [long]$a.bytes) { throw 'Artifact size mismatch' }
    $PayloadBytes += [long]$a.bytes
}
foreach ($a in $Symbols) {
    $rel = [string]$a.relative_path
    if ([IO.Path]::IsPathRooted($rel) -or $rel -match '(^|[\\/])\.\.([\\/]|$)' -or $rel.Contains(':') -or $Seen.ContainsKey($rel)) { throw 'Unsafe/duplicate symbol path' }
    if ($rel -match '[\\/]' -or ($rel -ine 'manifest.json' -and $rel -notmatch '^[^:]+\.pdb$') -or $rel -ieq 'spt.pdb') { throw 'Only flat OS PDBs + manifest.json allowed; release PDB handled separately' }
    $Seen[$rel] = $true
    $src = Join-Path $m.symbols_source $rel
    Assert-LocalPath $src
    Assert-Hash $src $a.sha256
    if ((Get-Item -LiteralPath $src).Length -ne [long]$a.bytes) { throw 'Symbol size mismatch' }
    $PayloadBytes += [long]$a.bytes
}
if (-not $Seen.ContainsKey('manifest.json')) { throw 'Missing pinned OS-symbol manifest.json' }
if ($PayloadBytes -gt 6GB) { throw 'Payload exceeds 6 GiB budget; no on-node build/download/extraction' }
$OldPdbExists = Test-Path -LiteralPath $Pdb
$BackupBytes = (Get-Item -LiteralPath $Binary).Length
if ($OldPdbExists) { $BackupBytes += (Get-Item -LiteralPath $Pdb).Length }
# Sources are already on-node: remaining demand is destination copies + rollback.
$Free = [IO.DriveInfo]::new('C:\').AvailableFreeSpace
if ($Free -lt ($PayloadBytes + $BackupBytes + 5GB)) { throw 'Insufficient free space: preserve 5 GiB headroom; do not delete resident/user data' }
$OldVersion = (Invoke-Bounded $Binary @('--version') 10).Trim()
$OldHash = (Get-FileHash -LiteralPath $Binary -Algorithm SHA256).Hash
$OldPdbHash = $null
if ($OldPdbExists) { $OldPdbHash = (Get-FileHash -LiteralPath $Pdb -Algorithm SHA256).Hash }
$Original = [ordered]@{
    binary=$Binary; version=$OldVersion; exe_sha256=$OldHash; pdb_existed=$OldPdbExists; pdb_sha256=$OldPdbHash
    user_diag=[Environment]::GetEnvironmentVariable('SPT_RC_HITCH_DIAG','User')
    user_symbols=[Environment]::GetEnvironmentVariable('SPT_NET_DIAG_SYMBOL_DIR','User')
    process_diag=$PriorProcessDiag; process_home=$PriorProcessHome
}
$Original | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $Run 'original.json') -Encoding UTF8
Copy-Item -LiteralPath $Binary -Destination (Join-Path $Run 'spt.exe.original')
Assert-Hash (Join-Path $Run 'spt.exe.original') $OldHash
if ($OldPdbExists) {
    Copy-Item -LiteralPath $Pdb -Destination (Join-Path $Run 'spt.pdb.original')
    Assert-Hash (Join-Path $Run 'spt.pdb.original') $OldPdbHash
}
$Before = Read-Status
$Before | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $Run 'before.json') -Encoding UTF8
$BeforeCensus = Read-Census
$BeforeCensus | ConvertTo-Json -Depth 30 | Set-Content -LiteralPath (Join-Path $Run 'before-endpoints.json') -Encoding UTF8
foreach ($id in $m.suspended_ids) {
    $protectedBaseline = @($BeforeCensus.subnets | ForEach-Object { $_.endpoints } | Where-Object { $_.id -eq $id -and $_.status -eq 'Suspended' } | Select-Object node,id,status -Unique)
    if ($protectedBaseline.Count -ne 1) { throw "Missing/ambiguous protected suspended baseline: $id" }
}
$Before.local_endpoints | Format-Table id,state,alive,ready | Out-Host
$Held = Hold-Resident $m.task_session_id
```

Inspect both baselines: only `ball-b` and `engine-room` may be live **locally**; remote active agents are not part of this cutover. Record the five protected suspended rows by node+ID/status from the complete list, not from `local_endpoints`. The known five IDs are `chert`, `emphasys`, `hfen-user`, `liam`, `swan`, displayed on HFENDULEAM in Main's receipt. If a protected row is missing or its ownership/status is ambiguous, stop and escalate; never fabricate a local row. Keep backup, environment record and task XML even if staging later fails. Do not build/download symbols or unzip a large archive on the 15-GB-free machine.

## 3. Quiesce only the two named endpoints; lifecycle stays extension-owned

Ask the ball-b and engine-room extension owners to complete their continuity/save procedure and quiesce. They decide when shutdown is safe. Do not send lifecycle commands from an agent, and do not issue `ready`, `register`, `recharge`, bulk stop, or any command to the five suspended endpoints.

Only after each extension has confirmed its save, the human's exact bounded shutdown commands are:

```powershell
Invoke-Bounded $Binary @('endpoint','shutdown','ball-b') 45 | Out-Host
Invoke-Bounded $Binary @('endpoint','shutdown','engine-room') 45 | Out-Host
$Quiet = Read-Status
$Quiet | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $Run 'quiesced.json') -Encoding UTF8
$QuietHosted = Read-HostedSessions $Held.Id
$QuietHosted | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $Run 'quiesced-hosted-sessions.json') -Encoding UTF8
if (@($QuietHosted.sessions).Count -ne 0 -or @($QuietHosted.bringing_up).Count -ne 0) { throw 'Hosted/in-flight session remains; extension owner must resolve it' }
if (@($Quiet.local_endpoints | Where-Object { $_.alive }).Count -ne 0) { throw 'Live endpoint remains; do not stop the daemon' }
foreach ($id in @('ball-b','engine-room')) {
    $q = @($Quiet.local_endpoints | Where-Object { $_.id -eq $id })
    if ($q.Count -gt 1 -or @($q | Where-Object { $_.alive }).Count) { throw "Ambiguous/live target: $id" }
}
$QuietCensus = Read-Census
$QuietCensus | ConvertTo-Json -Depth 30 | Set-Content -LiteralPath (Join-Path $Run 'quiesced-endpoints.json') -Encoding UTF8
Assert-Preserved $Quiet $QuietCensus
```

`endpoint suspend` is not a substitute: it initiates a rest/echo-gate transition and does not prove immediate PTY teardown or completed save. `shutdown` additionally waits for confirmed broker-hosted teardown (15-second internal reap bound). Require successful shutdown receipts AND the above census; a suspended label alone is not enough. Any refusal/timeout ends this attempt; no automatic `stop`, `--force`, PID-tree kill or retry loop. Have the extension owner resolve it. Do not restart either endpoint automatically after staging or rollback.

## 4. Stop exact task/daemon, install checked payload, set USER variables

This deliberate stop uses only the reviewed task and retained exact daemon handle. It does not use `spt daemon stop`: that CLI writes a durable stop-inhibit, while this task's `daemon run --detached` action does not take the `start` path that clears it. Do not create/remove an inhibit file as a workaround.

```powershell
Stop-ExactResident $Held
$Held.Dispose(); $Held = $null
Assert-Absent
# Fresh-file epoch: preserve old log only after writer absence, never search an old append tail.
if (Test-Path -LiteralPath $Log) {
    Move-Item -LiteralPath $Log -Destination (Join-Path $Run 'daemon.stderr.before.log')
}
[void](New-Item -ItemType Directory -Path $m.symbols_destination)
foreach ($a in $Symbols) {
    $dst = Join-Path $m.symbols_destination $a.relative_path
    [void][IO.Directory]::CreateDirectory((Split-Path -Parent $dst))
    Copy-Item -LiteralPath (Join-Path $m.symbols_source $a.relative_path) -Destination $dst
    Assert-Hash $dst $a.sha256
}
Copy-Item -LiteralPath $m.pdb.path -Destination (Join-Path $m.symbols_destination 'spt.pdb')
Assert-Hash (Join-Path $m.symbols_destination 'spt.pdb') $m.pdb.sha256
Assert-Absent
Copy-Item -LiteralPath $m.pdb.path -Destination $Pdb -Force
Copy-Item -LiteralPath $m.exe.path -Destination $Binary -Force
Assert-Hash $Binary $m.exe.sha256
Assert-Hash $Pdb $m.pdb.sha256
[Environment]::SetEnvironmentVariable('SPT_RC_HITCH_DIAG','1','User')
[Environment]::SetEnvironmentVariable('SPT_NET_DIAG_SYMBOL_DIR',[string]$m.symbols_destination,'User')
if ([Environment]::GetEnvironmentVariable('SPT_RC_HITCH_DIAG','User') -ne '1' -or [Environment]::GetEnvironmentVariable('SPT_NET_DIAG_SYMBOL_DIR','User') -cne $m.symbols_destination) { throw 'USER environment write failed; rollback' }
if ((Invoke-Bounded $Binary @('--version') 10).Trim() -cne $m.cli_version) { throw 'Installed CLI version mismatch; rollback' }
```

If copy/hash/environment fails, do not start a partly installed image. Run rollback below. An unexpected resident appearing between gates is a stop condition, not authorization to overwrite its running image or kill a new unverified process.

## 5. Run exact task and verify a new daemon epoch

```powershell
Assert-Absent
Assert-Task
if (Test-Path -LiteralPath $Log) { throw 'Log unexpectedly reappeared before restart; investigate writer' }
$StartedUtc = [DateTime]::UtcNow
$LastTaskLaunchUtc = $StartedUtc
$StartedMs = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
Invoke-Bounded $SchTasks @('/Run','/TN',$TaskName) 15 | Out-Host
# At most six census attempts, each itself <=15 seconds, plus five one-second sleeps.
$NewRows = @()
for ($i=0; $i -lt 6; $i++) {
    $NewRows = @(Resident-Rows)
    if ($NewRows.Count -eq 1) { break }
    if ($NewRows.Count -gt 1) { throw 'Multiple residents; no broad kill allowed' }
    if ($i -lt 5) { Start-Sleep -Seconds 1 }
}
if ($NewRows.Count -ne 1 -or [DateTime]::Parse($NewRows[0].CreatedUtc).ToUniversalTime() -lt $StartedUtc) { throw 'No provably fresh daemon; rollback' }
$Held = Hold-Resident -1 $StartedUtc
$After = Read-Status
if ($After.broker_image -cne $m.broker_version -or [string]$After.pid -ne [string]$Held.Id) { throw 'Live broker version/identity mismatch; rollback' }
$After | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $Run 'after.json') -Encoding UTF8
$AfterCensus = Read-Census
$AfterCensus | ConvertTo-Json -Depth 30 | Set-Content -LiteralPath (Join-Path $Run 'after-endpoints.json') -Encoding UTF8
Assert-Preserved $After $AfterCensus
$StartRecord = $null
for ($i=0; $i -lt 10; $i++) {
    if (Test-Path -LiteralPath $Log) {
        if ((Get-Item -LiteralPath $Log).Length -gt 16MB) { throw 'New log >16 MiB; preserve separately, do not silently truncate' }
        $lines = (Read-SharedLog) -split "`r?`n"
        foreach ($line in $lines) {
            $at = $line.IndexOf('NET_DIAG_V1:')
            if ($at -lt 0) { continue }
            try { $record = $line.Substring($at + 12).Trim() | ConvertFrom-Json } catch { continue } # final partial line may still be writing
            if ($record.event -eq 'NET_DIAG_START' -and $record.v -eq 1 -and $record.seq -eq 0 -and [string]$record.broker_pid -eq [string]$Held.Id -and [long]$record.wall_ms -ge $StartedMs -and -not [string]::IsNullOrWhiteSpace($record.run_id)) { $StartRecord = $record; break }
        }
    }
    if ($null -ne $StartRecord) { break }
    Start-Sleep -Seconds 1
}
if ($null -eq $StartRecord) { throw 'No fresh PID/time-matched NET_DIAG_V1 START; rollback' }
if (@($StartRecord.worker_tids).Count -ne 2 -or @($StartRecord.worker_tids | Select-Object -Unique).Count -ne 2 -or $StartRecord.canary_period_ms -ne 25 -or $StartRecord.dropped_records -ne 0) { throw 'Incomplete START contract; rollback' }
foreach ($cap in 'NET_IF_ENUM','NET_GATEWAY_LOOKUP','NET_CANCEL_MIB','NET_MEET_BIND','NET_MEET_RETIRE','NET_NTP_REFRESH') {
    if ($cap -notin $StartRecord.capabilities) { throw "Missing diagnostic capability: $cap" }
}
$StartRecord | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $Run 'fresh-start.json') -Encoding UTF8
```

Before collecting, inspect Process Explorer's Environment page for **`$Held.Id`, executable `$Binary`, creation time from `daemon-<PID>.json`**. Record only `SPT_RC_HITCH_DIAG=1` and `SPT_NET_DIAG_SYMBOL_DIR=<exact manifest destination>`, with PID/creation time and observation UTC in the operator receipt; do not dump unrelated secrets. Both must be actual daemon-process values, not USER registry values or caller-shell values. Missing access/proof is a failed gate. The paired postflight snapshots and `Assert-Preserved` protect non-target local rows and remote suspended node+ID/status rows; none are woken here.

Fresh START is startup evidence, not proof of successful OS frame symbolization or a mechanism verdict. The frozen analyzer evaluates actual stack records; unresolved OS frames remain insufficient evidence. No debugger attach or induced network fault is part of this field staging kit.

## 6. Bounded rollback (operator chooses this on any post-stop failure)

Keep endpoints quiescent under their extensions. Never automatically resume them. Rollback restores the original binary/PDB and both prior USER values, deleting newly introduced USER values by passing `$null`. It restarts the SAME reviewed task. It preserves diagnostic logs and the staged symbol directory as evidence; it does not delete unrelated data or change task configuration.

Use the same human shell/helpers/manifest/run directory. If the shell was lost, first recover **that exact** `$Run`, `original.json` and manifest pin, reload the helpers and original process environment values; do not select a backup by wildcard or create a new baseline. If task XML/identity is now different, absence is unprovable, permissions fail or a live endpoint returned, STOP and hand the recorded evidence to the operator—no wider kill target.

```powershell
$Original = Get-Content -LiteralPath (Join-Path $Run 'original.json') -Raw | ConvertFrom-Json
Assert-Task
$RollbackRows = @(Resident-Rows)
if ($RollbackRows.Count -gt 1) { throw 'Ambiguous residents: rollback cannot safely stop them' }
if ($RollbackRows.Count -eq 1) {
    $RollbackStatus = Read-Status
    if (@($RollbackStatus.local_endpoints | Where-Object { $_.alive }).Count) { throw 'Endpoint returned; extension must quiesce before rollback' }
    if ($null -ne $Held) { $Held.Dispose(); $Held = $null }
    if ($null -ne $LastTaskLaunchUtc) {
        $Held = Hold-Resident -1 $LastTaskLaunchUtc
    } else {
        $Held = Hold-Resident $m.task_session_id
    }
    Stop-ExactResident $Held
    $Held.Dispose(); $Held = $null
} else {
    # A failed task launch may still own a task instance; end ONLY this exact task.
    End-RunningTask
}
Assert-Absent
Assert-Hash (Join-Path $Run 'spt.exe.original') $Original.exe_sha256
if ($Original.pdb_existed) { Assert-Hash (Join-Path $Run 'spt.pdb.original') $Original.pdb_sha256 }
if (Test-Path -LiteralPath $Log) {
    $savedLog = Join-Path $Run ('daemon.stderr.rollback-' + [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssfffZ') + '.log')
    Move-Item -LiteralPath $Log -Destination $savedLog
}
Copy-Item -LiteralPath (Join-Path $Run 'spt.exe.original') -Destination $Binary -Force
if ($Original.pdb_existed) {
    Copy-Item -LiteralPath (Join-Path $Run 'spt.pdb.original') -Destination $Pdb -Force
    Assert-Hash $Pdb $Original.pdb_sha256
} elseif (Test-Path -LiteralPath $Pdb) {
    # Delete only the PDB installed by this kit, never an unrecognized replacement.
    Assert-Hash $Pdb $m.pdb.sha256
    Remove-Item -LiteralPath $Pdb
}
Assert-Hash $Binary $Original.exe_sha256
[Environment]::SetEnvironmentVariable('SPT_RC_HITCH_DIAG',$Original.user_diag,'User')
[Environment]::SetEnvironmentVariable('SPT_NET_DIAG_SYMBOL_DIR',$Original.user_symbols,'User')
foreach ($pair in @(@('SPT_RC_HITCH_DIAG',$Original.user_diag), @('SPT_NET_DIAG_SYMBOL_DIR',$Original.user_symbols))) {
    if ([Environment]::GetEnvironmentVariable($pair[0],'User') -cne $pair[1]) { throw 'Prior USER environment restore failed; do not restart' }
}
if ((Invoke-Bounded $Binary @('--version') 10).Trim() -cne $Original.version) { throw 'Rollback version mismatch; do not restart' }
Assert-Absent
Assert-Task
$RollbackStartedUtc = [DateTime]::UtcNow
$LastTaskLaunchUtc = $RollbackStartedUtc
Invoke-Bounded $SchTasks @('/Run','/TN',$TaskName) 15 | Out-Host
$RollbackRows = @()
for ($i=0; $i -lt 6; $i++) {
    $RollbackRows = @(Resident-Rows)
    if ($RollbackRows.Count -eq 1) { break }
    if ($RollbackRows.Count -gt 1) { throw 'Ambiguous residents after rollback start' }
    if ($i -lt 5) { Start-Sleep -Seconds 1 }
}
if ($RollbackRows.Count -ne 1 -or [DateTime]::Parse($RollbackRows[0].CreatedUtc).ToUniversalTime() -lt $RollbackStartedUtc) { throw 'Rollback task restart unconfirmed; leave endpoints quiescent' }
$Held = Hold-Resident -1 $RollbackStartedUtc
$Restored = Read-Status
$Restored | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $Run 'rollback-status.json') -Encoding UTF8
$RestoredCensus = Read-Census
$RestoredCensus | ConvertTo-Json -Depth 30 | Set-Content -LiteralPath (Join-Path $Run 'rollback-endpoints.json') -Encoding UTF8
Assert-Preserved $Restored $RestoredCensus
Assert-Hash $Binary $Original.exe_sha256
$env:SPT_RC_HITCH_DIAG = $Original.process_diag
$env:SPT_HOME = $Original.process_home
```

Now verify the new rollback daemon's Environment page contains the original two values, or that each originally absent variable is absent. USER restoration alone is not proof that cached task-engine environment was refreshed. If stale, rollback is **not fully verified**: stop collection and hand off; no Scheduler restart, task rewrite, sign-out, reboot or machine-environment workaround is included in this authority. Confirm restored live broker version against the original `before.json` `broker_image`, and verify the five protected suspended endpoint states again. If any verification fails, keep endpoints quiescent and report exact failure; do not claim rollback complete.

Failure before section 4: installed binaries/task are unchanged; retain evidence, dispose the held handle, restore this human shell's two process values from `original.json`, and let extension owners decide whether/when to resume only the two endpoints. Failure during a command timeout: helper terminates only its owned client; inspect resident/task/process evidence before deciding whether rollback is safe. A failed `/End`, unconfirmed death or inaccessible identity is an operator escalation, not permission to bypass a refusal. A failed restore leaves the task stopped; do not start an unverified partial restore.

## 7. Exact bounded collector after successful staging gates only

Do not run this after rollback or without both process-environment observations. The collector is read-only: no endpoint lifecycle, no configuration writes, no external debugger. It has a 180-second schedule, 5-second per-query deadline, 2-second query termination confirmation and bounded drain. Outer cap 200 seconds. On an outer timeout its child query may outlive the collector briefly, but is read-only and internally bounded; never widen the kill target.

```powershell
Assert-Hash $m.collector_path $m.collector_sha256
$Samples = Join-Path $Run 'samples.jsonl'
if (Test-Path -LiteralPath $Samples) { throw 'Refuse to overwrite capture' }
$sampleText = Invoke-Bounded $PowerShell @('-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File',$m.collector_path,'-Seconds','180','-HomePath',$HomePath,'-Binary',$Binary) 200
[IO.File]::WriteAllText($Samples, $sampleText, [Text.UTF8Encoding]::new($false))
```

A nonzero collector exit intentionally stops this block; partial JSONL remains in its numbered `call-*.stdout.txt`, with stderr adjacent. Preserve it as failed capture, never relabel it a pass. The before/after log snapshots and frozen analyzer are integrated by Main's existing field driver; this kit does not edit that analyzer. Known analyzer SHA256: `6f36563c6882aed3d4d14f5b276531911d844f6c629b41963378f883a4e9ab24`; known DELIVERY SHA256: `bd7f8f3e16e63b6f8c5a1bd7fa12daa89a29544d2b5b58c6b6bbcfe2aa2c0538`. Keep adjacent frozen rounds module with the analyzer.

Alternatively, after the same staging gates, the existing HFENDULEAM field wrapper captures bounded before/after log snapshots and runs the frozen analyzer in one command. Run **one** collector, not both:

```powershell
python .spt/preserved/302/step1-field.py --host decid@enlyzeam --home 'C:\Users\decid\AppData\Local\spt-core' --binary 'C:\Users\decid\AppData\Local\spt-core\bin\spt.exe' --seconds 180 --out .spt/preserved/302/field-release-3142753a-operator
```

The output directory must be new. Preserve any nonzero/insufficient result; neither command changes analyzer thresholds or restarts the node.

## Completed HFENDULEAM actual-daemon rehearsal

Four fresh private `SPT_HOME` trees, actual `spt.exe daemon run --detached`, existing collector configured for 60 seconds each. The daemon's real environment was read back, not inferred from the query client. Both broker and brain were proven members of the owned native Job; all four ended `COMPLETE / CONFIRMED_GONE / active_processes=0`. No resident was used.

| Build / knob | Collected rows | Analyzer-usable rows | Activation observation |
|---|---:|---:|---|
| Debug / unset | 56 | 24 | Zero `NET_DIAG` records; networking enabled |
| Debug / set | 56 | 29 | Fresh START; interface-enumeration, gateway and NTP boundaries |
| Release / unset | 56 | 30 | Zero `NET_DIAG` records; networking enabled |
| Release / set | 55 | 29 | Fresh START; interface-enumeration, gateway and NTP boundaries |

Both positive epochs parsed with frozen analyzer `6f36563c…`; all collectors exited 0. **All four analyzer exits were 2 / INSUFFICIENT_EVIDENCE**: less than 95% usable samples, cadence gaps, the intentional window below 170 seconds, and no observed stale episode. This proves the activation/log/parser plumbing, not field sampling sufficiency, starvation causality, or successful in-stall symbolization. No fixture was substituted: forced stale is not exposed by the actual CLI daemon. Private empty-subnet homes did not exercise meet-bind/retire or cancellation boundaries. Exact samples, raw logs, environment readbacks, membership, summaries and teardown receipts are under `.spt/preserved/302/daemon-rehearsal/{debug-unset,debug-set,release-unset,release-set}`. No thresholds were relaxed and no failed arm was rewritten.

### Kit verification

`daemon-rehearsal/CONTROLS.json`, SHA256 `3f057e315b370a306adfe6d92d8319dcda7ff8b36cf0efa8ea241a5e13bdc07e`, pins all four control receipts and raw inputs. The additional census-helper proof used another private actual release daemon: real `sessions=[]` and `bringing_up=[]`, wrong server PID refused, and a shared live-log START read succeeded. Its broker and brain were both native-Job members; teardown again ended `COMPLETE / CONFIRMED_GONE / active_processes=0`. Receipt: `daemon-rehearsal/census-helper-proof/receipt.json`.

Final PowerShell 5.1 parsing found zero errors in all 11 code blocks. Actual `Expand-Archive` succeeded into a private proof directory, all 21 extracted file hashes matched, and extracted release `spt.exe --version` returned `spt 0.70.0`. The extraction scratch was removed after verification; the pinned ZIP and readback receipts remain. `traceable-reqs check`: 924 complete, zero incomplete/findings. **Not executed:** ENLYZEAM transfer/extraction, endpoint shutdown, task End/Run, binary replacement, USER environment mutation or rollback. Those remain human-only commands; inheritance and quiescence must still be proved at execution time.

## Source grounding

- `.spt/preserved/302/daemon-rehearsal/enlyzeam-task-readonly.json`: Main's retained read-only task XML, current process identity and LocalAppData receipt; no task mutation performed.
- `.spt/preserved/302/enlyzeam-symbols/manifest.json`: available flat OS-symbol entries and producer-local search-path warning; transfer only flat PDBs and this manifest.

- `docs/DEBUG-ROLLOUT.md:15-27,43-51,85-91`: signed channel pin/apply, live broker-resource refusal; this manual exception does not pretend to use it.
- `crates/spt/src/cli.rs:652-680,7876-7941`; `crates/spt/src/teardown.rs:38-51,184-203`: suspend versus shutdown, topology-aware confirmed reap and internal 15-second bound.
- `crates/spt/src/cli.rs:8140-8186`: endpoint caller refusal, live-session guard and durable stop-inhibit. `crates/spt-daemon/src/service.rs:209-218,235-296`: exact task name, boot-only Windows task, no service routing for daemon start/stop.
- `crates/spt/src/cli.rs:6371-6377,8270-8311,8714-8774,8843-8851`: local endpoint status fields, live broker PID/image, diagnostic environment changes status JSON shape. No PID-file liveness authority.
- `.spt/preserved/302/STEP1-DRIVER.md:13-18,53-67`: canonical daemon stderr log, fresh START/run_id/seq/worker/capability contract. START symbol-directory reporting is **not established**; actual-process inspection is required.
- `.spt/preserved/302/step1-collect.ps1:3-9,80-91,106-129,148-167,194-197,228-274`: exact `-Seconds`, `-HomePath`, `-Binary` parameters, read-only status invocation, bounds and failure semantics.
- `crates/spt-daemon/src/{frame,codec,transport,msg,brain}.rs`: existing protocol-v1 brain hello and length-prefixed `sessions` / `sessions-reply` census. The bounded child reads only this census, validates the named-pipe server PID, and refuses malformed/unavailable results. Empty normal status is never substituted for an empty broker census.
- `daemon-rehearsal/enlyzeam-broker-pipe-shared-readonly.json`: observed canonical pipe `spt-daemon-broker-78bf18a9fd96fdc6.sock` for the reviewed ENLYZEAM home. This candidate is always checked against the retained live broker PID before the census. The same read exposed Windows log sharing requirements; `Read-SharedLog` uses `ReadWrite|Delete` sharing and a fixed-length, 16-MiB-capped snapshot.
