Commit Graph

15 Commits

Author SHA1 Message Date
Oliver Kieselbach d16b60f14c feat(v2): WG Part-2 architectural cleanup + install-mode handoff fix
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>
2026-05-04 18:36:37 +02:00
Oliver Kieselbach 579291272e fix(v2): split EnrollmentFactsObserved from SessionStarted to fix JoinMode race
V2 tracker / Backend-register-session race could land SessionStarted after
ESP/Tracker signals had already advanced Stage past SessionStarted. The
HandleSessionStartedV1 dead-end-Wache then swallowed the registry-derived
profile facts (enrollmentType + isHybridJoin), leaving JoinMode=Unknown on
every affected enrollment_complete payload (Session 10c8e0bf).

Root cause was a concept mix in SessionStarted: a lifecycle anchor doing
double duty as a static-facts seed. Split into two signals:

 - SessionStarted: pure lifecycle anchor (Stage / ClassifierTick / step).
 - EnrollmentFactsObserved: stage-agnostic, idempotent, monotonic facts
   carrier. New reducer handler delegates to ApplyEnrollmentFactsObserved
   (renamed from ApplySessionStarted, with set-once reason for v1 to
   honour the documented idempotency contract).

AwaitEnrollment is transparently covered: AgentBootstrap blocks until the
MDM cert exists, so the registry is guaranteed populated by the time
PostEnrollmentFactsObserved runs; a single post per agent run suffices.

Tests:
 - DecisionCore: 234/234 (4 new fact-handler tests + idempotency + monotonicity)
 - Agent.V2.Core: 771/771 (3 new producer tests, 1 reshaped)
 - Agent.Core (V1): 507/507 sanity-check unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:19:34 +02:00
Oliver Kieselbach 0305c0d635 test(v2): add UserDriven Hello-skipped fixture (anon df7abbc2)
Real-world V2 enrollment-session replay covering a UserDriven-v1 happy
variant where Hello-for-Business was enabled but per-user enrollment
resolved as "skipped" rather than "Success". Exercises the Hello+Desktop
AND-gate's tolerance for any terminal Hello outcome.

Asserts Stage=Completed, Outcome=EnrollmentComplete, HelloOutcome=skipped,
ScenarioProfile.Mode=Classic (set by ImeUserSessionCompleted).

README replaced with pointer to ScenarioTests.cs as source of truth —
old "(empty)" placeholder dropped, only the new entry listed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:59:21 +02:00
Oliver Kieselbach 3bfe7d7fdb v2 codex-04: wire AppInstall signals as observation-level facts
Closes Codex review follow-up #4. Before this commit the reducer had
no handler for DecisionSignalKind.AppInstallCompleted /
AppInstallFailed — the signals were real (ImeLogTrackerAdapter posts
one terminal per app) but fell through to HandleUnhandledSignal, so
the journal accumulated 'unhandled_signal_kind:AppInstallCompleted:v1'
dead-ends and the per-app outcomes disappeared. Subsequent ESP / IME
classifiers had no structured evidence of which apps blocked.

Scope: observation-only, as the Master-Tracker recommended. Neither
handler changes Stage or Outcome — they roll the terminal outcome into
a new DecisionState.AppInstallFacts aggregate that:

  - increments CompletedCount (+ Installed / Skipped / Postponed
    breakdown from the adapter's newState payload)
  - increments FailedCount and appends the appId to FailedAppIds,
    deduplicated and capped at MaxFailedAppIds (50) so pathological
    sessions cannot bloat DecisionState.

Every DecisionSignalKind now has at least one handler; the dead-end
path is reachable only through an unknown (kind, schemaVersion) pair
— the genuine forward-compat failure mode per plan §2.5. The
'Still unwired' dispatch comment is replaced with a 'Fall-through:
unknown (kind, schemaVersion)' label.

Tests:
  - new AppInstallSignalHandlerTests (+10): Installed/Skipped/Postponed
    breakdowns, unknown newState forward-compat, no stage/outcome
    change, failed count + appId recording, dedup on repeated appId,
    MaxFailedAppIds cap keeps first-observed entries, mixed-stream
    integration covering all buckets in one fold.
  - HarnessTests.Replay_unknownSignalKind_recordsDeadEnd renamed to
    Replay_unknownSchemaVersion_recordsDeadEnd, switched from the
    now-wired AppInstallCompleted to a v99 SessionStarted so the
    dead-end path stays covered through its real-world trigger.
  - ReducerFailSafeTests.Reduce_unhandledKind same retarget.

Suites green: DecisionCore 161/161 (+10), Agent.V2.Core 643/643,
Functions 734/734. Build 0 warnings.

Fixtures:
  - tests/fixtures/signal-kinds/app-install-completed-v1.json
  - tests/fixtures/signal-kinds/app-install-failed-v1.json

Plan: .claude/plans/codex-04-appinstall-signals-wiring.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:45:52 +02:00
Oliver Kieselbach 386f3e909f v2 codex-02: EffectRunner posts EffectInfrastructureFailure on critical fail
Closes Codex review follow-up #2. Before this commit, a
DecisionEffectKind.ScheduleDeadline / CancelDeadline failure raised
EffectRunResult.SessionMustAbort but the decision world never heard
about it — DecisionStepProcessor only logged and continued. A safety
deadline could live in DecisionState without ever being armed on the
scheduler, leaving the session to hang until the max-lifetime
watchdog tripped hours later.

Flow now:

  1. EffectRunner.TryRunCriticalDeadlineEffect fails.
  2. EffectRunner posts a synthetic
     DecisionSignalKind.EffectInfrastructureFailure signal onto the
     ingress before returning. Payload: reason + failingEffect.
  3. Ingress worker picks it up next iteration.
  4. HandleEffectInfrastructureFailureV1 (new, in DecisionEngine.Shared.cs)
     transitions Stage=Failed, Outcome=EnrollmentFailed, ClearDeadlines,
     and emits one EmitEventTimelineEntry effect for enrollment_failed
     with the reason from the payload.
  5. DecisionStepProcessor observes the terminal stage and fires the
     Terminated event; EnrollmentOrchestrator tears down normally.

EnrollmentFailed (not Aborted) distinguishes infrastructure-induced
failures from user/operator-initiated aborts in forensics. The ingress
post is wrapped in a try/catch so a full/closed channel cannot crash
the worker — the max-lifetime watchdog remains the last-resort exit.

Changes:
  - DecisionSignalKind: new EffectInfrastructureFailure enum value.
  - DecisionEngine: dispatch entry (v1).
  - DecisionEngine.Shared: HandleEffectInfrastructureFailureV1 handler,
    mirrors HandleSessionAbortedV1 shape but with EnrollmentFailed
    outcome and a failure timeline effect.
  - EffectRunner: PostEffectInfrastructureFailureSignal helper, called
    from the critical-effect path before returning SessionMustAbort.
  - EffectRunResult: XML-doc clarifies that SessionMustAbort is now
    observability-only (reducer terminates on its own).
  - DecisionStepProcessor: comment update — no code change needed, the
    synthetic signal reaches state naturally.
  - tests/fixtures/signal-kinds/effect-infrastructure-failure-v1.json:
    replay fixture per the enum's self-documented contract.

Tests (+8):
  - DecisionCore EffectInfrastructureFailureHandlerTests: transition
    shape, deadline clearing, enrollment_failed emission with reason,
    missing-reason fallback, double-post idempotency.
  - EffectRunnerTests: schedule-fail posts signal with correct payload,
    cancel-fail same, ingress-post exception tolerated.
  - FakeSignalIngressSink: ThrowOnPost toggle for the tolerance test.

All suites green: DecisionCore 144/144 (+5), Agent.V2.Core 633/633
(+3), Functions 727/727. Build 0 warnings.

Plan: .claude/plans/codex-02-effect-failure-to-decision.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:16:32 +02:00
Oliver Kieselbach 96dc1482c7 chore: drop local-only tooling + fixtures from repo
- tests/fixtures/enrollment-sessions-local/ is raw/pre-anonymization session
  fixture staging — keep untracked; remove .gitkeep + README.md from the index
  and simplify the gitignore to ignore the whole folder.
- tools/export-session-fixture.mjs is a local conversion helper; add to
  gitignore next to the existing tools/orphan-analysis.py entry.

Files remain on disk locally; only the Git index is pruned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:22:01 +02:00
Oliver Kieselbach 7c29b607dc v2 single-rail 6.1: guard DeviceSetupProvisioningComplete terminal on Classic path
HandleDeviceSetupProvisioningCompleteV1 completed unconditionally — in session
e259c121-dc13-46d6-8e96-118f1da9845e (2026-04-22, Classic UserDriven-v1) a late
DeviceSetupProvisioningComplete with payload deviceSetupResolved="unknown" fired
AFTER AccountSetup had started and DesktopArrived had been observed, but BEFORE
HelloResolved — terminating the session at Stage=Completed before Hello even ran.

Guard: when state.AccountSetupEnteredUtc != null, treat the signal as informational
only (step index + ordinal advance, stage unchanged, no completion effect). On the
Classic path, terminal completion still requires the Hello+Desktop AND-gate
(HandleHelloResolvedV1 / HandleDesktopArrivedV1). The original SelfDeploying /
Device-Only terminal semantics are preserved for AccountSetupEnteredUtc == null.

The redundant AccountSetupEnteredUtc == null check around the EnrollmentType
upgrade is now implicit and removed.

Regression coverage:
- New fixture tests/fixtures/enrollment-sessions/userdriven-late-provisioning-complete-v1.jsonl
  reproduces the 6-signal sequence from the reference session.
- New test ClassicScenarioTests.LateProvisioningComplete_afterAccountSetup_doesNotCompletePrematurely
  asserts terminal trigger == HelloResolved, DeviceSetupProvisioningComplete transition
  is taken but stage-unchanged, and DeviceOnlyDeployment hypothesis stays Unknown.

Tests: 109/109 DecisionCore + 559/559 V2.Core green; solution builds clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:09:14 +02:00
Oliver Kieselbach 489f124c6d v2 single-rail 4.1a: add InformationalEvent signal kind + payload keys
Introduce the generic pass-through signal that carries an EnrollmentEvent
shape through the decision engine. Any peripheral collector / lifecycle
source needing the Events timeline will post this instead of calling
TelemetryEventEmitter directly, so the engine is the single ordering
source for the wire. Promotion to a decision-specific kind is a later,
localized change.

- DecisionSignalKind.InformationalEvent (new).
- SignalPayloadKeys: EventType + Source (mandatory), Message, Severity,
  ImmediateUpload, DataJson (optional).
- tests/fixtures/signal-kinds/informational-event-v1.json documents the
  wire shape with a realistic NTP-probe payload.

No reducer handler yet — that is the next subtask (4.1b); this commit is
purely the contract extension.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:47:02 +02:00
Oliver Kieselbach 5b7f424d19 refactor(agent-v2): route whiteglove_resumed / system_reboot_detected / admin preemption through reducer effects
V2 parity cleanup - the three lifecycle events were emitted from Program.cs
directly while the corresponding reducer handlers (HandleWhiteGlovePart1To2Bridge,
HandleSystemRebootObservedV1) returned Array.Empty<DecisionEffect>(). Admin
preemption synthesised an EnrollmentTerminatedEventArgs and invoked
terminationHandler.Handle directly, bypassing the reducer entirely.

This commit unifies all three flows: the reducer is the single state owner, the
telemetry event appears as an EmitEventTimelineEntry effect of the state
transition, and the orchestrator's Terminated event fires automatically when
stage goes terminal - no more synthetic bypass.

- B1: HandleWhiteGlovePart1To2Bridge emits whiteglove_resumed timeline entry.
- B2: HandleSystemRebootObservedV1 emits system_reboot_detected with payload
  keys previousExitType/lastBootUtc.
- B3: new DecisionSignalKind.AdminPreemptionDetected + new SessionOutcome.AdminPreempted.
  Reducer HandleAdminPreemptionDetectedV1 transitions to Completed/Failed based on
  adminOutcome payload, emits enrollment_complete/_failed timeline entry with
  adminAction+source data keys. Program.cs posts the signal; terminationHandler
  still subscribes to orchestrator.Terminated (no direct Handle calls on the
  startup-preemption path).

Fixtures: tests/fixtures/signal-kinds/{system-reboot-observed,admin-preemption-detected}-v1.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:00:25 +02:00
Oliver Kieselbach 75c98d131f refactor(decisioncore): edge scenarios — hybrid reboot, ESP failure, recovery (M3.5)
Final M3 sub-milestone. Closes the plan's §4 M3 scenario matrix:

Handlers (DecisionEngine.Edge.cs):
 - EspResumed              mid-ESP reboot. Stage preserved; records
                           SystemRebootUtc on first observation (hash
                           stable on repeats).
 - EspTerminalFailure      Stage=Failed + Outcome=EnrollmentFailed,
                           deadlines cleared, enrollment_failed event
                           emitted with reason carried from payload.
                           Definitive — no classifier re-run.
 - SystemRebootObserved    records SystemRebootUtc so the WhiteGlove
                           sealing classifier credits the +15 reboot
                           weight on the next RunClassifier effect.

Scenario fixtures (committed-anonymized):
 - hybrid-reboot-v1.jsonl        ESP -> mid-phase reboot -> EspResumed ->
                                 AccountSetup -> Hello -> Desktop ->
                                 Completed. No WhiteGloveSealed path.
 - esp-terminal-failure-v1.jsonl ESP -> AccountSetup -> terminal failure
                                 -> Failed / reason=
                                 esp_terminal_failure_account_setup.

Tests (EdgeScenarioTests, 5 new):
 - Hybrid reboot preserves stage, records reboot fact, completes
   normally without triggering WhiteGlove.
 - ESP terminal failure transitions straight to Failed.
 - Restart-recovery determinism: replaying the same JSONL twice yields
   identical FinalStepHash, Stage, Outcome and transition count.
 - Prefix-replay check: truncated log ends in the expected intermediate
   stage (simulates crash between signals).
 - Global sanity: every committed fixture (all 12) ends in a terminal
   stage — regression guard for any future reducer change.

Suite totals: 57 DecisionCore (was 52) / 507 Legacy / 557 Functions =
1121 tests green. All 14+ scenarios from plan §R6 Szenario-Matrix pass
through the reducer with the expected terminal outcome.

This closes M3 (DecisionEngine-Reducer + Classifier-Kernel). M4 wires the
SignalAdapter + EffectRunner + TelemetryTransport in the V2 agent runtime.

Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.5 — M3 complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:26:13 +02:00
Oliver Kieselbach 8e4faf4251 refactor(decisioncore): WhiteGlove Part 2 + Part2CompletionClassifier + 2 scenarios (M3.4)
Plan §4 M3.4 + plan §2.3 WhiteGlove Part-2 state machine: post-reboot user
sign-in is now a first-class path with a dedicated classifier, a 24h safety
deadline, and an explicit Sealed -> AwaitingUserSignIn -> CompletedPart2 /
Failed progression.

Classifier (WhiteGlovePart2CompletionClassifier : IClassifier):
 - Scoring rules NEW (no Legacy equivalent): each of the four Part-2 user
   signals (+25) — UserAadSignInComplete, HelloResolvedPart2,
   DesktopArrivedPart2, AccountSetupCompletedPart2. All four together hit
   the Confirmed threshold (100); missing one drops to Weak (75).
 - WhiteGlovePart2CompletionSnapshot implements the new IClassifierSnapshot
   interface; existing WhiteGloveSealingSnapshot was retrofit to the same
   contract so the harness anti-loop is uniform.

Handlers (DecisionEngine.WhiteGlovePart2.cs):
 - SessionRecovered (Shared.cs) now bridges a WhiteGloveSealed state into
   WhiteGloveAwaitingUserSignIn and arms the 24h WhiteGlovePart2Safety
   deadline on the recovered signal's occurredAt.
 - Four Part-2 signal handlers (UserAadSignInComplete, HelloResolvedPart2,
   DesktopArrivedPart2, AccountSetupCompletedPart2) record facts + emit a
   Part-2 RunClassifier effect; facts arriving outside the AwaitingUserSignIn
   stage are recorded but no classifier runs (prevents stray Part-2 verdicts
   on non-sealed sessions).
 - WhiteGlovePart2Safety deadline handler terminates with Failed /
   enrollment_failed reason=part2_user_absent.
 - ClassifierVerdictIssued router extended to dispatch on classifier id —
   Part-2 verdicts route to ApplyWhiteGlovePart2Verdict; Confirmed triggers
   Stage=WhiteGloveCompletedPart2 + Outcome=WhiteGlovePart2Complete +
   whiteglove_part2_complete event.

DecisionState: four new SignalFact<DateTime>? fields for the Part-2 signals
(full builder parity).

ClassifierAwareReplayHarness: anti-loop lookup now routes per classifier id
(WhiteGloveSealing <-> WhiteGloveSealing.LastClassifierVerdictId;
Part2Completion <-> WhiteGlovePart2Completion.LastClassifierVerdictId).
Snapshot hashing works through the IClassifierSnapshot interface instead
of a WhiteGloveSealingSnapshot type check.

Scenario fixtures (committed-anonymized):
 - whiteglove-part2-happy-v1.jsonl  seal Part 1, reboot, 4 Part-2 signals
                                    within minutes -> Confirmed + Part2Complete.
 - whiteglove-part2-stuck-v1.jsonl  seal Part 1, reboot, 24h passes, safety
                                    deadline fires -> Failed / part2_user_absent.

Tests (3 new in WhiteGlovePart2ScenarioTests):
 - Happy terminal state + hypotheses + all four facts + no residual deadlines
 - Stuck terminal state + Part-1 Confirmed remains, Part-2 Rejected with reason
 - Bridge check: after SessionRecovered(Sealed), Part-2 safety is active

Suite totals: 52 DecisionCore (was 49) / 507 Legacy / 557 Functions.

Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.4 + §2.3 Part-2 stages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:21:53 +02:00
Oliver Kieselbach f75a8d4375 refactor(decisioncore): WhiteGlove Part 1 handlers + sealing classifier + 4 scenarios (M3.3)
Plan §4 M3.3: the first kernel-service classifier goes live (plan §2.4 L.8),
the effect-based classifier-run contract is wired end-to-end, and the anti-loop
via InputHash is proven by a dedicated fixture.

Classifier (WhiteGloveSealingClassifier : IClassifier):
 - Scoring ported 1:1 from Legacy WhiteGloveClassifier (same weights, same
   thresholds, same asymmetric-conservative rule: only Confirmed drives the
   WhiteGloveSealed stage).
 - Positive weights: ShellCoreWhiteGloveSuccess=80, SealingPattern=40,
   DeviceOnlyDeployment=15, SystemReboot=15.
 - Hard excluders (-100 each): AadJoinedWithUser, DesktopArrived, HelloResolved.
   Soft exclude: AccountSetupActivity=-40.
 - WhiteGloveSealingSnapshot is the classifier input type and computes a
   deterministic SHA256-short InputHash over WG-relevant fields only — so
   the anti-loop triggers on meaningful fact changes, not on unrelated state.

Handlers (DecisionEngine.WhiteGlove.cs):
 - WhiteGloveShellCoreSuccess       records fact; emits RunClassifier effect;
                                    arms ClassifierTick if not already.
 - WhiteGloveSealingPatternDetected mirror handler for the IME-pattern path.
 - ClassifierVerdictIssued          applies the verdict to WhiteGloveSealing
                                    hypothesis; on Confirmed transitions to
                                    WhiteGloveSealed stage +
                                    Outcome=WhiteGlovePart1Sealed and emits
                                    the whiteglove_complete event.
 - HandleClassifierTickDeadlineFired (shared router)  re-emits RunClassifier
                                    and re-arms itself unless terminal.

ClassifierAwareReplayHarness (tests-only):
 - Loops per-step effects: any RunClassifier effect is resolved synchronously
   via the registered IClassifier; the verdict is wrapped in a
   ClassifierVerdictIssued signal and fed back into Reduce. Anti-loop skips
   the call when snapshot hash == state.WhiteGloveSealing.LastClassifierVerdictId.
 - Exposes ClassifierRunStats for regression tests.

DecisionState extension: two new SignalFact<bool> fields capture the WG
sightings (ShellCoreWhiteGloveSuccessSeen, WhiteGloveSealingPatternSeen). The
classifier reads them via WhiteGloveSealingSnapshot.

Scenario fixtures (4, committed-anonymized):
 - whiteglove-inline-v1.jsonl            Confirmed on ShellCore alone (80).
 - whiteglove-signal-correlated-v1.jsonl Weak after pattern, ClassifierTick
                                         anti-loop-skipped, Confirmed after
                                         ShellCore escalates the score.
 - whiteglove-false-positive-v1.jsonl    SealingPattern + late-AADJ hard-
                                         excludes -> Rejected; session still
                                         completes via Classic Hello+Desktop.
 - whiteglove-antiloop-v1.jsonl          Single pattern + two ticks; runs=1,
                                         skipped_by_antiloop=2.

Tests: 4 scenario tests in WhiteGlovePart1ScenarioTests, all passing.
DecisionCore.Tests: 49 (was 45). Legacy 507 + Functions 557 unchanged.

ReducerFailSafeTests + HarnessTests updated to use AppInstallCompleted as
the still-unhandled sentinel (WhiteGloveShellCoreSuccess is now wired).

Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.3 + §2.4 classifier kernel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:15:06 +02:00
Oliver Kieselbach 0ebfa2fc3c refactor(decisioncore): SelfDeploying + Device-Only handlers + 2 scenarios (M3.2)
SelfDeploying-v1 vertical slice: DeviceSetupProvisioningComplete terminal
handler, DeviceOnlyEspDetection deadline wiring, and the
DeviceOnlyDeployment hypothesis progression UserPresent / DeviceOnly /
Ambiguous expressed via Hypothesis.Reason (plan §2.3).

Handlers (DecisionEngine.SelfDeploying.cs):
 - DeviceSetupProvisioningComplete  terminal. Cancels all deadlines,
                                    finalizes DeviceOnlyDeployment based on
                                    user-presence facts (AadJoinedWithUser,
                                    HelloResolvedUtc, DesktopArrivedUtc),
                                    raises EnrollmentType to Strong when
                                    no AccountSetup was observed before.
 - DeviceOnlyEspDetectionFired      hypothesis-only update to
                                    DeviceOnlyDeployment=Strong(DeviceOnly).
                                    Stage unchanged; completion still
                                    requires explicit provisioning signal.

Classic.cs extended:
 - First EspPhaseChanged(DeviceSetup) arms the DeviceOnlyEspDetection
   deadline (5 min window, plan §2.7).
 - EspPhaseChanged(AccountSetup) cancels it — UserDriven-v1 path.

Shared DeadlineFired router routes the deadline to the SelfDeploying body.

Scenario fixtures (tests/fixtures/enrollment-sessions/):
 - selfdeploying-happy-v1.jsonl            — 3 signals, SkipUserStatusPage
                                             path. Terminal Completed with
                                             DeviceOnly hypothesis confirmed
                                             and no user-presence facts.
 - selfdeploying-esp-exit-unknown-v1.jsonl — 4 signals, deadline fires at
                                             T+5m and sets DeviceOnly before
                                             provisioning-complete arrives.

Tests (SelfDeployingScenarioTests, 4 new):
 - SelfDeployingHappy terminal state + hypotheses
 - SelfDeployingHappy deadline cleanup
 - Device-Only ESP-exit-unknown deadline -> provisioning flow
 - AccountSetup on the Classic path cancels DeviceOnlyEspDetection
   (regression guard — UserDriven should never classify as DeviceOnly)

InternalsVisibleTo added to DecisionCore for DecisionCore.Tests so the
internal DeviceOnlyReasons tokens stay hidden from consumers while tests
assert on them directly.

Suite totals: 45 DecisionCore (was 41) / 507 Legacy / 557 Functions.

Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:04:52 +02:00
Oliver Kieselbach 0210d42c7f refactor(decisioncore): Classic UserDriven-v1 handlers + 3 scenarios (M3.1)
First vertical slice of real reducer behavior: six handlers for the Classic
UserDriven-v1 enrollment flow plus the shared DeadlineFired router, with three
committed-anonymized scenario fixtures driving the proof.

Handlers (DecisionEngine.Classic.cs):
 - EspPhaseChanged       stage progression DeviceSetup -> AccountSetup;
                         populates CurrentEnrollmentPhase + DeviceSetupEnteredUtc /
                         AccountSetupEnteredUtc / FinalizingEnteredUtc facts;
                         raises EnrollmentType hypothesis to Weak on AccountSetup.
 - EspExiting            records EspFinalExitUtc; schedules HelloSafety (300 s);
                         transitions to AwaitingHello and emits ScheduleDeadline.
 - HelloResolved         records HelloResolvedUtc + HelloOutcome; cancels
                         HelloSafety; completes session if Desktop already arrived.
 - DesktopArrived        mirror of HelloResolved; completion when Hello already
                         resolved.
 - ImeUserSessionCompleted  raises EnrollmentType hypothesis to Strong; records
                            matched pattern id.
 - AadUserJoinedLate     HYPOTHESIS-ONLY (project memory
                         feedback_aad_joined_late_not_completion). Records
                         AadJoinedWithUser fact + annotates EnrollmentType reason.
                         Stage unchanged, no terminal event. Regression guard test
                         verifies From/To stage equality.

Shared router (DecisionEngine.Shared.cs):
 - DeadlineFired dispatches on Payload[deadline]. HelloSafety timeout synthesizes
   HelloOutcome=Timeout and drives the same completion path as HelloResolved.
   Unknown deadline names cancel the deadline and record a neutral transition —
   forward-compat with M3.3 (ClassifierTick) / M3.4 (WhiteGlovePart2Safety).

Constants:
 - DeadlineNames enumerates the 7 pflicht-deadlines from plan §2.6.
 - SignalPayloadKeys standardizes payload keys used by collectors and the
   deadline scheduler.

Scenario harness + fixtures (tests/fixtures/enrollment-sessions/):
 - ScenarioTestBase locates repo root and wraps FixtureLoader + ReplayHarness.
 - userdriven-happy-v1.jsonl            — 7 signals, terminal Completed /
                                          EnrollmentComplete / HelloOutcome=Success.
 - userdriven-hello-timeout-v1.jsonl    — Desktop arrives before Hello, deadline
                                          fires at T+5m -> HelloOutcome=Timeout,
                                          Completed.
 - late-aadj-v1.jsonl                   — Late-AADJ mid-flight does not complete
                                          prematurely; session still completes
                                          normally via Hello + Desktop.

Tests: 4 scenario tests in ClassicScenarioTests + determinism check.
Total suite: 41 DecisionCore.Tests (was 37 at M3.0) / 507 Legacy unchanged /
557 Functions unchanged.

Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:59:30 +02:00
Oliver Kieselbach 710504549b refactor(agent): V2-Agent-Split skeleton + infrastructure copy (M2)
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>
2026-04-20 13:31:30 +02:00