Sprout.DevTools 0.0.0-preview0016

This is a prerelease version of Sprout.DevTools.
dotnet tool install --global Sprout.DevTools --version 0.0.0-preview0016
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Sprout.DevTools --version 0.0.0-preview0016
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Sprout.DevTools&version=0.0.0-preview0016&prerelease
                    
nuke :add-package Sprout.DevTools --version 0.0.0-preview0016
                    

Sprout.DevTools

Sprout.DevTools is Sprout's unified developer CLI. One framework-dependent .NET tool combines:

  • black-box application orchestration: build, launch, package, capture/record, UI Automation, Sandbox, and Hyper-V workflows;
  • white-box read-only inspection of applications that explicitly expose a secure local diagnostics endpoint.

The NuGet tool package is Sprout.DevTools; the installed command is sprout-devtools.

Install

dotnet tool install -g Sprout.DevTools --prerelease
sprout-devtools doctor

Requires a .NET 10 runtime on PATH. The tool package bundles its managed dependencies and declares no NuGet dependencies, but the executable is framework-dependent. Its version advances independently of the Sprout framework version.

Command surface

Domain Commands
Host and application orchestration doctor, host, vm, selftest, test, process-test, capture, record, gate, drive, experiment, build, produce, run, deploy, debug, generate, isolate, drive-shell
Runtime inspection inspect list, inspect connect, inspect snapshot, inspect export
Orchestration-owned workers (not public automation) agent, host agent, host mailbox-agent, vm pool provision-worker, hidden record-worker

Use sprout-devtools --help for the complete surface and sprout-devtools <command> --help for command-specific options.

Concurrent worktrees share one on-demand localhost target host for headless self-tests:

sprout-devtools selftest <app>
sprout-devtools host status --json

The host dispatches self-tests through FIFO target lanes and writes each result into a job-id child directory. Localhost and Sandbox admit one writer each; a managed Hyper-V pool admits one per Ready VM. --target local selects localhost explicitly; --target direct is a diagnostic escape hatch that bypasses queue protection. See docs/guide/devtools-target-host.md.

Running sprout-devtools host serve explicitly prints a readiness banner and waits for shutdown in the ordinary console. Use sprout-devtools host status --json for queue state and durable job records for execution steps. Ctrl+C exits only when idle. Automatic background startup remains silent and detached from the submitting client's terminal/stdout pipes. host status includes process creation FILETIME. Cleanup of a run-owned private broker can require that exact PID/FILETIME/payload plus --require-exit; the server compares identity atomically with idle admission and the client emits a held-handle JSON exit receipt. The strict request uses the v6+ strict-stop RPC, so an older replacement host rejects it before stop. Unqualified stop keeps its existing behavior.

selftest --target auto prefers a ready managed Hyper-V pool, then a configured Windows Sandbox, and uses localhost only when no compatible isolated provider exists. Select explicitly with --target vm --pool <name>, --target sandbox --target-tool-dir <dir>, or --target local. Brick and full-gate AOT checks use --target vm --pool default --expect-token <feature-token> so missing VM prerequisites block instead of falling back to the developer host.

Portable .NET test applications use a separate result contract:

sprout-devtools build tests\Example.Tests\Example.Tests.csproj --self-contained true --output out\tests
sprout-devtools test out\tests\Example.Tests.exe --target vm --pool default --target-tool-dir <self-contained-tool-dir>

An approved current machine may be selected only with both explicit options:

sprout-devtools test out\tests\Example.Tests.exe --target host --allow-host

Use a worktree-owned broker without cloning or replacing the managed pool:

sprout-devtools test out\tests\Example.Tests.exe --target vm --pool default `
  --target-state-dir <private-broker-root> `
  --pool-state-dir <existing-pool-root> `
  --target-tool-dir <self-contained-tool-dir>

The separate pool root is accepted only for managed Hyper-V requests, is preserved in receipts/results, and does not change the global VM lease, credential reference, immutable identity, cancellation, service cleanup, or rollback contracts. Omission preserves the prior single-root behavior.

The project uses Microsoft's generated MTP runner and TRX reporting extension. test requires positive executed/passed counts, no adverse outcomes, exit zero, and held-process cleanup. VM execution additionally requires verified guest rollback. It fingerprints the complete published payload and retains the exact TRX and filter. Host execution requires --target host --allow-host, rejects VM routing options, and is never an automatic fallback. Read portable .NET tests. The build override is explicit; omitting --self-contained preserves the project's deployment model.

Arbitrary native and console test executables use a separate VM-only contract:

sprout-devtools process-test out\native\Example.Tests.exe `
  --target vm --pool default --target-tool-dir <self-contained-tool-dir> `
  --success-token NATIVE-CONSOLE-TESTS-PASSED `
  --test-arg=--mode --test-arg strict

The literal must occur as exactly one complete stdout line and never on stderr. Exit zero, bounded output, the locked full payload, retained-process identity/exit, target-service cleanup, and VM rollback are also required. A token printed before a crash, nonzero exit, or timeout fails. There is no host, Sandbox, direct, or automatic fallback. Guest JSON and job-scoped paths are validated before accepting typed status defaults. The child inherits a temporary no-GP-fault-dialog process mode only across its serialized launch; the worker mode is restored immediately without changing WER registry policy. See native and console VM tests.

To validate an exact local DevTools build without stopping or replacing a resident target host owned by another session, use the direct authenticated isolation route:

sprout-devtools isolate out\tests\Example.Tests.exe --portable-test `
  --backend vm --pool default --devtools <self-contained-tool-dir> `
  --filter 'FullyQualifiedName~PreviewTests'

It retains the same strict TRX, locked-payload, held-process, per-VM lease, and checkpoint-rollback requirements and never falls back to the developer host. The requested timeout applies exactly to the test process; bounded worker connection, payload validation, cleanup/report, and host-receive reserves are separate. A claimed pass is rejected unless the guest result, host-selected payload manifest, executable/TRX hashes, process generation, service cleanup, and lease identity all agree.

Caller-owned existing Hyper-V guests use --existing-vm-profile on explicit VM selftest, isolate, and drive-shell. The shared resolver binds immutable VM/checkpoint identities under the lease and uses Credential Manager references, without adopting a pool or VHD. Read existing VM targets for the profile and credential lifecycle. A submitted selftest timeout requests cancellation and exposes terminal/cleanup proof; do not release transient credentials while targetJobCleanupVerified is false.

Import and provision a managed VM pool:

sprout-devtools vm image import <windows.iso> --sha256 <sha256> --source licensedByol
sprout-devtools vm pool ensure default --image <sha256> --image-index <index> --members 2 --devtools <self-contained-tool-dir>
sprout-devtools vm diagnose default --json
sprout-devtools vm pool repair default --devtools <current-tool-dir> --wait-seconds 60
sprout-devtools vm quarantine explain default --member <id> --json
sprout-devtools vm quarantine recover default --member <id> --wait-seconds 60 --json

The pool owns exact VM/VHD/checkpoint identities, keeps distinct guest passwords in Windows Credential Manager, installs the generic hvsocket target server, verifies its post-checkpoint handshake, and quarantines a member whose restore or final power-off cannot be verified. Diagnosis is read-only and never boots a VM. Member recovery uses the same exclusive lease and clears only the selected member after exact identity, checkpoint rollback, and power-off all verify, including a bounded baseline target-service health check before cleanup. Whole-pool repair hash-binds the supplied payload and leases every member. --plan-only writes preview files without changing durable pool state.

Record a bounded animation or an asserted interaction:

sprout-devtools record <app> --app-arg=--animation-demo --duration-ms 3000 --fps 15 --artifacts out\recording
sprout-devtools record <app> --app-arg=--checkbox-demo --duration-ms 3000 --drive-script steps.json

The first command publishes recording.mp4 and recording.json. The second starts the existing drive script only after the first valid frame, then reports its UIA assertions beside the video evidence. MP4 remains post-compositor triage evidence; it is not a deterministic pixel gate.

Record an already-running process without changing its window or lifecycle:

sprout-devtools record --pid 1234 --duration-ms 3000 --fps 15 --artifacts out\attached-recording

Attached recording pins the original process generation and HWND in both the supervisor and recording worker, requires Windows.Graphics.Capture without PrintWindow fallback, rejects drive scripts and all launch/package options, and never launches, activates, foregrounds, resizes, inputs into, or terminates the target. The owned worker remains bounded and may be terminated on failure. The MP4 is visual triage only and must not be included in GPU or power measurements.

Capture the current main window of an already-running process without changing its navigation or lifecycle:

sprout-devtools capture --pid 1234 --artifacts out\capture

Attach capture performs no launch, package registration, activation/foregrounding, input, resize, or termination. It pins process creation/image identity and verifies the selected HWND still belongs to that exact process before and after capture. result.json reports run.app.mode: "attached" and run.app.pid; window-capture.json records the dimensions and backend beside window.png.

Measure an already-running process without the tool launching, foregrounding, resizing, pixel/window capturing, closing, terminating, or otherwise taking lifecycle ownership of the target, and without requiring the diagnostics endpoint:

sprout-devtools experiment --pid 1234 --plan experiment-plan.json `
  --sample-ms 200 --warmup-ms 5000 --baseline-ms 10000 `
  --action-ms 10000 --cooldown-ms 5000 --artifacts out\experiment --json

Repeat --observe-gpu-pid <pid> for up to eight explicitly selected secondary processes, for example a helper process and the same-session DWM. This works in attached, metrics-only and owned experiments. All observers reuse the target's exact PDH collection, counter arrays, query generation and interval endpoints: no second query or sample loop. Each observer holds its own read-only handle, creation FILETIME and image identity, verified before and after collection. Nonpositive, duplicate, target and DevTools-self PIDs are rejected; pass exactly one integer per flag. An inaccessible, exited or identity-mismatched observer stays independently unavailable, never rebinds to a reused PID and no longer aborts the other measurements. It receives no UIA actions and is never launched or terminated. Disposal releases only its identity handles; owned cleanup still targets only the launch-owned process.

experiment.json preserves observedGpuPid, observedGpu, and the three legacy observed* phase metrics as aliases for the first requested observer, not a combined process group. Additive ordered observedGpuPids and observedGpus lists report every observer at report, sample and phase scope. Samples include creationFileTime; engines retains measured rows, while engineReadings distinguishes measured and unavailable named counters per LUID/physical-adapter/engine-index/type. Missing values are omitted, never zero. Missing/truncated engine rows do not imply inactivity. The single-observer CSV keeps its existing observedGpu column; multiple observers append a JSON-valued observedGpus column after the existing columns. Without observers, all observer fields/columns are absent. See the exact observer artifact shape. Observer busy is a maximum, never an engine/process sum, whole-adapter total, present rate or energy measurement. --require-gpu continues to apply to the primary target only.

Add --nvml-device-index 0 only when whole-device NVIDIA hardware telemetry is needed. The index is explicit and independent of PDH engine/adapter identifiers: it is not inferred from either observed PID. DevTools calls the installed System32 nvml.dll directly in the existing sample loop, with one owned initialization/shutdown pair; it does not spawn nvidia-smi, create another timer, install a library, or change GPU/power-plan/clock settings.

The opt-in deviceTelemetry sample object reports the device index/name, UTC read timestamp, collection duration, availability/diagnostic, powerW, graphicsClockMHz, memoryClockMHz, pState (0 means P0), and gpuUtilizationPct. Power is converted from NVML milliwatts to watts. The CSV adds a JSON-valued deviceTelemetry column; phase summaries add the four devicePowerW, deviceGraphicsClockMHz, deviceMemoryClockMHz, and deviceGpuUtilizationPct gauges. P-states are categorical performance levels and are not averaged. Missing libraries, unsupported readings, invalid device handles, or invalid sensor outputs remain explicitly unavailable/partial with null values, never default zero. An otherwise valid PID sample can still have unavailable hardware telemetry. A failed owned-session shutdown makes the result errored and preserves the original error/samples plus nvmlShutdownDiagnostic.

These are whole-device, not PID-attributed, readings. They do not measure CPU/system power, battery energy, or fan speed and cannot isolate the application's contribution from other GPU workloads. Driver averaging/update cadence may differ from the experiment cadence; sequential sensor calls are not an atomic hardware snapshot. The per-read collection duration exposes their sampling overhead. Without the option, no NVML work or additional output occurs.

Add --gpu-trace only when local GPU scheduling evidence is needed. This opt-in records the built-in WPR GPU.Light profile in file mode around the bounded experiment, including preparation and warm-up. It requires an elevated Windows terminal, installed wpr.exe, and verifiable named system/event collectors. It never elevates automatically, selects Network/FileIO/private payload profiles, changes a pre-existing recording, or stops the attached application. Every WPR operation uses a fresh GUID instance name as its final argument pair. Unsupported/localized WPR status output is refused rather than guessing ownership.

The gpu-trace-<id> artifact directory contains UTC boundary metadata, exact WPR commands/output, and a nonempty gpu.etl only when WPR produced one. ETL is machine-wide, not PID-filtered: it can contain other processes, system metadata, CPU stacks and GPU profile bookkeeping (including disk events). Keep the entire directory local; do not upload it or choose a cloud-synchronized directory. UNC paths, network drives, and all reparse-point ancestors (including directory junctions and symbolic links) are refused before sidecar writes. The sidecar exclusively creates its own run directory and evidence files; an existing directory is never adopted. With --gpu-trace, the report filenames experiment.json, experiment.csv, and result.json must also be absent: the complete output set is reserved with CreateNew before writing. Existing outputs are never overwritten, including failures while attaching to a missing positive PID. Non-tracing output behavior is unchanged. PDH remains the exact-PID metrics source. Recording an ETL does not prove GPU queue duration, GPU command attribution, or a rendering root cause; analyze the scheduling events separately.

Ctrl+C and experiment failure still attempt an owned-instance stop (60 seconds), status (10 seconds), and if needed partial-ETL preservation (10 seconds), owned-instance cancel (15 seconds), and final status (10 seconds). Each WPR subprocess bound is a total deadline, with a reserved termination interval: it includes process exit, termination confirmation, and stdout/stderr drain. A held-open inherited pipe cannot hold cleanup indefinitely. Partial output, unconfirmed process IDs, and termination failures remain explicit; a still-unconfirmed WPR child also prevents a successful cleanup claim even if a subsequent instance status currently reports inactive. WPR failures and incomplete traces fail the command; they are not zero-duration GPU work or successful captures. The metadata identifies the owned instance if cleanup cannot be confirmed. Force-killing DevTools, OS shutdown, or an OS refusal to stop tracing cannot guarantee cleanup; never use a global wpr -cancel for recovery. Metadata updates use atomic replacement. If evidence persistence fails, the original experiment/start failure and cleanup state survive in memory; local stderr receives structured trace metadata, and a failed final result write falls back to a structured stdout failure without relying on the damaged artifact directory.

For local analysis, open the ETL in Windows Performance Analyzer, select the GPU scheduling/usage tables available for that trace, filter by the target PID and experiment UTC interval, and save the selected table configuration as a WPA .wpaProfile. An installed WPAExporter supports:

wpaexporter.exe -i out\experiment\gpu-trace-<id>\gpu.etl `
  -profile gpu-analysis.wpaProfile -outputfolder out\gpu-tables -outputformat CSV

Use a profile actually saved by WPA; DevTools does not invent a profile schema or claim any exported GPU columns without a real ETL. Exporter -range 10s 20s selects trace-relative time, not experiment-relative time. Check wpaexporter.exe -help on the analysis machine. Exported data is local-only as well.

The plan uses schema sprout.devtools.experiment.plan.v1. Before the first discarded PDH prime and warm-up sample, the command verifies the pinned process and main HWND, creates one attached UIA client/reconnecting root, and performs a read-only provider initialization. experiment.json records the preparation outcome and result.json records a prepare step. Preparation failure writes partial errored artifacts without running an action, so target-side provider/session allocations happen before warm-up sampling and are excluded from baseline statistics.

Every action begins with its own find and may use only allowlisted UI Automation patterns, waits, and assertions; synthetic SendInput operations and UIA snapshots remain rejected. Both find and findWithin accept the same optional zero-based occurrence. Attached experiments index only same-PID Control View matches, skip foreign hosted-process subtrees before inspecting private element properties, and never persist the matched Name or AutomationId. Any find detail emitted by the drive layer is limited to query shape, observed count, and selected/requested index. This is a tool-ownership contract, not a claim that UIA patterns are lifecycle-inert: an application-defined Invoke, Select, Expand, or other trusted pattern action may intentionally navigate, open or close an application window, or request application lifecycle behavior. The prepared client and root are reused across every action while each DriveExecutor.Run resets its logical current element. Each action refreshes its OS transaction deadline, cancellation, and pinned target/window checks; a replaced main HWND reconnects only through the same-PID guard and releases the stale local wrapper. All remaining local UIA/client wrappers are released after the final cooldown/verification boundary or on error, without closing or terminating the target. Expiration is checked after every provider call, so a mutation that returns late is reported as actionTimedOut and no later step runs. Experiment UIA is confined to the attached PID: descendant finds include and recheck CurrentProcessId, and every mutating pattern rechecks it immediately before calling the provider. Ordinary drive retains its hosted cross-process UIA behavior. Baseline, every action label, and cooldown must each contain a valid sample; configured required phases must be at least one sample cadence. The target is reverified after the cooldown boundary before success. The command writes metric-only experiment.json and invariant-culture experiment.csv beside the unchanged orchestration result.json. Every tick combines process readings with exact-PID Windows PDH GPU engine and process-memory wildcard readings. GPU busy is the maximum target engine, engine sum can exceed 100 percent, and their means and baseline deltas are weighted by GPU interval duration; p50/p95 remain sample nearest-rank. Dedicated/shared bytes are deduplicated endpoint gauges with ordinary unweighted sample statistics. Each phase has one terminal collection at or after its nominal end while that phase remains active. Its start may be late by at most one quarter of sample-ms, capped at 250 ms; the report records that tolerance, nominal end, PDH-backed actual end, and both start and actual-end lateness. A later start is recorded out-of-phase and cannot satisfy coverage; the runner never waits an extra cadence to replace it. An action is canceled only after this terminal collection, then fully awaited before a fresh discarded PDH prime starts cooldown, so action execution cannot contaminate cooldown GPU intervals. Unavailable or partial GPU data remains null with an explicit diagnostic; it never becomes zero and does not invalidate process metrics by default. Add --require-gpu when every required phase must contain a fully valid GPU sample. PDH availability depends on the display-driver stack, so remote, virtualized, software-only, and restricted sessions may remain process-metrics-only.

Provider failures inside an action retain query-free actions[].failure diagnostics: the one-based step index, operation, exception type, COM HRESULT when available, and elapsed milliseconds in that step. Failed assertions also identify their step and operation, but omit duration when no step-local timing was captured. Raw provider messages, query values, and matched element identities are not copied into this object. The existing actionTimedOut / uiaUnavailable category remains, and recording more detail does not retry a mutation or extend its deadline.

Use {"op":"waitUntilScrollPercent","vertical":0} after an asynchronous scroll when the destination is known. It polls the actual Scroll pattern state with a 0.01-percentage-point tolerance, uses the existing find/action budgets and cancellation, and stops later steps if the requested state is not reached. It does not infer completion from request acceptance or a fixed delay. Unlike the existing one-shot expectScrollPercent (1 percentage point), this is a precise completion predicate. Supply only axes being observed; changing content extent may change the target percentage, so do not reuse a destination calculated against an obsolete extent.

Drive an AUMID-launched package directly from the complete loose layout produced before MSIX packing:

sprout-devtools drive out\AppX --packaged --script startup.json --artifacts out\evidence

JSON find and findWithin steps accept an optional zero-based occurrence (default 0). The query polls deterministic UIA Control View preorder until that match exists; snapshot depth/node caps do not limit finding:

[
  { "op": "find", "automationId": "sidebar.connector-list" },
  { "op": "findWithin", "controlType": "ListItem", "occurrence": 2 },
  { "op": "select" }
]

This is the no-admin Developer-Mode path for identity-sensitive startup: it preserves the real manifest, framework dependencies, capabilities, resources, and assets, then runs the ordinary UIA/capture pipeline against the PID returned by packaged activation. The declared framework packages must already be installed. Pass --packaged:false for the unchanged executable path.

For a reusable same-version development package, keep the build output separate from a stable resident layout:

sprout-devtools deploy out\AppX --package-dir out\resident-package --keep-open --artifacts out\deploy --json

The resident updater serializes first-party operations by package identity, refuses Store/non-development or ambiguous registrations and active package processes, stages and hashes before replacing the stable layout, and never uninstalls the prior registration. Application data remains attached to the unchanged identity. Use --no-launch for an update-only invocation. Leave --form omitted so the layout's real manifest is detected. See devtools-package-updates.md.

Omitting --packaged entirely detects the form instead: run, deploy, drive and capture launch an app that ships its own package under that package's identity, and anything else as a plain executable, silently. run and deploy express the same thing as --form auto, which is their default. The result document's run.app reports form, formSelection, identitySource and aumid, so which form ran is data rather than an inference. A packaged launch that fails is an errored result naming the cause — it never falls back to running the raw executable.

An isolated drive launches into a freshly restored guest, so its main-window poll has a guest-aware 30-second default instead of the interactive drive command's 10 seconds. Override the independently bounded stages when needed:

sprout-devtools isolate <app> --devtools <self-contained-tool-dir> `
    --drive-script steps.json --drive-app-arg=--demo `
    --drive-wait-ms 45000 --drive-find-timeout-ms 8000 --drive-settle-ms 1200

--drive-wait-ms bounds provider-ready window polling; it is not a sleep. --drive-find-timeout-ms bounds each UIA find, --drive-settle-ms is the explicit post-window settle delay, and --drive-no-capture skips post-action evidence. --drive-snapshot-depth forwards drive's own --snapshot-depth (default 8) into the guest — it only bounds the auto-captured uia-tree.txt/uia-snapshot.json evidence, never the find/assert gate. Negative timing/depth values fail before the app or guest launches.

Add --record --record-app-arg=--demo to run the same recorder inside Sandbox/Hyper-V. With --record --drive-script, the guest launches the app once through record and lifts video, metadata, and UIA evidence under guest\results\record.

For a signed app whose framework dependencies are absent from the guest, supply those signed packages explicitly:

sprout-devtools isolate out\publish --exe App.exe --devtools out\devtools --backend vm `
    --no-selftest --packaged --drive-packaged --drive-script steps.json `
    --dependency-package out\dependencies\Framework.msix --artifacts out\evidence

--dependency-package is repeatable: at most eight local .msix/.appx framework files, 512 MiB each and 1 GiB total. Directories, path traversal, reparse/network paths, malformed archives, duplicate identities, application/resource packages, and missing package signatures fail before guest launch. The option only supports the signed drive-only route (--no-selftest --packaged --drive-packaged --drive-script, without recording or demo captures). No dependency discovery, downloading, certificate trust, or package installation occurs on the host.

The guest uses the same Add-AppxPackage -DependencyPath transaction as the target MSIX; Windows validates signatures, publisher/version requirements, and compatibility before activation. Only the app's explicitly supplied certificate is trusted; dependency certificates are never imported. An incompatible dependency is an error, never an unpackaged, self-contained, loose-register, or synthetic-package fallback.

Dependencies automatically select strict cleanup for the in-guest drive. Their identities join the signed-session lease and pre-install snapshot: exact preexisting signed registrations are retained, conflicting versions are refused, and newly registered frameworks are removed after the target. Immutable staged copies and dependencyPackageSha256 assertions bind host inputs to the consumed guest bytes. Require the nested dependencyPackageCleanup, packageUninstall, and processCleanup receipts and the outer checkpointRollback; successful UIA alone is not a successful session. Without dependency inputs, existing behavior is unchanged. An explicit, non-isolated drive --packaged --install signed --require-cleanup accepts the same option, but installs on that command's machine; use the isolated form on a developer desktop.

isolate --backend vm --pool <name> consumes the managed pool created by vm pool ensure, including per-member Credential Manager secrets, and uses the shared cross-process lease through checkpoint rollback and power-off. Inspect its nonsecret routing identity first with vm pool status <name> --public --json. Use --vm-wait-seconds 0 for immediate failure. The old SPROUT_DEVTOOLS_HYPERV_* configuration remains available only through the explicit --legacy-vm-env option.

drive-shell --pool <name> also consumes the managed pool created by vm pool ensure, including its per-member Credential Manager secrets. Its live JSONL channel supports arbitrary UIA operations and screenshots plus display query/change and exact client resize. --capture-backend WindowsGraphicsCapture requires WGC for shell captures and failure evidence; a capture request can override the default with args.backend. Explicit WGC never falls back, and successful PNGs include requested/actual backend metadata in the response and a JSON sidecar. live-session.json records the selected VM after rollback.

Install and run

dotnet tool install -g Sprout.DevTools --version <version> --add-source <feed>
sprout-devtools doctor --json
sprout-devtools inspect list

From source:

dotnet run --project tools\Sprout.DevTools\Sprout.DevTools.csproj -- doctor --json
dotnet run --project tools\Sprout.DevTools\Sprout.DevTools.csproj -- inspect list

The unified tool is framework-dependent because its UI Automation client requires runtime COM marshalling. Target applications, Sprout.Diagnostics, Sprout.DevTools.Protocol, Sprout.DevTools.Local, and the independent Desktop Inspector retain their NativeAOT gates.

Inspect an opted-in target

The target application must explicitly enable diagnostics and start a local endpoint:

await using var diagnostics = await RuntimeDiagnostics.StartLocalAsync(
    application,
    new RuntimeDiagnosticsLocalOptions
    {
        TargetName = "my-sprout-app",
        TargetVersion = "1.0.0",
    });

Then use the process id or advertised instance id:

sprout-devtools inspect list
sprout-devtools inspect connect <pid-or-instance-id>
sprout-devtools inspect snapshot <pid-or-instance-id>
sprout-devtools inspect export <pid-or-instance-id> --output artifacts\snapshot.json

--sessions <directory> selects an isolated rendezvous directory. --connect-timeout-ms bounds pipe connection, process verification, and protocol hello. Snapshot/export --deadline-ms bounds the target-side wait for a UI safe point; it does not change the per-capture UI-thread budget.

The inspection path uses the same same-user, process-verified Local transport and source-generated Protocol contracts as the Desktop Inspector. It does not load the target's runtime assemblies or reconstruct truth from UI Automation.

Result contracts

One executable does not mean one ambiguous payload:

  • application check commands retain sprout.devtools.orchestration.result.v1, including tests, assertions, and evidence artifacts;
  • inspection commands retain sprout.devtools.cli.result.v1;
  • snapshot/export artifacts retain sprout.devtools.snapshot.v1.

Application check exit codes remain 0 for pass, 1 for an assertion failure, and 2 for skipped/errored/other non-verdict status. Management commands (doctor, host, vm), interactive/server commands, and inspection commands retain their own documented output and exit-code mappings. Consumers must branch on the command domain and schema version rather than guessing from prose.

Target-routed orchestration results add run.host.target, targetSelection, and targetReason. They remain nullable for commands that do not yet use the target host.

Architecture

Project Responsibility
tools/Sprout.DevTools Primary executable, root command, inspection commands, and shared process initialization
tools/Sprout.DevTools.Orchestration Non-packable Sprout.DevTools.Orchestration implementation module
tools/Sprout.DevTools.Orchestration.Tests Deterministic orchestration and opt-in E2E tests
packages/Sprout.DevTools.Local Secure discovery and same-user/process-verified local connection
packages/Sprout.DevTools.Protocol Bounded, source-generated wire contracts
tools/Sprout.DevTools.Desktop Independent NativeAOT graphical inspector

The Desktop application consumes typed Shell/Local APIs directly. It never launches this CLI and parses standard output.

Develop and verify

Build and test the command host:

dotnet build tools\Sprout.DevTools\Sprout.DevTools.csproj -c Debug
dotnet test tests\Sprout.DevTools.Tests\Sprout.DevTools.Tests.csproj -c Debug

Build and test the orchestration module:

dotnet build tools\Sprout.DevTools.Orchestration.slnx -c Debug
dotnet test tools\Sprout.DevTools.Orchestration.slnx --no-build -c Debug

Pack the primary tool:

dotnet pack tools\Sprout.DevTools\Sprout.DevTools.csproj -c Release

The cross-process inspection gate is scripts\verify-devtools-local.ps1. The Desktop product gate is scripts\verify-devtools-desktop.ps1.

Boundaries

  • No automatic endpoint: applications must opt in at package, feature-switch, and runtime levels.
  • No mutation, arbitrary property getter, reactive-value hydration, or general remote execution.
  • No inspection via process-memory scraping or UI Automation inference.
  • No target-framework reference from the orchestration module.
  • No second CLI package, command, or compatibility alias.

The command architecture is recorded in ADR 0365. The transport and target boundary are recorded in the Local client ADR.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.0.0-preview0016 0 9/18/2026
0.0.0-preview0015 6 9/18/2026
0.0.0-preview0014 42 9/16/2026
0.0.0-preview0012 54 9/14/2026
0.0.0-preview0011 47 9/13/2026
0.0.0-preview0010 45 9/13/2026
0.0.0-preview0009 53 9/13/2026
0.0.0-preview0008 45 9/13/2026
0.0.0-preview0007 55 9/13/2026
0.0.0-preview0006 51 9/13/2026
0.0.0-preview0005 53 9/12/2026
0.0.0-preview0004 60 9/12/2026
0.0.0-preview0003 76 8/30/2026
0.0.0-preview0002 104 8/3/2026
0.0.0-preview0001 72 7/30/2026