Sprout.DevTools
0.0.0-preview0016
dotnet tool install --global Sprout.DevTools --version 0.0.0-preview0016
dotnet new tool-manifest
dotnet tool install --local Sprout.DevTools --version 0.0.0-preview0016
#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, includingtests,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 | Versions 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. |
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 |