Skip to content

Measuring e2e code coverage locally

Reflow can record how much of a Go application, or of a Swift, C, C++ or Objective-C application built with LLVM source-based coverage, your end-to-end flows exercise. You build the application with coverage instrumentation, run it while you apply flows, stop it gracefully, and ingest the profile it writes. Reflow stores one canonical document per run window, tied to the exact commit and tree it was measured against. For Go, that is checked against the build stamp in the binary. For LLVM, it is the worktree you bind, because LLVM binaries carry no commit stamp. Go and LLVM programs produce separate documents; see Swift and other LLVM languages and Coverage from a MacOSApp run. To query stored documents, see Reading coverage as an agent.

This page covers the local, process-level workflow (reflow coverage). It measures whole processes over a run window. Per-flow numbers and an automatic counter-reset surface are not available yet.

  • For Go: a Go module whose server or worker runs from your own main package. For Swift and other LLVM languages, see their section for the requirements.
  • For Go: Go 1.20 or later (the examples were verified with Go 1.26.1). The go on your PATH must be the same version that built the binary.
  • A Reflow login and a selected collection for that login: either reflow collections select <id> or a target selected with reflow target <url> --group <name> --collection <id>. Otherwise pass --collection <id> to each command.

Go only starts its coverage runtime from an instrumented main package. If main is excluded from -coverpkg, the build still succeeds, but coverage is silently inactive: no counters are written when the process exits. Always build the binary whose main you run with -cover, and include main in the instrumented packages.

Injecting coverage with go build -overlay is not supported. Go’s cover tool does not apply overlays, which forces main out of the instrumented set.

Choose one of the two supported setups. Both must build with -buildvcs=true (the default inside a Git checkout) so that the binary records the commit it was built from. reflow coverage bind refuses binaries without that stamp.

Setup (a): a build-tagged adapter in package main

Section titled “Setup (a): a build-tagged adapter in package main”

Commit this file next to your main.go. The build tag keeps it out of normal builds.

//go:build reflowcoverage
package main
// Reflow coverage startup check (setup a). Runs from init(); `reflow coverage verify` is the backstop.
import (
"fmt"
"os"
"path/filepath"
"runtime/coverage"
)
func init() {
dir := os.Getenv("GOCOVERDIR")
if dir == "" {
fmt.Fprintln(os.Stderr, "reflow coverage: GOCOVERDIR is not set")
os.Exit(70)
}
if err := coverage.WriteMetaDir(dir); err != nil {
fmt.Fprintln(os.Stderr, "reflow coverage: coverage is not active:", err)
os.Exit(70)
}
marker := filepath.Join(dir, fmt.Sprintf("reflow-start.%d", os.Getpid()))
if err := os.WriteFile(marker, nil, 0o600); err != nil {
fmt.Fprintln(os.Stderr, "reflow coverage: marker:", err)
os.Exit(70)
}
}

Save it as zz_reflow_coverage.go. At startup it checks that coverage is active, exits with status 70 if it is not, and writes a reflow-start.<pid> marker that reflow coverage verify counts.

Build from inside the checkout:

Terminal window
go build -buildvcs=true -tags reflowcoverage -cover -covermode=atomic \
-coverpkg=./... -o /tmp/app-cov/app .

Setup (b): a harness module committed inside the app repository

Section titled “Setup (b): a harness module committed inside the app repository”

If you cannot add a file to your main package, commit a small nested module, for example internal/reflowcov/, whose own main calls your application’s command package and includes the same adapter file:

internal/reflowcov/go.mod
module example.com/app/internal/reflowcov
go 1.26
require example.com/app v0.0.0
replace example.com/app => ../..
internal/reflowcov/main.go
package main
import "example.com/app/cmd/server"
func main() { server.Main() }

Run go mod tidy in the harness directory and commit its go.sum. Build it with your application’s packages instrumented; the pattern also covers the harness’s own main:

Terminal window
cd internal/reflowcov
go build -buildvcs=true -tags reflowcoverage -cover -covermode=atomic \
-coverpkg=example.com/app/... -o /tmp/app-cov/app .

The harness must live inside the application repository. Go stamps the VCS revision from the repository that contains the main module, so a harness kept elsewhere carries a foreign or missing stamp, which bind refuses. Reflow maps Go import paths to files through every go.mod in the committed tree, so nested modules resolve to their real paths.

Go writes coverage counters only when main returns or the program calls os.Exit. A process killed by an unhandled signal writes nothing. Handle SIGTERM and SIGINT so that main returns after a graceful shutdown:

func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: routes()}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdown)
// Returning from main flushes the coverage counters.
}

Run the built binary directly. go run starts a separate child process and is not supported.

Put the cover directory and the instrumented binary outside the repository checkout, for example under /tmp/app-cov/. bind records whether the worktree is clean with git status; files it can see make every document dirty, and Go stamps later builds with vcs.modified=true. If they must live inside the checkout, git-ignore them. bind refuses a cover directory inside the worktree that is not git-ignored (cover_dir_in_worktree) and warns about such a binary.

  1. Build the binary with setup (a) or (b), from a committed tree. Go records vcs.modified=true for untracked files as well as modified ones, so build from a clean checkout or worktree: git status --porcelain must print nothing. bind refuses a binary stamped vcs.modified=true when the worktree is clean, and warns when it binds a dirty worktree.

  2. Bind a fresh, empty cover directory to the binary and the current commit:

    Terminal window
    reflow coverage bind --binary /tmp/app-cov/app --cover-dir /tmp/app-cov/data \
    --exclude '**/testenv/**' --exclude '**/*_gen.go' --json

    bind creates the directory (mode 700) if needed and refuses one that is not empty, or one inside the worktree that is not git-ignored. It records the commit, tree, object format and dirty state, the binary’s SHA-256 and its go version -m build information, and writes reflow-coverage-binding.json (mode 600) in the cover directory. It refuses a binary that has no VCS stamp, was built from a different commit, was built from a modified tree while the worktree is clean, or was not built with -cover. The repository label comes from the origin remote as host/owner/repo; pass --repository <label> to set it explicitly. Optional --exclude globs are recorded in the binding file, and bind prints how many tracked non-test .go files each one matches; see Excluding code from the denominator. The Next: line names verify when the binary was built with -tags reflowcoverage (the adapter is compiled in), and the exit-only steps otherwise.

  3. Start every instrumented process (for example a server and a worker running the same binary) with the cover directory in its environment, using your usual process manager:

    Terminal window
    GOCOVERDIR=/tmp/app-cov/data /tmp/app-cov/app serve &
    GOCOVERDIR=/tmp/app-cov/data /tmp/app-cov/app worker &
  4. Verify that coverage is active in exactly the processes you expect:

    Terminal window
    reflow coverage verify --cover-dir /tmp/app-cov/data --processes 2

    Within --timeout-ms (default 10000), exactly one covmeta.* file and exactly that many reflow-start.<pid> markers must exist. Reflow then reads the cover mode with go tool covdata debugdump; go env GOVERSION must match the binary’s Go version. The mode and process IDs are recorded in the binding file. A build that leaves main uninstrumented fails here.

    When several processes start at the same moment, one may occasionally print coverage meta-data emit failed ... rename .... The processes share the same meta-data, and verify confirms the single covmeta.* file. If verify passes, the run is complete; it fails when the meta-data is missing.

  5. Apply your flows as usual. Keep each apply’s request identity (request.idempotency_key in reflow apply --json) if you want the document to name the batches it covers.

    Terminal window
    reflow apply --json --result-file /tmp/app-cov/apply.json
  6. Stop the processes gracefully with SIGTERM, so each one returns from main and writes its covcounters.* file.

  7. Ingest the cover directory:

    Terminal window
    reflow coverage ingest --cover-dir /tmp/app-cov/data \
    --batch <idempotency-key> --json

    ingest requires the unmodified binding file and a completed verify; no process still writing (counters_in_progress); exactly one covmeta.* file, matching the verified one; reflow-start.* markers for exactly the verified processes (process_count if one started after verify); and exactly one complete covcounters.* file for each verified process and for no other (missing_counters, unverified_counters, duplicate_pid, counter_incomplete). It then derives the profile itself: you no longer run go tool covdata textfmt. ingest checks that go env GOVERSION equals the binary’s Go version (go_version_mismatch; every Reflow go call runs with GOROOT unset, so the go first on PATH uses its own toolchain and an inherited GOROOT of another release cannot break covdata), copies exactly the checked files into a private temporary directory, checks the copies again (cover_dir_changed if a file was replaced, grew or became a link after the checks), runs go tool covdata textfmt on that copy only, removes the directory afterwards, and requires the profile’s mode: to equal the verified mode. Deriving the profile from a copy of the checked files means the document cannot describe a profile from another run. It maps import paths to repository files through the go.mod files of the bound commit’s tree (not your current worktree), builds one canonical document, uploads it and prints the document ID, totals, batch counts and how many files each exclude glob matched. Byte-identical canonical documents (same profile, binding, exclude globs, producer and scope, including batch keys) dedupe: an identical document already stored in the collection is reused rather than stored twice. Batch keys are part of the document, so the same profile ingested with different --batch keys, or once with and once without, is stored as separate documents. Profiles larger than 64 MiB or 2,000,000 blocks are refused as too_large, never truncated. --batch is repeatable, up to 64 keys.

    Pass every batch that ran during the window, whatever its outcome: a batch whose runs failed, were skipped or ended unknown still drove traffic that is in the counters, and Reflow accepts any key that has runs in the collection (up to 1,000 runs per batch). An apply refused before admission (its preparation failed, its group was still owned) ran no flow and launched no process: when the server refuses such a key (unknown_batch), ingest reads each key’s runs, leaves out the keys with no run at all, ingests the batches that ran and names the left-out keys in emptyBatches and a warning. --processes stays the count you launched. Ingest still refuses unknown_batch when no key has runs, or when a refused key’s runs are in another collection. For each key the document reports sameBatchReusedFlows (runs in that batch with at least one step, every one of them reused) and executedFlows (the batch’s other runs: at least one step ran rather than being reused, or the run has no steps). A flow that the plan reused whole from an earlier batch creates no run in the new batch, so it is in neither count; pass the earlier batch’s key too if its traffic belongs to the window. reusedFlows is a deprecated alias of sameBatchReusedFlows. For the whole document, ingest also prints flows: executions (the batches’ executedFlows summed), distinctFlows (distinct flows among them) and crossBatchReruns (a flow that ran again in a later batch of the window counts once in distinctFlows); documents and show report the same.

    You may still pass a profile, as [--format go-textfmt] <file>, for example one your own tooling already produced. It must be byte-identical to the derived profile, or ingest refuses it as profile_mismatch and reports both sizes and SHA-256 digests. go tool covdata textfmt sorts packages and blocks, so the output is the same every time for the same cover directory.

  8. Inspect the stored result:

    Terminal window
    reflow coverage status --latest
    reflow coverage status --latest --clean --top 20
    reflow coverage status --document <document-id> --json

    --latest selects the newest finalized document; --clean limits it to documents bound to a clean worktree. The output shows statement coverage first, then line coverage, the instrumented-package denominator and any exclusions, followed by the packages with the most uncovered statements (--top N, default 10, at most 100). The attribution line shows the tier: process (verified) or process (exit-only). Ties are ordered by package name. Packages beyond the 2,000-row limit are folded into one row; its uncovered statements are reported separately and never ranked. With --json, the result adds percent (statements, lines; after exclusions, two decimals, null when there is nothing to count), rawPercent (the same figures without exclusions) and topUncovered (package, uncoveredStmts, stmtsFound, stmtsHit, percent), plus excludeMatches (see Excluding code from the denominator).

Use a new, empty cover directory and a new bind for each run window.

If you cannot add the startup adapter or a harness module, you can still measure a run window with a plain -cover build. This path is weaker than the verified path above (setup (a) or (b)), which remains the recommended setup: nothing checks the processes when they start, so Reflow checks the run only against the files Go writes: the covmeta.* file written when an instrumented process starts, and the covcounters.* file written when it exits. Documents ingested this way are marked exit-only.

  1. Build from a clean, committed tree, with no adapter and no build tag. Go records vcs.modified=true for untracked files as well as modified ones, so build from a clean checkout or worktree: git status --porcelain must print nothing.

    Terminal window
    go build -buildvcs=true -cover -covermode=atomic -coverpkg=./... \
    -o /tmp/app-cov/app .
  2. Bind a fresh, empty cover directory before running anything. Keep it outside the worktree, or git-ignore it:

    Terminal window
    reflow coverage bind --binary /tmp/app-cov/app --cover-dir /tmp/app-cov/data

    Without -tags reflowcoverage in the binary’s build information, the Next: line describes this path: run with GOCOVERDIR, stop with SIGTERM, then ingest --processes N.

  3. Start every process with GOCOVERDIR=/tmp/app-cov/data, then apply your flows. Skip reflow coverage verify; it needs the adapter’s startup markers.

  4. Stop every process and wait for all of them to exit. A plain Go binary writes its counters only when main returns or the program calls os.Exit. Go’s default SIGTERM handling kills the process without writing anything, so exit-only cannot rescue an application that does not return from main on SIGTERM. Handle the signal as shown in Make SIGTERM return from main.

  5. Ingest, giving the number of processes that ran. ingest derives the profile itself, exactly as on the verified path:

    Terminal window
    reflow coverage ingest --cover-dir /tmp/app-cov/data --processes 2 --json

    If ingest reports counter_count with no counter files at all, the processes may still be running: stop them with SIGTERM (main must return), then ingest again.

ingest --processes checks, in order: the binary still has the SHA-256 recorded at bind (binary_changed, binary_missing); no process is still writing (counters_in_progress); exactly one covmeta.* file exists (no_covmeta: the binary was not built with -cover, GOCOVERDIR was wrong or unset, or no process started; multiple_covmeta); it is a valid Go coverage meta-data file whose hash matches its contents and its name (covmeta_version, covmeta_corrupt, invalid_mode, covmeta_hash_mismatch); every package it describes is embedded in the bound binary (covmeta_not_from_binary); there are exactly that many complete counter files from distinct processes (counter_hash_mismatch, counter_count, duplicate_pid, counter_incomplete), each written on the bound binary’s platform (counter_platform_mismatch). Only then does it derive the profile, with the same go env GOVERSION check (go_version_mismatch), the same private copy of exactly the checked files (cover_dir_changed) and the same optional byte-identical <file> comparison (profile_mismatch) as the verified path, and require the profile’s mode to match the covmeta (mode_mismatch). A cover directory that was verified refuses --processes; ingest it without the flag.

Both tiers derive the profile from a copy of exactly the files they checked, so neither document can include counters from outside the run window that ingest checked. The tiers differ in what those checks can prove. What exit-only cannot prove, compared with the verified path:

  • No startup check. Nothing confirms that coverage was active in each process when it started. A process that never started, crashed or was killed without returning from main is detected only as a missing counter file (counter_count).
  • Process IDs are not registered in advance with --processes <n>. Reflow counts the counter files that exist; it cannot tell that they came from the processes you meant to run. Record each start instead (below) to require exactly the processes you launched.
  • Another build of the same source is not detected when it differs only in -covermode or uses a narrower -coverpkg: its packages are still embedded in the bound binary. Compare the document’s instrumented packages with the packages you expect; reflow coverage status prints the count for exit-only documents (instrumentedPackages in --json).

Instead of a number, you can record every instrumented start as it happens and have ingest require exactly those processes. No app change is needed, and the process stays a direct child of whatever started it.

  1. After bind, start each process with GOCOVERDIR and record its pid:

    Terminal window
    GOCOVERDIR=/tmp/app-cov/data /tmp/app-cov/app &
    reflow coverage record-start --cover-dir /tmp/app-cov/data --pid $!

    record-start writes an owner-only reflow-launch.<pid>.<16 hex> file that holds only the format, the pid and the time: never the command line, the environment, the working directory or a binary hash. It makes no network call. It refuses a cover directory without a binding (no_binding) or one that was verified (usage).

  2. Stop every process as above, then ingest with --processes launched:

    Terminal window
    reflow coverage ingest --cover-dir /tmp/app-cov/data --processes launched --json

    The pids of the launch records must equal the pids of the counter files:

    • a counter file whose pid no record names is unrecorded_counters (a process you did not record wrote into this directory);
    • a record without a counter file is missing_counters (the process crashed, was killed or is still running);
    • no records at all is no_launch_records, and a malformed record is launch_record_invalid;
    • the same pid recorded twice is duplicate_launch_record.

    Two counter files from one pid stay duplicate_pid, so pids must be unique within a cover directory. Every other exit-only check runs unchanged, in the same order. Both pid sets are in the refusal details.

The ingest result reports processes with the source (count, launched or verified), the expected and found counts and the pids. The stored document itself does not carry the count yet: cite the ingest result as the evidence of how many processes ran.

When the processes write their profiles into another directory, for example a VM guest whose GOCOVERDIR is exported to a new host directory, keep the bound cover directory for the binding and point exit-only ingest (and check) at the exported files instead of copying them:

Terminal window
reflow coverage ingest --cover-dir /tmp/app-cov/data --data-dir /exports/run-1/go --processes 2 --json

--data-dir reads covmeta.*, covcounters.* and tmp.* from that directory and ignores its other files; the binding and any launch records stay in --cover-dir. Every exit-only check above applies unchanged. It requires --processes <n> or --processes launched (usage otherwise), because verify watches the cover directory itself, and it refuses a missing directory or a link (data_dir_invalid). You assert that the directory holds exactly this window’s files: like a copy into the bound directory, it has no fresh-directory guarantee.

reflow coverage check --cover-dir <dir> is a read-only, offline report. It makes no API call. It reports:

  • the binding, and whether it was verified;
  • the covmeta hash and how many covmeta files exist;
  • the counter files, their distinct and duplicate pids, and their GOOS/GOARCH (read from each counter file; the command line Go records there is discarded and never printed);
  • tmp.* files that are still being written;
  • the launch records and any unmatched pids.

Without --processes it exits 0 and says the result is not asserted. With --processes <n> or --processes launched it runs the same cover-directory checks as an exit-only ingest, except the bound binary’s hash and package scan, and exits non-zero with ingest’s refusal code (in error.code, beside the report).

The bound binary may be cross-compiled for another OS or architecture, for example a Linux binary run in Docker from a macOS host. Build it on the host at the bound commit with CGO_ENABLED=0 -buildvcs=true, bind it there, bind-mount that exact file read-only into the container, and give the container a cover directory it can write (a non-root image user that cannot write it produces no covmeta). Ingest on the host with the same Go toolchain that built the binary.

Every counter file must have been written on the binding’s platform. Its GOOS/GOARCH must equal the binding’s go version -m GOOS/GOARCH, otherwise ingest refuses counter_platform_mismatch (a binding without GOOS/GOARCH is refused too).

Each docker run makes the process pid 1, so a second start in the same cover directory refuses duplicate_pid. Run the container with --pid=host and record the pid Docker reports, or use one cover directory per start:

Terminal window
docker run -d --pid=host --name app -v /tmp/app-cov/app:/app:ro \
-v /tmp/app-cov/data:/cov -e GOCOVERDIR=/cov image /app
reflow coverage record-start --cover-dir /tmp/app-cov/data \
--pid "$(docker inspect -f '{{.State.Pid}}' app)"

docker stop sends SIGTERM, so main must return on it.

  • Statement coverage is the primary figure. stmtsHit / stmtsFound uses Go’s own statement counts and matches the percentage go tool cover -func reports for the same profile.
  • Line coverage (Reflow) is shown second. It counts source lines covered by Go’s statement blocks; a line is hit when any block on that line ran. It differs from Go’s statement percentage, so Reflow labels it explicitly.
  • Denominator: instrumented packages. Only packages compiled with coverage appear. Code you excluded from -coverpkg is absent, not counted as uncovered, and every percentage is shown with that note.
  • Refused entries. Profile entries with absolute paths, or import paths that do not belong to a module in the bound tree (such as dependencies), are counted as refused and never stored.

Generated code, test environments or vendored helpers can dominate the denominator without being meaningful targets for end-to-end flows. Pass --exclude <glob> to bind, once per glob:

Terminal window
reflow coverage bind --binary /tmp/app-cov/app --cover-dir /tmp/app-cov/data \
--exclude '**/testenv/**' --exclude '**/gen/**' --exclude 'internal/k8s/**' \
--exclude '**/*_gen.go'

Globs are anchored at the repository root: they match the whole repository-relative path of a file, with / separators. testenv/** matches only a top-level testenv/ directory, not server/internal/testenv/env.go; to match a directory at any depth, start the glob with **/, as in **/testenv/**.

PatternMatches
** (a whole segment)Zero or more whole path segments
*Any run of characters within one segment, never /
?Exactly one character other than /
anything elseItself, literally (including ., [, { and spaces)

So **/*_gen.go matches x_gen.go and a/b/x_gen.go, testenv/** matches everything under testenv/, and *.go matches only files at the repository root. Globs must be printable ASCII, 1 to 256 characters, without a leading /, a .. segment or \; at most 64 are allowed. They are recorded in the binding file and covered by its digest.

Each command reports how many files every glob matched, so a glob that misses is visible at once:

  • bind counts tracked repository files: the tracked non-test .go files in the bound commit’s tree.
  • ingest counts instrumented profile files: the files in the canonical document, which only lists files of instrumented packages.
  • status prints the instrumented profile file counts the server recorded when it stored the document; documents stored before these counts existed show “counts unavailable”. Run inside a checkout that has the bound commit, it also prints the tracked repository file counts.

The two numbers differ whenever the build did not instrument every package a glob covers (for example generated packages left out of -coverpkg), or an instrumented package has files with no statements, which go tool covdata textfmt does not list (type-, const- or var-only files are common in generated code). Files refused as absolute or outside the repository are not counted either. Each line says which number it is.

A glob that matches nothing produces a warning that suggests the **/ form.

Exclusion is applied only when the numbers are aggregated. The uploaded document still contains every file and the complete totals, plus the list of globs. Package rows, the headline percentages and the top uncovered packages use only files that no glob matches, and the output lists the globs with the number of excluded files, statements and lines. rawPercent keeps the figures before exclusion.

A real Go service measured during design (Gram) had a denominator of 232,404 statements and 366,326 lines across 2,510 files in 463 package directories. Each process wrote about 1.9 MB of counters, a counter flush took about 3 ms on average, and ingest took 0.6 seconds. That is well inside the limits: 20,000 files, 2,000,000 ranges, a 64 MiB profile and a 16 MiB compressed document.

The document also lists the instrumented packages (their import paths). At most 4,096 are allowed, each 1 to 256 bytes and 60 KiB (61,440 bytes) in total; beyond that the document is refused as too_large. The list is stored complete, before any --exclude, in a database field whose size cannot grow without downtime, so these caps leave room for every pattern plus its JSON quoting. The byte cap is usually the one reached first: Gram’s 655 packages take about 45 KB, and a repository whose import paths average 68 bytes reaches the cap at about 850 packages. Import paths may contain only ASCII letters, digits and -._~+!@, separated by single slashes (Go itself accepts only -._~+ besides letters and digits), so no pattern grows when it is written as JSON; a path outside that set is refused as invalid_canonical. Narrow -coverpkg if a very large repository exceeds the caps.

Terminal window
reflow coverage delete --document <document-id>

delete retires one finalized document. Its uploader can delete it, and so can a team owner or admin using an admin-scoped token (forbidden otherwise). The document disappears from status at once, and its stored object is removed in the background. Repeating the command while the document is still being deleted succeeds; a repeat after it has been removed reports not_found. A reservation that was never finalized is refused as not_finalized and expires on its own. Otherwise retention removes documents: each collection keeps its latest 50 clean and, separately, its latest 20 dirty documents.

Apps built with LLVM source-based coverage (Swift, C, C++, Objective-C) use --format llvm-cov. Their documents are separate from Go documents: they report line and region coverage, have no statement coverage, and are never blended with a Go helper’s numbers. reflow coverage status summarizes Go documents only: for an LLVM document it reports unsupported_format and names the reads to use instead, reflow coverage show and reflow coverage files.

What you need:

  • An llvm-cov that writes JSON export format 2.0.1 or 3.0.1. Reflow accepts only these two and refuses any other version as unsupported_llvm_export. The llvm-cov in Command Line Tools 27 writes 3.0.1. To check yours, merge the profile from a trial run (not in a bound directory) and read the version at the end of a summary export:

    Terminal window
    xcrun llvm-profdata merge -sparse <trial>.profraw -o <trial>.profdata
    xcrun llvm-cov export -summary-only -instr-profile <trial>.profdata <binary> | tail -c 40

    The output ends with "version":"3.0.1"} or "version":"2.0.1"}. On Linux, run the tools without xcrun.

  1. Build the instrumented binary, for example with SwiftPM: swift build -c release -Xswiftc -profile-generate -Xswiftc -profile-coverage-mapping -Xswiftc -file-prefix-map -Xswiftc "$PWD=.". In Xcode, pass the same compiler flags (-profile-generate, -profile-coverage-mapping, -file-prefix-map <root>=.) to the target that builds the app. The prefix map keeps recorded source paths relative, but the result depends on the directory you build from (a package in a subdirectory can record a doubled prefix). Check one recorded file name, for example with llvm-cov export -format=lcov ... | grep -m1 ^SF:, and set --path-strip to match, or "" when it is already repository-relative.
  2. Bind a fresh, empty directory outside the worktree to the exact binary that will run and the prefix to strip from recorded paths. For an app bundle that is the Mach-O executable in <App>.app/Contents/MacOS/. Bind the copy that actually launches: re-signing or copying from another build changes its bytes, and ingest refuses binary_changed. reflow coverage bind --format llvm-cov --binary <binary> --cover-dir <dir> --path-strip <prefix>. If the recorded paths are already relative to the repository root (for example a build with -file-prefix-map <root>=.), pass an explicitly empty prefix: --path-strip "" (or --path-strip=). Every stripped path must be a file in the bound commit; other paths are counted as refused, not stored.
  3. Run each instrumented process with LLVM_PROFILE_FILE=<dir>/<name>-%p.profraw (%p gives one file per process; %m also works; do not use %c continuous mode; a fixed file name lets later processes overwrite earlier profiles). Children inherit the variable when the parent passes its environment through. Quit each process normally (for a Cocoa app, its own Quit command). By default the LLVM profile runtime writes the profile from its exit handler, so only a normal exit writes it: SIGKILL and a crash never do, and a signal the program does not handle (for example SIGTERM sent to an ordinary Cocoa app) ends it without writing. An app that handles SIGTERM or SIGINT itself and then exits normally does write its profile; rely on that only for apps whose handler you know. Wait until every process has exited before ingesting.
  4. Ingest with the exact process count: reflow coverage ingest --format llvm-cov --cover-dir <dir> --processes <n> [--batch <key>].... n is the number of instrumented LLVM processes you launched in the window, counted from what you started, not from the files you find. With %p each of them must leave one .profraw. Ingest refuses (profile_count_mismatch) unless exactly n profiles exist, and refuses any other file in the directory. A missing profile is missing evidence, not 0%: never lower n to match the files, which would silently accept a process that was killed or crashed; fix the stop and start a new window. Profiles exported elsewhere (a VM guest) can be read in place with --data-dir <dir>; the binding stays in --cover-dir and the same checks apply to that directory.

Ingest merges the raw profiles with llvm-profdata, exports them with llvm-cov (through xcrun on macOS) and refuses unless the per-line result equals llvm-cov’s lcov line records and the regions equal its region summary. It also refuses if the binary or the llvm tool versions changed since bind. Line counts are LLVM’s per-line view; llvm-cov’s per-file line summary adds up per-function counts and can exceed it. These documents are always exit-only: nothing verifies at startup that profiling is active, and the binary carries no commit stamp, so the source binding is the worktree you bound.

The MacOSApp plugin launches an isolated copy of a macOS app for each step. To measure that app (and any Go helper it starts), set up one run window per producer and give the app the coverage variables through its binding.

  1. One directory and one bind per producer. Create a fresh, empty directory outside the worktree for each instrumented program, for example /tmp/myapp-cov/swift for the app and /tmp/myapp-cov/go for a Go helper. Bind each one to the exact binary that will run: --format llvm-cov with the Mach-O executable inside the isolated copy named by the binding’s application, and the Go helper binary with a plain bind. The LLVM document is always exit-only. Go and LLVM documents are separate; never add their counts or quote one blended figure.

  2. Pass the variables through environmentFile. In a Reflow run, the binding’s private environmentFile is the only way to give the app GOCOVERDIR or LLVM_PROFILE_FILE: Reflow starts plugins with a fixed minimal environment, so variables exported in your shell or in the device service’s environment do not reach the app. The file is read on the device that runs the step, so use absolute paths on that machine:

    {
    "LLVM_PROFILE_FILE": "/tmp/myapp-cov/swift/myapp-%p.profraw",
    "GOCOVERDIR": "/tmp/myapp-cov/go"
    }

    Keep it chmod 600. The app’s children inherit these variables when the app passes its environment through, which is how a Go helper started by the app receives GOCOVERDIR. Launching through LaunchServices or open instead may not propagate them. The step’s state records the file’s SHA-256 (inputs.environmentFileSha256), not its values.

  3. Apply your flows and keep each apply’s request identity as a batch key: request.idempotency_key from reflow apply --json, or the request_id you pass to manage_execution action: "apply" over MCP.

  4. Count actual launches, not flows. Count each instrumented runtime separately, from the processes the window started:

    • App (LLVM): every MacOSApp run and inspect step that launches the app starts one app process; add any other process of the same bound executable started in the window. Steps reused from earlier evidence and steps refused before launch start nothing. That total is the app’s n. A different instrumented binary, such as a Go helper or another LLVM helper, is a separate document with its own directory, bind and count.
    • Go helper: every Go process started with that GOCOVERDIR: each helper the app started, each child that inherited the variable, and any other run of the same binary in the window.

    Every counted process must write its file. MacOSApp quits the app through its own Quit command when it can, and the step’s state records how it stopped (lifecycle.appStop, lifecycle.forced). graceful_quit with lifecycle.forced: false is a normal exit through the app’s own Quit, the safe path for a Cocoa app. terminated means SIGTERM: whether a profile was written depends on how the app handles it; for an ordinary Cocoa app expect none. killed means SIGKILL, which writes nothing. A Go process writes counters when main returns or it calls os.Exit; a crash or SIGKILL writes nothing, and SIGTERM writes only if the program handles it and returns. If a counted process left no file, the ingest refuses: do not lower n; fix the stop and run a new window.

  5. Ingest each directory with the same batch keys, after every process has exited:

    Terminal window
    reflow coverage ingest --format llvm-cov --cover-dir /tmp/myapp-cov/swift --processes <app-n> --batch <key>
    reflow coverage ingest --cover-dir /tmp/myapp-cov/go --processes <go-n> --batch <key>

    A missing profile or counter file refuses the ingest. It is missing evidence, never 0%: start a new window rather than editing the count.

The Browser plugin can measure the JavaScript a page runs with V8 precise coverage, mapped through source maps to your TypeScript or JavaScript files. It stores reflow.code-coverage/3 documents, separate from Go and LLVM documents; coverage diff judges each changed file by the format that measures it. It is off by default: the browser compiles page code with coverage counters, which slows it.

  1. Bind a fresh directory outside the worktree (or git-ignored):

    Terminal window
    reflow coverage bind --format browser-js --cover-dir /tmp/web-cov/run1 \
    --web-root client/dashboard

    --web-root is the repository directory the dev server serves URL paths from (Vite’s root). Without it, each source maps to the one tracked file whose path ends in the URL path.

  2. Point the Browser binding at it with "coverageDirectory": "/tmp/web-cov/run1" (Chromium only). After every step and when the browser closes, the provider records coverage for the target origin’s scripts, plus the transformed scripts and source maps. Scripts under /node_modules/ and Vite’s own /@vite/ modules are skipped. Each browser session writes one profile, marked closed only after its final record.

  3. Apply your flows, keeping each apply’s request identity as a batch key.

  4. Ingest after every browser has closed:

    Terminal window
    reflow coverage ingest --format browser-js --cover-dir /tmp/web-cov/run1 --batch <key>

    An unclosed profile (a killed browser) refuses session_unfinished: that is missing evidence, never 0%. --processes <n> asserts the session count.

A line is instrumentable when the source map maps generated code to it; its count is the innermost V8 block count there, so a branch that never ran is missed. Every original source’s sourcesContent is checked against the bound tree’s Git blob: a source the server served from another checkout is left out of the document and named in a warning (served_source_mismatch when nothing else maps), never attributed to the bound commit’s lines. A module no session loaded is absent from the document, so coverage diff reports it as unmeasured. Counts a renderer accumulated after the last record are lost when a navigation replaces it.

To record only which modules each run’s page loaded, without coverage, set "loadedModules": true in the Browser binding: every snapshot then retains the modules the session loaded so far, and reflow evidence diff compares both runs’ served sources (see reflow evidence --help). A flow needs at least one snapshot for this record.

A document describes everything the instrumented processes executed during the run window: startup, background jobs and requests from anything else that talked to them, as well as your flows. Batch keys record which applies ran in the window and how many flows each executed or reused. They do not mean every covered line was caused by those flows.

A document is clean only when git status --porcelain=v2 --untracked-files=normal --ignore-submodules=none printed nothing at bind time. Otherwise it is dirty and records a digest of that status output. Dirty documents are for local status only. They are never review or pull request evidence. Build and bind from a committed, clean tree for results you want to share.

For a commit review with per-hunk verdicts across binary members, see Changed-line coverage and binary windows. The commit form is coverage diff --base <commit>; the UUID document-base form below keeps its existing document-comparison behavior.

Agents and scripts read stored documents with read-only commands. Each returns one bounded page with --json; pass nextCursor back unchanged with --cursor to continue. In the local MCP connection (reflow mcp) the query_code_coverage tool runs the same reads (operation: documents, show, files, lines, gaps, diff, source, source_selection, source_pending or source_revisions) and never writes. The remote HTTP MCP connection does not offer it.

ReadWhat it answers
documents [--limit <n>]Which finalized documents exist, newest first, with their exact IDs.
show --document <id>Source provenance (repository, commit, tree, clean or dirty), producer and tool versions, tier, batch evidence and metrics.
files --document <id> [--query <substring>]Files ranked by missed lines.
gaps --document <id>Non-excluded files with missed line ranges [start, end], largest gaps first.
lines --document <id> --path <file> [--with-source [--from <line> --to <line>] [--revision <id>]]One file’s instrumented line ranges [start, end, hits] and its missed runs, optionally with its source.
diff --document <after> --base <before>What changed between two documents.
source --document <id> --path <file> [--from <line> --to <line>] [--revision <id>]The text of one covered or selected file from the source snapshot.

source-selection, source-pending and source-revisions --document <id> (MCP source_selection, source_pending, source_revisions) are read commands too; they read source metadata (see Which paths are snapshotted, and keeping source in sync). source-selection and source-pending are collection-level and take no document ID.

To open a document in the dashboard’s coverage explorer, print its link:

Terminal window
reflow dashboard links coverage --document <id> [--base <document-id>] [--path <file>] [--collection <id>]

It prints the hosted URL and, when one is running, the local dashboard URL, after the server confirms the collection, the document, the base document and that the file is instrumented in the document. --base takes an earlier document ID, never a commit. A base measured at another commit opens as a descriptive comparison (tree_changed) with no per-line gain. The MCP tool get_dashboard_links takes kind: "coverage" with document_id, base_id, path and collection_id (required on the remote HTTP MCP connection).

Every document read names an exact document ID. Copy it from documents; there is no mutable “latest” alias in these reads. A document removed by retention or delete answers not_found.

A metric that a producer does not measure is null and shown as “not measured”, never as 0%. Go measures statements but not regions or branches; LLVM (Swift) measures regions but not statements.

When a document is uploaded, the server can also store a source snapshot: the exact bytes of the document’s files at its commit, plus any selected paths. The uploader reads them from the Git objects of the document’s commit, never from the working tree. Source is shown as source at the document’s commit, as attested by the uploader. The server cannot prove that those bytes were the compiler’s inputs.

source reads one file from that snapshot (serverSource, origin server-snapshot). lines --with-source adds the same serverSource for the file it reports.

  • Windows: an explicit window (--from, --to) covers at most 2,000 lines. Without a range, a file of at most 20,000 lines is read whole and a longer one returns its first 2,000 lines. --from alone reads up to 2,000 lines from there, and --to alone reads from line 1. A range that starts past the end of the file is refused. partial is true exactly when the answer is not the whole file.
  • Verification: the document’s provenance must be complete and well formed (repository, commit, tree, object format, and a dirty flag that is true or false); otherwise nothing is shown and Git is never read. The client checks every answer against the document, commit, path and revision, including an answer that the file is unavailable: fields such as a document ID, canonical hash, kind or sizes that contradict the request refuse it. The served lines must be exactly the ones the request asks for, within the stored-file limits (1 MiB). A whole file is also verified by its SHA-256 and Git blob ID. A window is identity-checked only, not hash-verified. For lines --with-source, the source must also belong to the same document, canonical hash and provenance as the lines it is attached to, and the file must have at least as many lines as the coverage measured. A snapshot summary that does not match its document (commit, selected and covered counts) is refused by show, source and lines --with-source.
  • Revision: by default the read uses the document’s linked revision, which need not be the newest, and asks the server for exactly that revision. If it is relinked or removed between reading the document and reading the file, the read refuses with revision_changed: run it again. --revision picks another one; show names the linked revision.
  • Not stored: a path can be unavailable with a reason: excluded_path (credential files, .env files, VCS internals, vendored trees), secret_like, binary_or_non_utf8, too_large, quota, dirty_capture, not_in_document, not_snapshotted or commit_unavailable. Secret screening is best effort: treat source text as sensitive.

In the app, open Code coverage for a collection: /<team>/code-coverage?collection=<id>&doc=<document>[&path=<file>&view=source]. Source shows “Source at commit <commit>, as attested by the uploader”. A whole file is labelled “bytes verified against the attested sha256 and Git blob”; a window of a larger file is labelled “identity-checked, not hash-verified”. Selected (non-instrumented) paths open with &kind=selected and have no coverage marks. Privacy-withheld, quota, dirty or mismatched source is explained and never offered a local copy. A git show command is offered, in the source view only, when the coverage service stores no source snapshots or the document has none, and only for documents captured without uncommitted changes. An unavailable answer for a document with a snapshot (any reason, including not_snapshotted and commit_unavailable) is shown as withheld by the server, with its reason, and no local copy is offered.

When the document has no linked snapshot and the server answers not_snapshotted or commit_unavailable, or the server does not support source snapshots (unsupported), the reads fall back to your checkout. When a revision was read (the linked one or --revision), those reasons describe that snapshot’s entry for the path: they are reported, and the local text is withheld (withheld_by_server). reflow coverage source and the MCP source operation with --revision (revision) never read your checkout, even when the server does not support source snapshots: the local text is withheld (withheld_by_server) and Git is not read. lines --with-source keeps its released fallback there (see below). localSource adds the text only when:

  • the local Git repository has the document’s commit;
  • that commit’s tree equals the recorded tree;
  • the document was built from a clean tree.

If the lines or the document say the document was captured with uncommitted changes, Git is never read (built_dirty). Such a document never shows source: text the server sends for it anyway is refused (invalid_response).

The text comes from Git’s object at that commit (not from the working tree) and is labelled local-checkout with its blob ID. Otherwise localSource names why it is unavailable (for example built_dirty, commit_missing or tree_mismatch). The fallback is always the whole file. For lines --with-source, --from, --to and --revision apply to serverSource only, including on a server without source snapshots.

reflow coverage source (and the MCP source operation) also gates its local fallback, because it accepts any path:

  • a path excluded by the shared admission rules (credential and key files, .env files, VCS internals, vendored trees) is refused with excluded_path before any request is sent: no document or source read, and Git is not read;
  • the path must be a file the document measured, proven by its line coverage for that document; otherwise not_in_document, and Git is not read. Line coverage that is not well formed ([start, end, hits] integers, sorted and disjoint) is refused (invalid_response), also before Git;
  • these Git reads ignore replacement objects (refs/replace), so the commit, tree and blob are Git’s own objects;
  • the committed entry must be a regular file (mode 100644 or 100755): a symlink or other entry is excluded_path, as at upload;
  • the blob’s original bytes (at most 1 MiB, read without decoding) must hash to the blob Git names (blob_mismatch otherwise), pass the same admission as an upload (too_large, binary_or_non_utf8, secret_like) and be valid UTF-8; a file that fails returns no text. A U+FFFD character that is really in the file is kept.

lines --with-source keeps its released behaviour: its local copy is the whole measured file at the exact commit, without these extra admission checks.

Every other reason withholds local text too: Reflow never reads your checkout for a path that the snapshot deliberately left out. dirty_capture is reported locally as built_dirty. A failed source read never falls back. That includes forbidden, busy, revision_changed, a transport failure (transport_error), an answer for another document or commit (identity_mismatch), and an answer that breaks the checks above (invalid_response):

  • source exits non-zero;
  • lines --with-source reports reason: "error" in serverSource and still returns the lines.

When the server serves the file, lines --with-source adds no localSource; the local copy is only a fallback.

Size fields on an unavailable answer must be consistent: a file with N lines has at least N bytes, and zero lines means zero bytes. Anything else is refused (invalid_response) before any fallback.

For the coverage read commands, status and the MCP query_code_coverage tool, error output names only the error code and HTTP status, never server text. Only documented error codes are shown; any other value from the server is reported as http_<status>. On the read commands and the MCP tool, a failed connection, abort or timeout is transport_error with client wording only; another unexpected failure in the MCP tool is coverage_failed with a fixed client message.

Human output is a display, not the verified bytes: it is escaped, and credential-shaped values (tokens, passwords, bearer values, URL credentials and parameters) are redacted on a best-effort basis, on each original line before escaping. Redaction can miss credentials it does not recognize. --json and the MCP tool give the exact text. Human output of the read commands (results and errors) is escaped with one grammar: a backslash is shown as \\, every control character (everything below U+0020, including tab and line feed inside a value, DEL and U+0080 to U+009F) as \xHH, and invisible format characters (bidi overrides and isolates, zero-width marks, U+2028 and U+2029) as \u{HHHH}, in source text, paths and every server value. Source text is printed one file line per output line, so a tab or carriage return inside a line shows as \x09 or \x0d. Escaped values cannot move the cursor, retitle the terminal, fake an output line or visually reorder a file name, and a file that literally contains \x1b reads \\x1b, unlike an escaped ESC. --json and the MCP tool keep the exact values, including the verified text. Every other reflow coverage error message is escaped the same way; status and sync results are not (control characters other than tab and line feed are removed). The repository label in the provenance is not verified against your remote.

Which paths are snapshotted, and keeping source in sync

Section titled “Which paths are snapshotted, and keeping source in sync”

A snapshot always lists every file of the coverage document, excluded files included, each either stored or with a reason. It can also store paths outside the document:

  • The collection’s selection set: up to 256 repository paths stored for every document of the collection. source-selection prints the set and its version. source-selection set --source-path <path>... --if-version <n> replaces it (--clear sets the empty set). --if-version is the collectionVersion you last read; if the set changed since, the command is refused with conflict. Changing the set needs a token with run, or a team owner or admin in the app. It prints the new version and the number of paths.
  • A document’s explicit paths: the paths you pass with --source-path. They stay with the document: later syncs keep them, and --source-path adds to them. --replace-explicit (with --source-path) makes them exactly the paths given, and --clear-explicit drops them. The set and the explicit paths together hold at most 256 paths; a sync that would exceed that fails with selection_too_large before uploading anything.

Every path is read from the document’s own commit, never from your working tree. A path that does not exist at that commit is reported as not_in_document.

After ingest. A successful ingest (a new document or an existing one) then syncs that document’s source in the ingest’s own collection. That sync has a 60-second bound, counted like --max-seconds below but from the start of the sync (after the ingest); its elapsedMs counts from there too. The result’s sync field and the human output report what happened. A failed or incomplete sync never changes the ingest’s exit status, but it is reported as failed, not as success. --no-sync skips it; --source-path <path> (repeatable) adds explicit paths.

Later. A snapshot is a source revision. When the selection set changes, older documents keep their old revision until they are synced again. source-pending lists recent clean documents (the latest 10) whose linked revision does not use the current set, and source-revisions --document <id> lists a document’s revisions with the linked one marked. In the local MCP connection, query_code_coverage offers the same reads as operations source_selection, source_pending (limit up to 20) and source_revisions. They are metadata only and never write. Of the coverage operations, the review-reader MCP profile offers just these three, with no source text and no local Git.

reflow coverage source sync uploads new revisions from your checkout:

  • --document <id> [--source-path <path>]... [--replace-explicit | --clear-explicit] syncs one document; --pending syncs the pending documents.
  • --max-documents <1-3> and --max-seconds <1-60> bound one run. The time bound starts with the command and covers finding the collection and the checkout, Git work, compression and every request. At the bound (or on Ctrl-C) a running Git process is killed, and the command waits for that process itself to end (while it looks up the checkout, at most 2 more seconds) before it reports. Reflow does not signal a process that Git itself started; it closes its own end of Git’s output, so such a process may get an error when it next writes. A request to the Reflow API that is already in flight cannot be cancelled: the command stops waiting for it, and the request ends on its own timeout. The result’s elapsedMs is measured from the command’s start. A document it stops before any upload step is reported not_attempted (budget); Ctrl-C at that point reports not_attempted (cancelled). Once an upload step has begun, an interrupted document is unconfirmed, and the documents after it are not_attempted (stopped).
  • It needs the document’s commit in the local repository and never fetches, even in a partial clone, and ignores Git replacement objects. A commit (or a file of it) that Git reports missing, with no error, is skipped as commit_unavailable without using up --max-documents. An object Git cannot read (for example a corrupt one) and any other Git error fail the document (git_failed). Dirty documents get no source.
  • Every path stored without source is listed with its reason (secret_like, excluded_path, too_large, …), never its content.
  • It exits non-zero unless every document is created or existing (linked and current) or skipped (commit_unavailable or dirty). stale_selection (the set or the linked revision changed meanwhile), conflict, revision_limit, quota, integrity, failed, not_attempted and unconfirmed all fail the run.
  • It prints every revision ID it learned and never repeats an uncertain step blindly. After an unconfirmed outcome, run sync again: it reads the revision’s state from the server and continues from there, uploading a reserved revision with a fresh upload grant or linking a finished one.
  • Its human output (and the post-ingest sync lines) prints each outcome’s reason and revision IDs as the server sent them: control characters other than tab and line feed are removed and credential-shaped values are redacted on a best-effort basis, but the read commands’ documented-code filter and escaping grammar do not apply. A refusal’s message is escaped, but it may carry a server-supplied code or value (for example source-selection set’s error code). --json gives the exact values.
  • A document keeps at most 8 revisions. At the limit, sync refuses with revision_limit; only --evict-unlinked deletes the oldest revision that is not linked. The linked revision is never deleted.
  • conflict means another revision has the same selection with different contents. --supersede <revisionId> (with --document) stores a distinct revision that supersedes it.

A revision built against a set that has since changed is stored but not linked, and a newer link is never overwritten; the document stays pending. When the set changes in a way that leaves a document’s paths the same, the next sync links the existing revision for the new set without uploading again.

Source sync runs only from the CLI for now: after ingest, or as reflow coverage source sync. An MCP tool for agents, and background sync, are not available yet.

Start with coverage-guided testing to choose a useful uncovered behavior and decide what to keep. The Code coverage view’s history shows retained capture totals; a plateau is a prompt to inspect gaps, not proof that the suite cannot improve or that equal totals cover equal code.

Coverage is process-wide: nothing attributes lines to a single flow. To learn what one new test adds without running the existing suite again, run only the new test in its own window and compare cumulative documents built from saved raw files: see cumulative coverage and test gain. That page also covers how diff classifies a pair (likeForLike or descriptive) and the refusals to expect.

  • Zero added coverage is advisory, not proof that a test is redundant. A new assertion, an important boundary value, a regression case or a different oracle on the same path can matter without running new lines. Keep such a test and note why (“zero-delta, kept because …”).
  • Stop duplicating equivalent scenarios: the same class of inputs, the same path and the same assertions as an existing test. Coverage alone cannot show that; compare the tests’ stated purposes and oracles too.
  • Never skip, disable or delete a test from coverage data alone. Flag it for review with its purpose beside the delta.
  • Do not add exclusions to make numbers pass, and do not chase excluded files.
  • Prefer gaps that carry risk over raising a percentage.
CommandPurpose
reflow coverage bind --binary <path> --cover-dir <dir> [--collection <id>] [--repository <label>] [--exclude <glob>]...Tie a fresh cover directory to the binary and current commit.
reflow coverage bind --format llvm-cov --binary <mach-o> --cover-dir <dir> --path-strip <prefix>Same for an LLVM (Swift, C, C++, Objective-C) binary; --path-strip "" when recorded paths are already relative.
reflow coverage verify --cover-dir <dir> --processes <n> [--timeout-ms <ms>]Check that exactly n instrumented processes started with coverage active.
reflow coverage record-start --cover-dir <dir> --pid <pid>Record one instrumented start for ingest --processes launched (local, no network).
reflow coverage check --cover-dir <dir> [--processes <n>|launched]Offline report of a cover directory; with --processes, apply the exit-only checks.
reflow coverage ingest --cover-dir <dir> [--processes <n>|launched] [--batch <key>]... [--source-path <path>]... [--no-sync] [[--format go-textfmt] <file>]Derive the profile and upload one canonical document for the run window (--processes for exit-only), then sync its source.
reflow coverage ingest --format llvm-cov --cover-dir <dir> --processes <n> [--batch <key>]...Merge exactly n LLVM profiles and upload one exit-only document.
reflow coverage status [--document <id> | --latest [--clean]] [--top <n>] [--collection <id>]Show statement and line coverage and the most uncovered packages.
reflow coverage delete --document <id> [--collection <id>]Retire one finalized document.
reflow coverage documents [--limit <n>] [--cursor <c>]List finalized documents and their IDs.
reflow coverage show --document <id>One document with provenance, evidence and metrics.
reflow coverage files --document <id> [--query <substring>] [--limit <n>] [--cursor <c>]Files ranked by missed lines.
reflow coverage gaps --document <id> [--limit <n>] [--cursor <c>]Missed line ranges in non-excluded files.
reflow coverage lines --document <id> --path <file> [--with-source [--from <line> --to <line>] [--revision <id>]]One file’s line ranges and missed runs, optionally with its source.
reflow coverage source --document <id> --path <file> [--from <line> --to <line>] [--revision <id>]One file’s text from the source snapshot, or the allowed local fallback.
reflow coverage diff --document <after> --base <before> [--limit <n>] [--cursor <c>]Like-for-like gain or a descriptive change between two documents; across commits, with both commits in the checkout, it leads with the commit’s changed lines and their head hit state, then lists unchanged lines that newly ran or stopped running as secondary.
reflow coverage source-selectionThe collection’s source selection set and its version.
reflow coverage source-selection set (--source-path <path>... | --clear) --if-version <n>Replace the collection’s source selection set.
reflow coverage source-pending [--limit <n>]Recent clean documents whose source does not use the current set.
reflow coverage source-revisions --document <id>A document’s source revisions, the linked one marked.
reflow coverage source sync (--document <id> [--source-path <path>]... [--replace-explicit | --clear-explicit] [--supersede <revisionId>] | --pending) [--max-documents <n>] [--max-seconds <s>] [--evict-unlinked]Upload source revisions from your checkout.
reflow dashboard links coverage --document <id> [--base <document-id>] [--path <file>] [--collection <id>]Hosted and local coverage explorer URLs for one document.

Every command accepts --json. Refusals are named (for example unstamped_binary, vcs_mismatch, process_count, missing_counters, profile_mismatch or mode_mismatch) and exit non-zero.