mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
7fb78a7e296d358bf70ce0051429da0516126214
13 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8ad472d1bc |
fix(agent): single-instance guard counts a same-named process only in session 0
A process name is not a protected namespace: any user session can keep a binary named like the agent resident, which made every agent start during that session exit 1 before registration and let install mode report a runtime launch that never happened. The guard and install mode's launch verification now share one rule (FindSiblingAgentPid): a name match counts only in session 0 and never for the caller itself. Every runtime start lands there (Scheduled Task, WMI launch from the SYSTEM install chain, self-update restart), so behaviour for real agents is unchanged. The session id comes from the process snapshot, so no handle is opened; a process that cannot be read does not count, which fails open into two agents rather than none. Two interactive --console runs in the same user session no longer block each other; an interactive run next to the session-0 runtime still does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
f607e21c13 |
agent+rules: Hybrid Join user affinity from the IME user token; IME backlog eras anchored by the bootstrap record
hybrid_login_pending now measures the absence of a real-user desktop after a reboot (cancelled by the DesktopArrivalDetector or a real JoinInfo user), is never armed on WhiteGlove devices (resume marker, Part-2 flag, .part1- archive) and reports the JoinInfo placeholder as a fact only — on a Hybrid join the AD sign-in never replaces it. New EntraUserAffinityDetector: armed at the real-user desktop on Hybrid devices, the first IME-TOKEN-SUCCESS line after the desktop posts ime_user_token_acquired; ten minutes without one post entra_user_affinity_pending with the IME token-failure codes. ImeLogTracker exposes raw per-line token callbacks past the historic-replay gate; HelloTracker surfaces User Device Registration 304/305 as device_registration_event. ANALYZE-ID-004 v2.0.0 keys on those events (event_type not_exists gained a branch), GATHER-DEVICE-006 follows, GATHER-DEVICE-007/008 collect the Entra operational log. Constants + MCP catalog gain the five types (D-072), the guard test learns the multi-line OnTraceEvent shape, gather_* outputs pass the MCP filter gate by prefix. CMTrace: ImeLogEraPreScan/CmTraceEraTable anchor the backlog era that holds the bootstrap execution record against the Deployed install marker (0.13-0.36 s in the field), whole era or nothing, sibling files by local time; origin era-anchored. Backend phase_duration/app_install_duration refuse a reader-zone-fallback endpoint. Decisions D-190, D-191. Session a7140f98. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d192a8e0fa |
fix(agent): token-aware runtime launch - Scheduled Task first on stripped MSI tokens
The MDM LOB Bootstrap-MSI chain (EnterpriseDesktopAppManagement CSP -> msiexec -> runner -> bootstrap -> --install) carries a SYSTEM token with SeTimeZonePrivilege and SeSystemEnvironmentPrivilege REMOVED. WMI Win32_Process.Create duplicates the caller token onto the runtime, so every MSI-delivered agent ran degraded for its whole life: tzutil exit 5 (ACCESS_DENIED) on timezone auto-set and privilege_denied on SEC-001 firmware reads (observed 2026-08-17, sessions a11102f4/0fe2c562/3ae7528b vs. platform-script sessions 7b072755/c4c1c2d5 on the same binary). Install mode now probes its own token (TokenPrivilegeProbe, fail-soft to "held"): with a full token the proven WMI-first chain is unchanged; on a stripped token the order flips to schtasks /Run first - the Task Scheduler service starts the task with a fresh full SYSTEM token - with a 10s process-verification poll (schtasks only reports "queued", the 2026-04-30 silent-failure lesson), WMI as degraded-but-alive fallback, BootTrigger defer as the final net (full token there anyway). Also: MSI runner log encoding - PS 5.1 *>> redirection wrote UTF-16LE into the Add-Content (ANSI) msi-bootstrap.log, garbling every bootstrap line; script output now routes through the same Add-Content pipeline. Runner fix needs an MSI rebuild (wix build) to ship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ff35270d5d |
agent: legacy bootstrap-config without TenantIdWaitSeconds must not disable the wait
A bootstrap-config.json written before the TenantIdWaitSeconds property existed deserialized the missing field to 0, which the runtime treated as an explicit opt-out and skipped the 600s TenantId wait entirely — exactly the wait Hybrid-Join devices need while their Entra registration lands. TenantIdWaitSeconds is now nullable: missing/null means 'not configured' and falls through to the agent default (600s); an explicit 0 remains the legacy fast-fail opt-out. The install merge carries null through instead of freezing the default into the file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
af46160a65 |
fix(agent/v2): fail-soft runtime handoff so ASR-blocked installs don't strand devices
A customer device 2026-05-11 failed bootstrap at the WMI runtime spawn: returnValue=2 (Access Denied -- Defender ASR rule d1e49aac-8f56-4280-b9ba-993a6d77406c "Block process creations originating from PSExec and WMI commands"). The throw propagated to --install exit 1 -> bootstrap throw -> next IME run SKIPped via pre-flight, leaving the device with payload deployed + BootTrigger task registered but no live runtime until manual intervention. --install now runs a two-tier fallback: try WMI Win32_Process.Create -> try schtasks /Run on the BootTrigger task -> defer to BootTrigger on next reboot. Exit code is 0 whenever deployment + Scheduled Task creation succeeded; runtime-spawn failure is a WARNING, not fatal. The bootstrap script's Get-Process probe (Install-AutopilotMonitor.ps1:280) is now the canonical "no runtime came up" signal. Orchestration extracted to pure function DecideRuntimeLaunchOutcome, unit-tested with 8 cases pinning the contract (incl. ASR-rule GUID surfaced in diagnostic only when returnValue=2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d51bde6017 |
refactor(release): parametric per-line agent build pipeline
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>
|
||
|
|
d8e1eb9292 | Changed bootstrap script and V2 agent binary name (now without V2, preperation for V1 replacement) | ||
|
|
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>
|
||
|
|
b86c294771 |
fix(agent): merge bootstrap-config.json on --install instead of clobbering
Three follow-ups from the TenantIdAwaiter PR review: * HIGH: A redeploy that passed only --tenant-id-wait (PS1's default invocation today) was overwriting bootstrap-config.json with BootstrapToken=null / TenantId=null, pushing devices to cert-auth before their MDM cert was usable. The install path now reads the existing file, merges only fields explicitly given on this invocation, and writes the merged result back. Field-given booleans drive the merge so that prior values survive an absent CLI flag. * MEDIUM: --tenant-id-wait 0 is now a real opt-out. Previously the persist block was gated on tenantIdWaitSeconds > 0, so an admin who flipped the PS1 default to 0 and redeployed kept the old non-zero wait persisted. The gate is now "any flag explicitly set" and the merge writes 0 over the prior value when 0 is what was given. * LOW: --tenant-id-wait <SEC> is now in --help under a new "TenantId resolution" section so the start-reliability switch is discoverable. The merge logic is extracted as Program.MergeBootstrapConfig — a pure function — and covered by 7 InstallModeMergeTests cases (wait-only preserve, explicit-zero override, tenant-only preserve, token-only preserve, first-install path, PS1 default invocation, legacy existing-config without the new field). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3016c15919 |
feat(agent): event-driven TenantId wait via RegistryWatcher
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> |
||
|
|
60ea8e5c24 |
fix(diag): resolve schtasks.exe via SystemPaths in V2 install mode
Bare-name Process.Start under SYSTEM walks PATH and is a binary-hijack vector. Routes both schtasks.exe invocations in V2 install mode through the existing SystemPaths abstraction (System32-anchored). V1 left unchanged on user request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
07312ee2a3 |
fix(v2): 882fef64 PR3 — kernel-path observability (A2 + A3 + D1-D5)
Session 882fef64's 12-min agent log carried only `EnrollmentOrchestrator: started.`
and `decision terminal stage reached` between the first event and termination — no
stage-transition trail, no signal cadence, no effect failures. PR3 wires logging
into the kernel hot path so every Decision-Stage transition, every signal accept/
reject, every effect retry/critical-failure, and every IME app state change leaves
a trace at the right severity.
A2 — noise reduction:
- TelemetrySpool `enqueued itemId=...` DEBUG -> VERBOSE (600+ lines/session before)
- IME `impersonation` lines deduplicated by user with 60s rollup
A3 — strengthened existing weak lines:
- `Command line:` shows `(no args)` instead of empty
- `Process exit code: 0` -> `Process completed: <name> -> exit=N, durationMs=T`
- IME session change now carries sessionId+user
- HelloTracker 358/360 suppression includes eventTime+provider
- EnrollmentOrchestrator started/stopping carry sessionId/stage/hosts/signals
D1-D5 — observability into the silent kernel components:
- DecisionStepProcessor: per-step DEBUG (stage transitions + effects) + VERBOSE for no-ops
- SignalIngress: 5 strategic log lines (drop, append-fail, worker-fault, stop summary, processed)
- EffectRunner: 5 strategic log lines (retry, exhausted, critical, anti-loop, classifier throw)
- ImeLogTrackerAdapter: 7 emit-site log lines covering all 7 emission paths
- InformationalEventPost: per-emit DEBUG with eventType/source/immediate/severity/dataKeys
All log statements are null-safe (_logger?.Debug); legacy unit-test rigs without a
logger keep working. New ctor overloads on InformationalEventPost / SignalIngress /
EffectRunner / ImeLogTrackerAdapter are backwards-compatible (logger param defaults
null).
Side fix: BinaryIntegrityVerifierTests pinned to [Collection("SerialThreading")].
Process-global `_runtimeSelfUpdateFired` static raced with parallel mismatch tests;
PR3's logging shifted thread timing enough to surface the race consistently.
Tests: V2.Core 727, DecisionCore 231, Functions 736, vitest 179, solution build 0/0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
02bbb5490c |
feat(agent-v2): M4.6.α peripheral parity — lifecycle, install, self-update, watchdog
Closes the first batch of peripheral-parity gaps between Legacy and V2 so the
V2 agent is actually deployable/runnable in production. M4 kernel is unchanged
— only wiring and startup helpers.
Deployment + install
- Program.InstallMode.cs: --install mode ported 1:1 (payload copy, schtasks
Create/Run, bootstrap-config.json + await-enrollment.json persistence,
HKLM\SOFTWARE\AutopilotMonitor\Deployed marker)
- Configuration/BootstrapConfigFile.cs + AwaitEnrollmentConfigFile.cs
- Scheduled Task target is AutopilotMonitor.Agent.V2.exe (same task name
as Legacy — V2 replaces Legacy on the device)
Startup guards (Program.Guards.cs)
- Multi-instance guard (IsAnotherAgentInstanceRunning)
- clean-exit.marker writer registered in AppDomain.ProcessExit
- DetectPreviousExit: reads marker + crash_*.log + session files + event log
→ classifies {first_run, clean, exception_crash, hard_kill, reboot_kill}
- CheckEnrollmentCompleteMarker: ghost-restart detection via HKLM Deployed
key + enrollment-complete.marker → cleanup-retry + exit
- CheckSessionAgeEmergencyBreak: AbsoluteMaxSessionHours watchdog across
restarts (respects whiteglove.complete pause)
- WriteCrashLog: last-resort crash_YYYYMMDD_HHMMSS.log
- TryRead/Delete bootstrap-config.json + await-enrollment.json helpers
Self-update (SelfUpdater wired for the first time in V2 startup)
- CleanupPreviousUpdate + CheckAndApplyUpdateAsync called BEFORE logger init
- Cached remote-config.json read → BackendExpectedSha256 + AllowAgentDowngrade
- VersionCheckEventBuilder wired: emits agent_version_check event via
orchestrator.EventEmitter right after Start (when startup markers exist)
Agent max-lifetime watchdog (EnrollmentOrchestrator)
- New ctor param agentMaxLifetime: TimeSpan?
- New typed API: event EventHandler<EnrollmentTerminatedEventArgs> Terminated
with EnrollmentTerminationReason (MaxLifetimeExceeded | DecisionTerminalStage)
+ EnrollmentTerminationOutcome (Succeeded | Failed | TimedOut)
- M4.6.α fires MaxLifetimeExceeded only; DecisionTerminalStage is the M4.6.β
hook for CleanupService self-destruct + SummaryDialog launch
- Program.cs wires the event to the shutdown ManualResetEventSlim
Bootstrap-config cleanup (H-2 mitigation)
- BootstrapConfigCleanup.TryDeleteIfCertReadyAsync now actually called after
RemoteConfigService.FetchConfigAsync (cert probe confirms mTLS works)
SessionIdPersistence extensions
- New session.created companion file (UTC roundtrip) for the emergency break
- whiteglove.complete resume marker awareness
- SessionExists / LoadSessionCreatedAt / SaveSessionCreatedAt / IsWhiteGloveResume
- Delete now clears session.id + session.created + whiteglove.complete
Tests (+26 in V2.Core.Tests → 295 → 321)
- SessionIdPersistenceTests: +8 tests covering the new methods
- EnrollmentOrchestratorMaxLifetimeTests: watchdog fires once, respects null,
rejects non-positive, is cancelled by Stop (under SerialThreading collection)
- ProgramGuardsTests: 13 tests covering DetectPreviousExit state machine,
CheckSessionAgeEmergencyBreak (pass/trip/whiteglove-resume/recover-missing-
session.created paths), TryRead/Delete bootstrap + await-enrollment configs,
WriteCrashLog format
- V2.Core.Tests gains ProjectReference to V2 exe + V2 exe exposes
InternalsVisibleTo so the Program.Guards internal statics are testable
without duplicating code into V2.Core
All 4 suites green: 73 DecisionCore + 321 V2.Core + 507 Agent.Core + 557 Functions
= 1458 tests green.
Still open for M4.6.β: CleanupService terminal-event wiring, ServerActionDispatcher,
DiagnosticsPackageService, SummaryDialog launch + FinalStatusBuilder,
UserSessionProcessLauncher / UserProfileResolver wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|