Replaces V1/V2-hardcoded build + bootstrap with a single parametric
pipeline that scales to V3+ without rewrites. Full architecture in
.claude/plans/v2-cutover.md.
Stable namespace (Bootstrap-side, never renamed): generic Bootstrap
script, agent owns its --install defaults so it works for any line.
Versioned namespace (per-line, SelfUpdater-side): isolated *-v{N}.*
blobs; SelfUpdater never crosses lines or touches stable.
Backend GetAgentConfig dispatches per X-Agent-Version-Major to
AdminConfiguration.GetAgentLine(int) -> LatestAgentV{N}* fields.
Future V3 = add field set + switch arm. TableConfigRepository.Map
falls back to legacy unsuffixed columns for transparent migration.
V1 SelfUpdater now reads version-v1.json (would have been clobbered
by the V2 cutover otherwise). V2 agent owns its own 600s TenantId-wait
default so the Bootstrap calls --install plain.
Tests: 3289 across 6 suites, all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundled change covering three orthogonal pieces of work that all landed in
the same working tree before the next PR-style commit could be split out.
## WG Part-2 architectural cleanup (PR-A through PR-E)
Plan: ~/.claude/plans/wg-part2-architectural-cleanup.md.
The V2 agent carried a full separate "Part-2 apparatus" (4 special signal
kinds, 2 special stages, a special outcome, a special classifier, a 24h
safety deadline, a dedicated reducer file, adapter `part2Mode` switches,
a SessionRecovered bridge). This did not match OS reality: post-reseal
Part 2 is just a normal user-driven OOBE running through the Classic
flow. The apparatus also carried a real risk (24h false-positive
enrollment_failed mid-success) and was not even active in practice
(observed in session bdb3cf9d-541b-4c44-aebe-5b9e72743b0a, 2026-05-04).
V2 is now V1-symmetric:
PR-A — Orchestrator Resume Mechanic. EnrollmentOrchestrator detects a
persisted Stage=WhiteGloveSealed snapshot at Start(), archives the
reducer-state segments to state/.part1-<utc-ts>/, runs as a fresh
first-boot through the normal recovery branches, and posts a single
whiteglove_resumed InformationalEvent after the onIngressReady hook.
New helper class StateArchiver.cs + StateArchiverTests +
EnrollmentOrchestratorPart2EmissionTests.
PR-B — DecisionCore Cleanup. Removed:
- DecisionSignalKind.UserAadSignInComplete / HelloResolvedPart2 /
DesktopArrivedPart2 / AccountSetupCompletedPart2 / SessionRecovered
- SessionStage.WhiteGloveAwaitingUserSignIn / WhiteGloveCompletedPart2
- SessionOutcome.WhiteGlovePart2Complete
- DeadlineNames.WhiteGlovePart2Safety
- DecisionState's 4 Part-2 SignalFacts (+ builder + snapshot slots)
- WhiteGlovePart2CompletionClassifier + WhiteGlovePart2CompletionSnapshot
- DecisionEngine.WhiteGlovePart2.cs (entire reducer file)
- HandleSessionRecoveredV1 dispatcher + handler
PR-C — Adapter Cleanup. Dropped the inert `part2Mode` ctor parameter
from AadJoinWatcherAdapter, DesktopArrivalDetectorAdapter,
HelloTrackerAdapter, EspAndHelloTrackerAdapter (and their tests).
AadJoinHost is the only production caller using a named-arg form,
unaffected.
PR-D — Backend + Web. Removed `WhiteGloveCompletedPart2` cases from
TelemetryPayloadParser.IsTerminalStage and DecisionGraphBuilder
(IsTerminalStage + DeriveTerminalOutcome) plus their tests. Dropped
the dead `UserAadSignInComplete` constant from Shared.Constants.
Extracted pure helper computeWhiteGloveSplitSequence(events) in
app/sessions/[sessionId]/utils/eventHelpers.ts; useSessionDerivedData +
utils/sessionExportUtils now share it. New 6-case vitest covering
splitter behaviour (no marker, primary trigger via whiteglove_resumed,
agent_started fallback, Pending-only variants, primary-vs-fallback
race). PhaseTimeline + eventTypes were already on the new model.
PR-E — Tests + Fixtures Final Cleanup. Deleted the two
whiteglove-part2-*.jsonl fixtures + EdgeScenarioTests references;
removed dead `whiteglove_part2` case + test from SummaryDialog
OutcomeMapper; trimmed PR-B history breadcrumbs that still embedded
removed symbol names so the final grep is clean. Updated MEMORY
entries (project_v2_completion_location, project_wg_part2_simple_model,
project_wg_signal_validation_pending, project_bdb3cf9d_wg_followups,
MEMORY.md index).
What stayed: SessionStage.WhiteGloveSealed (Part-1 marker), the
`_isWhiteGlovePart2` Hint-Bool (V1-symmetric, plan §11) wired only into
EnrollmentTerminationHandler / SoftwareInventoryAnalyzer for shutdown-
analyzer phase=2 tagging, the `whiteglove_part1_complete` event-type +
`whiteglove_part1` outcome-string, `PausedForPart2` terminal-outcome
label for the Inspector, the `whiteglove_resumed` event as UI-splitter
trigger.
Final acceptance: grep for the removed symbol set in src/ excluding V1
(Agent.Core/) is empty. V2.Core.Tests 991 / DecisionCore.Tests 282 /
Functions.Tests 862 / SummaryDialog.Tests 44 / Web vitest 227 — all
green. Solution build clean.
## Resume-path correctness fixes (Codex review of the cleanup)
Three findings on the new resume mechanic:
P1a — AgentRuntimeHost was still skipping LifecycleEmitters
PostEnrollmentFactsObserved + PostSessionStarted on isWhiteGloveResume.
After PR-A's archive-and-reset the decision state is empty, so the
Classic flow needs both anchors (otherwise ScenarioProfile stays at
default and the Hello+Desktop AND-gate misclassifies). Fixed: post
unconditionally for facts; SessionStarted is now only gated by an
AdminAction, not by the resume flag. The orchestrator's
whiteglove_resumed lands first (smaller sequence) so the Web splitter
(`splitSequence = resumed.sequence - 1`) still puts SessionStarted
into the Part-2 timeline block.
P1b — StateArchiver was archiving event-sequence.json along with the
reducer state. Part 2 reuses the same SessionId and the backend orders
events session-wide by (SessionId, Sequence); the Web split also uses
`splitSequence = resumed.sequence - 1`. Resetting the counter at the
resume boundary would collide Part-1 and Part-2 sequences and break
the splitter. event-sequence.json is now intentionally preserved; the
three reducer-state segments still archive. StateArchiverTests +
RecoveryPathTests asserting the new contract.
P2 — V1 reset the local session-age clock on Part-2 resume
(SessionPersistence.ResetSessionCreatedAt, MonitoringService.cs:530).
V2 was clearing only the WhiteGlove marker, so a crash/reboot during
Part-2 AccountSetup would let the next start run the emergency-break
watchdog against the original Part-1 timestamp and trip too early.
AgentRuntimeHost now also calls SessionIdPersistence.SaveSessionCreatedAt
(DateTime.UtcNow) right after ClearWhiteGloveComplete. New
ProgramGuardsTests test asserting the V1-symmetric resume contract.
## Install-mode handoff hardening
Plan reference (memory): project_v2_install_runtime_handoff.md.
Bootstrap script + Program.InstallMode + InstallModeTaskXmlTests.
PR1 — Replace `schtasks /Run` with WMI Win32_Process.Create for the
install-time runtime handoff. schtasks /Run reports SUCCESS as soon
as the run is queued; the same battery / AV / OOBE-defer paths that
motivate PR2 also queue a manual /Run (observed WinPE 2026-05-04,
task stuck in Queued state). WMI also gives the runtime process the
same isolation Task Scheduler uses, without the Job-Object inheritance
risk of a direct Process.Start.
PR2 — Register the BootTrigger Scheduled Task from a hardened XML
definition rather than `schtasks /Create`. The CLI form inherits XML
schema defaults DisallowStartIfOnBatteries=true and
StopIfGoingOnBatteries=true, which queue the BootTrigger run
indefinitely on a laptop that boots on battery in OOBE / WinPE. The
XML disables both, sets StartWhenAvailable, and pins
ExecutionTimeLimit=PT0S so the long-running agent is never killed at
the schtasks 72h CLI default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bridges the OOBE / hybrid-AAD-join race window where the agent fires
before AADTenantID / CloudDomainJoin\TenantInfo are written to the
registry. Field evidence (GardnerMedia, 2026-04-30): the agent fired
~5 minutes before the AAD device cert landed; an event-driven wait of
10 minutes recovers the run instead of bailing and waiting for next
reboot.
Mechanism (V2 only — V1 keeps the fast-fail path):
* New TenantIdAwaiter.WaitForTenantId(timeoutSec, logger, ct) sets up
a CompositeRegistryChangeSignal over HKLM\SOFTWARE\Microsoft\Enrollments
and HKLM\SYSTEM\...\CloudDomainJoin (Registry64 view, recursive,
Name+LastSet+ThreadAgnostic filter), re-probes silently on every
change, and returns the resolved TenantId or null on timeout.
* 30 s belt-and-suspenders periodic re-probe protects against a missed
RegNotifyChangeKeyValue notification.
* 100 ms debounce after each signal so multi-value enrollment
transactions land before the re-probe.
Configuration:
* Opt-in via --tenant-id-wait <sec>. Default 0 = no wait (legacy
fast-fail). The PS1 bootstrap script owns the production default;
the agent itself never assumes one.
* CLI value persisted to bootstrap-config.json by --install so the
Scheduled Task picks it up on each restart.
* Install-AutopilotMonitor-v2.ps1 sets $tenantIdWaitSec = 600. Admins
override per tenant by editing this single line and re-deploying.
Testability:
* IRegistryChangeSignal abstraction lets unit tests drive the loop
with a fake signal and scripted probe — 8 deterministic cases cover
timeout, signal hit, multi-signal, cancellation, periodic re-probe,
whitespace/empty/null handling, zero-timeout short-circuit, and
argument validation.
* RegistryWatcherIntegrationTests exercise the real Win32 plumbing
(RegNotifyChangeKeyValue → Changed event) against HKCU so no
elevation is required and the test runs in standard CI.
* Both classes opt out of xUnit parallelisation — the timing-sensitive
Thread.Sleep handshakes are unreliable on a busy parallel pool.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
schtasks reports SUCCESS as soon as the run is queued, not once the
process is actually alive. A silent block (AV/EDR, AppLocker/WDAC) or
Task Scheduler defer during OOBE would otherwise stay invisible until
the next reboot triggers the task via /SC ONSTART.
After --install, poll Get-Process for AutopilotMonitor.Agent.V2 every
500ms up to 10s. Log pid + StartTime on success, or a clear WARNING
that points operators at TaskScheduler/Operational + AV/EDR logs on
failure. Same change applied to the production and dev variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new projects as a clean-slate V2 fork of the Legacy Agent:
AutopilotMonitor.Agent.V2 — net48 exe, minimal Program.cs,
buildcounter 2.0.N, version-v2.json
AutopilotMonitor.Agent.V2.Core — net48 library, 94 files copied from
Agent.Core via bulk sed namespace
rewrite; same folder layout
AutopilotMonitor.DecisionCore.Tests — xUnit net8.0 with ReplayHarness
+ 13 smoke/harness tests
V2 isolation is build-time-enforced via V2Isolation.targets imported from
both V2 csproj files — any Legacy ProjectReference fails the build with a
clear error. Grep-verified at commit time: zero `using
AutopilotMonitor.Agent.Core` statements and zero Legacy ProjectReferences
in the V2 tree.
Per plan §2.11, kernel items (CompletionStateMachine, CompletionGuards,
EnrollmentTracker*, Flows/*, WhiteGloveClassifier, WhiteGloveSignals,
EnrollmentStatePersistence) are transitionally retained so the V2 tree
compiles; M4 removes them when SignalAdapters + V2Orchestrator take over.
Per user direction (feedback_v2_kernel_only_refactor): the V2 refactor
replaces only the decision kernel. All peripheral capabilities — gather
rules, analyzers, diagnostics upload, timezone, server-driven actions,
summary dialog launch, env-var resolution, version checks, registry
watchers, network/DO/performance telemetry, self-update, bootstrap→cert
handover, self-destruct — are preserved verbatim via the bulk copy.
Supporting M3 bring-up platform:
- ReplayHarness (empty-stream deterministic, SHA256 hash over terminal
state fields) in DecisionCore.Tests/Harness/
- Fixture scaffold under tests/fixtures/: committed anonymized directory
with commit-policy README, plus gitignored enrollment-sessions-local/
for raw exports
- tools/export-session-fixture.mjs with --help / --dry-run / --validate
modes; real MCP wiring marked TODO(v2-m3)
V2 bootstrap scripts scripts/Bootstrap/Install-AutopilotMonitor-v2(.|-dev).ps1
(committed, pure ASCII verified per feedback_ps1_ascii_only). V2 deployment
scripts under scripts/Deployment/V2/ are gitignored (analog to Legacy).
Test totals at M2 gate: 13 DecisionCore + 507 Agent.Core (unchanged) +
557 Functions (unchanged) = 1077 green.
Plan: plans/REFACTOR_AGENT_V2.md §4 M2 + §2.16 V2-Agent-Split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PowerShell 5.1 (used by IME) reads scripts without BOM as ANSI,
corrupting UTF-8 multi-byte chars (em-dashes, Unicode symbols) and
causing cascading parse errors that prevented agent installation.
Replaced all non-ASCII characters with ASCII equivalents in both
Install and Uninstall bootstrap scripts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix for analyze rule reboot loop
added completed timestamp to progress portal
longer os age default in bootstrapper
removed legacy filed name in events
- install only during enrollment
Agent
- added geolocation support
- HelloDetector
- Reboot on complete
- WMI Model parsing fix
...and general small additions here and there