Logo ProcessKit API Reference

ProcessKit Namespace

Type/Module Description

BatchItem<'T>

One completed command from a completion-ordered batch stream (`Exec.outputStream` / `Exec.outputStreamBytes`): the command's **input index** — its position in the `commands` sequence handed to the verb — paired with that command's own independent `Result`. The index is what keeps a result traceable to its source once items no longer arrive in input order, and it is always the position in the ORIGINAL sequence, never a counter of how many items the stream has yielded so far. The `Result` means exactly what one element of `Exec.outputAll` means: an `Error` is a genuine run failure (`ProcessError`: couldn't start, timed out, was cancelled, exhausted its `Retry` budget, …), while a non-zero exit stays `Ok` data whose `Code` you inspect.

BatchPolicy

How `Exec.outputAllWithPolicy` / `outputAllBytesWithPolicy` behave once any command in the batch produces an `Error` — a genuine run failure (`ProcessError`: couldn't start, timed out, was cancelled, exhausted its `Retry` budget, …), never a non-zero exit, which stays data under `Ok` exactly as it does for `outputString`/`outputBytes` themselves.

Capability

What this host can do on ONE axis of the containment contract — the honest three-valued answer the capability snapshot reports instead of a bare `bool`. A boolean `false` says nothing about *why*, or about what would make it true, so a caller can neither explain the refusal to an operator nor act on it. Every value here carries that missing half: a `Qualified` capability states the qualification it holds under, and an `Unsupported` one states the precondition that is missing. This mirrors the rest of the library, where an unavailable operation is a typed `ProcessError` naming what it needed — never a silent downgrade. The verb itself stays the authority at the moment it runs (see `ContainmentCapabilities`): this is a point-in-time report, not a promise about a later call.

CaptureStream

Which of a child's two captured streams a decoded line came from. Handed to `ICapturePolicy.OnCapture` so one policy can treat the two streams differently (redact only the stream a passphrase prompt is echoed on, say). It mirrors `OutputEvent`'s stdout/stderr split, but is a bare discriminator carrying no line: the capture seam should not force a consumer that only wants to shape text to depend on the streaming event type. Deliberately carries no stable machine identifier (no `Name`/`FromName`, and no entry in `spec/identifiers.json`), unlike `RlimitResource` or `IoPriorityClass`: nothing serializes it — it is an argument passed to a callback within one process, never a value ProcessKit writes to a report, a log field, or a cassette.

CliClient (Module)

Pipe-friendly entry point for `CliClient`.

CliClient (Type)

A reusable handle to one command-line program with shared defaults applied to every invocation. The defaults live in a *template* `Command`, configured with the full `Command` builder via `WithDefaults` (timeout, working directory, environment, encoding, ok-codes, retry, logger, …) — no separate, partial setter API to learn. Build `Command`s for argument lists (`client.Command [ "status" ]`) or run them straight through the client's runner (`client.RunAsync [ "status" ]`).

Command (Module)

Pipe-friendly functions over `Command`, mirroring the instance **builder** methods. The run verbs (`RunAsync`/`OutputStringAsync`/`ParseAsync`/…) are instance methods only — end a pipeline with method syntax (`(cmd |> Command.arg "x").RunAsync()`), or go through `Runner.*` with an explicit runner.

Command (Type)

An immutable description of a process to run. Build it fluently — each method returns a new `Command`. The value is the *cold* description of a run; the process is launched only when a verb (`Runner.run`, `Command.start`, …) is invoked. Use the instance methods (`cmd.Arg "x"`) or the `Command` module's pipe-friendly functions (`cmd |> Command.arg "x"`).

CommandVerbs

Default-runner convenience verbs on `Command`, callable from F# and C# as `command.StartAsync()` / `command.RunAsync()` etc. They use a shared `JobRunner`; for a custom or injected runner, go through `Runner.*` or call the runner directly. The `cancellationToken` is optional and defaults to `CancellationToken.None`.

ConsoleEncoding

The encoding a **legacy console program** actually writes its output in on this host — the one answer needed to read a pre-UTF-8 Windows tool without mojibake. ProcessKit decodes captured output as UTF-8 by default, and that default is deliberately not changed by anything here: it is right for every modern tool, on every platform, and guessing a code page for a child that emits UTF-8 would corrupt output that reads correctly today. But a Windows console program written before UTF-8 — `ping`, `netstat`, `chkdsk`, most of the built-in tooling, and any application still built against the ANSI/OEM CRT — writes its non-ASCII text in a code page instead, so a UTF-8 decode turns every accented or Cyrillic character into `U+FFFD`. This module resolves **which** code page that is, so the fix is one call rather than a research project (`GetOEMCP` vs `GetConsoleOutputCP`, `chcp`, `CodePagesEncodingProvider`). Use it through `Command.ConsoleEncoding()`, which applies the result to text stdin and both captured streams, or take the `Encoding` directly from `current ()` for a `Pipeline`/`CliClient`/single-stream case.

ContainmentCapabilities

A point-in-time, side-effect-free snapshot of what process containment can actually do on **this** host for a given `ProcessGroupOptions` — obtained from `ProcessGroup.Capabilities`, without creating a group, spawning a process, or touching any container. `ProcessGroup.Mechanism` already reports the primitive a group ended up with, but only once one exists; until now a caller who needed to pick a portable policy *before* spawning had to create a group and try each operation to find out what it would get. This is the same honesty contract answered up front: for every axis either a real availability on this platform and these options, or a typed `Capability` that names the missing precondition — never a bare `false`. **A snapshot, not a promise.** Every value is read from the platform facts in force at the moment of the call: a mounted cgroup v2 hierarchy, a helper binary present in a trusted directory, the ConPTY entry point exported by this Windows build. A host can gain or lose any of them afterwards — a package installed, a filesystem unmounted — so the answer here is the answer for *now*, and the verb itself remains the authority at the moment it runs, still returning its own typed `ProcessError`. It is deliberately not cached for that reason. What it never does is create a process, a group, or a container, and it neither reads nor reports any argv or environment value.

ContentLengthSession

A full-duplex Content-Length framed transport over a live process's stdin/stdout, suitable for LSP, DAP, BSP, and similar protocols. Construct it over a command started with `Command.KeepStdinOpen()`, enumerate `FramesAsync()`, and send raw payload bytes with `SendAsync`. The session owns the run's stdout consumption; other output verbs on the same handle are refused. `Command.StreamBuffer` bounds the unread frame backlog; leaving it unset preserves the default unbounded backlog. Only its two LOSSLESS full modes are honoured here: `Backpressure` paces the parser (and, through the pipe, the child) against your consumer, and `Error` faults the frame stream at the cap. The two DROP modes are refused at construction with a typed `ProcessError.Unsupported` — a framed transport carries protocol messages a peer correlates with its requests, so quietly deleting a queued frame is a corruption no consumer could detect, and this library refuses inapplicable configuration rather than downgrading it silently. With a bounded backlog, drain `FramesAsync()` concurrently with your sends rather than awaiting a send first: backpressure deliberately stops the parser (and the child) once the backlog is full, so a consumer that only starts reading after some other await can stall the child it is waiting on. The constructor itself never waits on the child — a `Command.Stdin(source)` feeder is awaited by the first `SendAsync`/`FinishInputAsync` instead, so the caller always gets the session back and can start draining frames.

DelegatingProcessRunner

A pass-through `IProcessRunner` base for decorators: it forwards all three verbs to `inner`, so a wrapper — logging, retry, metrics, fault injection, fixed-latency, recording — overrides only the verb(s) it changes and inherits the rest. Each verb is an overridable member; the interface dispatches to it.

DetachedProcess

A child launched **outside** every containment primitive by `Command.LaunchDetached` / `Exec.detach` — the library's single, deliberate opt-out from the kill-on-dispose guarantee, for the spawn-and-forget cases containment makes impossible: a self-updater that must outlive the process it replaces, a restart-myself relaunch, a daemon/agent handed off to the OS. **This is a diagnostic snapshot, not a handle.** It owns nothing, holds no OS handle, and has no `Dispose`: the child is no longer ProcessKit's to manage, so there is deliberately nothing here to wait on, stream, signal, or kill. Everything a contained run gives you — `RunningProcess`, the `Outcome`, the Job Object / cgroup / process-group teardown — is what you traded away by calling the detached verb; go through `StartAsync`/`RunAsync` (or a `ProcessGroup`) if you want any of it back. If you later need to reach this process anyway, do it through the OS with the identity below (`System.Diagnostics.Process.GetProcessById`, a pid file, a service manager), accepting the pid-reuse risk that ProcessKit's own containment exists to eliminate. **`Pid` alone is not an identity.** A pid is reused by the OS once the process is gone, so a bare pid read later can name an unrelated process. `StartTime` is the standard disambiguator: the pair (`Pid`, `StartTime`) identifies this specific incarnation, and re-reading a live process's start time is how you check that a pid still refers to *this* child before acting on it. The pair is captured at launch, while the pid was still pinned (Windows: our own open process handle; POSIX: the child is unreaped), so it can never describe an already-recycled pid. Sealed with an internal constructor so it can gain fields without breaking the frozen API. Like `MemberInfo`, it deliberately carries **no command line and no environment** — only the program name already present on `ProcessError` — since argv routinely carries secrets.

Exec

Top-level conveniences: run a program by name (without first building a `Command`), and run a whole batch of commands with bounded concurrency. The single-command verbs are zero-config one-liners (for cancellation, build a `Command` and use its verbs, or go through `Runner`); the batch verbs take an explicit `CancellationToken` so a long fan-out can be cancelled.

ExpectMatch

What a `PtySession.ExpectAsync` pattern matched, plus everything the child emitted before it — the output a prompt was preceded by (a banner, a menu, an error line), which is usually what the script wants to inspect or log. Both are consumed from the session's window, so the next `ExpectAsync` starts after this match and can never match the same prompt twice.

Finished

The result of finishing a streamed run: how it concluded, its captured stderr, and whether output exposed by the streaming session was truncated. Returned by `RunningProcess.FinishAsync`, after stdout has been consumed as a stream. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

ICapturePolicy

 A consumer-supplied, typed seam that shapes each decoded line **just before it enters the capture
 backlog** — the redaction-at-capture extension point, set with `Command.CapturePolicy`.

 For every framed line handed to a capture, `OnCapture` runs *before* the line is retained, so the
 text it returns — never the raw line — is what lands in the backlog, and therefore in
 `ProcessResult.Stdout`/`Stderr` and `Finished.Stderr`. That is what the observing
 `Command.OnStdoutLine`/`OnStderrLine` handlers cannot do: they run *alongside* capture and can see
 a line, not shape what is kept.

 **Scope and boundaries (read before relying on it for secret hygiene).** This seam shapes the
 in-memory capture backlog and nothing else. It deliberately does **not** reach the independent
 observation sinks, each of which keeps its existing contract and sees the line **unshaped**:

  - the per-line handlers `Command.OnStdoutLine`/`OnStderrLine` and the tees
    `Command.StdoutTee`/`StderrTee` — they exist to observe the real output; if you also write to a
    log or a file there, redact in that sink too;
  - the **streaming** verbs, which hand each line to a live consumer rather than retaining it:
    `RunningProcess.StdoutLinesAsync`, `OutputEventsAsync`, `WaitForLineAsync`, the byte-chunk
    streams, `PtySession`'s window/transcript, `ContentLengthSession`'s frames, and the stderr
    readiness probes. (`FinishAsync`'s retained *stderr* on those same sessions **is** backlog, and
    is shaped.)
  - a **raw byte** capture, which has no decoded line to shape: `OutputBytesAsync`'s stdout. A bytes
    run's line-pumped **stderr** is still shaped.

 A `Pipeline` captures nothing but raw bytes — its final stdout and every stage's stderr — so it
 cannot shape anything at all; a stage carrying a policy is therefore **rejected** by `Pipe`
 (`ArgumentException`) rather than run with the seam quietly inactive. Run such a command on its own.

 **A failing policy fails closed.** If `OnCapture` throws — or returns `null` — the offending line
 is retained **empty**, never the raw line it was meant to scrub, and the policy stays active for
 the lines that follow. A redactor that throws blanks its output rather than leaking it. Nothing
 else reports that failure, so prefer a policy that cannot throw.

 **It must be thread-safe.** One policy instance serves both of a run's streams, whose pumps are
 independent tasks, so `OnCapture` can be called for a stdout line and a stderr line at the same
 time (and for several concurrent runs sharing one policy). A pure transform of its argument — the
 shape this seam is for — needs nothing extra; a policy that keeps mutable state has to guard it.

 **What it does not decide.** *How much* is retained, and what is evicted on overflow, remain
 `OutputBufferPolicy`'s job; the two compose orthogonally — this decides each retained line's
 content, that decides how many survive. Retention bookkeeping (the retained-byte total, the
 `DropNewest` seal, `Truncated`/`TooLarge`) is computed from the text you return, while the
 cumulative line counters and the raw-pipe byte counters (`RunningProcess.StdoutBytesSeen`) are
 taken before this seam and are untouched by it.

IoMax

Directional disk I/O rate limits for one device or volume. Linux uses `Target` as a cgroup v2 `major:minor` device key. Windows uses it as the NT device name of one volume; Windows Job Objects enforce one aggregate read/write bandwidth and IOPS ceiling for that volume.

IoPriority

One Linux I/O-scheduling priority as configured through `Command.IoPriority`: an `IoPriorityClass` and, for the two levelled classes, the level within it. The pair `ioprio_set(2)` itself encodes into a single value. Built only through the three validating factories below — `Idle`, `BestEffort level`, `RealTime level` — which reject an out-of-range level at that boundary with `ArgumentOutOfRangeException` rather than clamping it into range or handing the kernel a value it would refuse later. **Lower levels mean higher priority**, which is the kernel's own convention and the opposite of how `Priority` reads: `BestEffort 0` is the most aggressive best-effort setting and `BestEffort 7` the politest.

IoPriorityClass

The **Linux I/O-scheduling class** a child's disk work runs in — the class half of an `IoPriority`, and the axis `ionice(1)`'s `-c` option selects. A separate dimension from the portable CPU-scheduling `Priority`: that one decides how much *processor* the child gets, this one how its *block-device* requests are ordered against everyone else's. Neither implies the other, and a background job usually wants both. **Linux-only, and honestly so.** `ioprio_set(2)` is a Linux system call with no POSIX or Win32 equivalent, so a spawn carrying an `IoPriority` fails with `ProcessError.Unsupported` on Windows, macOS, and the BSDs rather than running the child at the inherited priority as if the request had been honoured (see `Command.IoPriority`).

IProcessRunner

The seam through which commands run to completion: three low-level primitives an implementer or mock provides. The public verb vocabulary (`RunAsync`/`OutputStringAsync`/`ExitCodeAsync`/… on a `Command`, a runner, a `CliClient`, or a `Pipeline`) is layered on top of these by the `Runner` module and its extension methods — so the primitives are named distinctly (`Capture*`/`Spawn`) and never collide with the verbs. (That collision used to force the capture verbs to silently drop the retry policy when called with a `CancellationToken`; with distinct names each verb is a single method taking an optional `CancellationToken` that retries uniformly whether or not a token is passed.) The default runner spawns real processes into a kill-on-drop group; test doubles (`ProcessKit.Testing.ScriptedRunner`) script replies with no subprocess. This interface is both the dependency-injection point and the test seam, so any .NET consumer can implement or mock it with plain `Task`-returning methods.

JobRunner

The default `IProcessRunner`: spawns each command into a fresh kill-on-dispose `ProcessGroup` (owned by the returned `RunningProcess`), then captures or streams its output and reaps the whole tree on completion, failure, or cancellation.

JsonRpcMessage

One JSON-RPC message the peer sent that is **not** an answer to a request this session made: a notification (no `id`), or a request the peer wants answered (`id` present, so `IsRequest` is true — LSP servers use these for `workspace/configuration`, `window/showMessageRequest`, and similar call-backs). Answers to this session's own requests never appear here: they are routed straight to the `RequestAsync` call waiting on them. The message is kept as raw JSON text rather than a deserialized value, because only the caller knows what type a given `method` carries. Use `ParamsAs<'T>` for a typed read, or `ParamsJson`/`Payload` to handle it by hand.

JsonRpcSession

 A typed JSON-RPC 2.0 conversation with a child process that speaks `Content-Length`-framed JSON — a
 language server (LSP), a build server (BSP), or an MCP-style tool. It is the layer above
 `ContentLengthSession`: that type frames bytes, this one serializes values, allocates and correlates
 request ids, separates answers from notifications, and bounds a call end to end.

 ```fsharp
 task {
     let command = (Command.create "language-server").KeepStdinOpen()

     match! command.StartAsync() with
     | Error err -> eprintfn $"{err.Message}"
     | Ok proc ->
         use proc = proc
         let session = JsonRpcSession(proc)

         match! session.RequestAsync("initialize", parameters, Lsp.Default.InitializeParams, Lsp.Default.InitializeResult, TimeSpan.FromSeconds 30.0) with
         | Ok result -> printfn $"{result.ServerInfo}"
         | Error err -> eprintfn $"{err.Message}"
 }
 ```

 **Not a debug-adapter (DAP) client.** DAP borrows LSP's `Content-Length` framing but not its
 envelope: its messages look like `{"seq":1,"type":"request","command":"next","arguments":{}}` — no
 `jsonrpc`, no `method`, no `id` — so they are not JSON-RPC, and this session ends on the first one
 with `ProcessError.Parse` rather than guessing. Drive a debug adapter with `ContentLengthSession`
 directly and decode that envelope yourself.

 **This session owns the run's framed transport.** Constructing it creates the one
 `ContentLengthSession` over the handle and immediately claims its frames, so the handle's stdout
 belongs to this session: capturing/streaming verbs, a `PtySession`, or a second framed session on the
 same `RunningProcess` are refused afterwards, and there is deliberately no way to enumerate the raw
 frames alongside it — a second reader would tear the peer's messages between two consumers. Dispose
 the `RunningProcess` (or its owning `ProcessGroup`) to reap the tree; the session itself holds no OS
 resource of its own. Build the command with `Command.KeepStdinOpen()`, or every send reports a typed
 `ProcessError.Unsupported`.

 **Concurrency.** Requests may be issued concurrently: each gets its own `id`, and the router matches
 every answer against the request that is waiting for that exact `id`. Sends are serialized, so two
 concurrent requests can never interleave inside one frame — and a call whose deadline or token ends
 it while it is still queued behind another send fails alone, having written nothing.

 **Failures are typed, never raw exceptions and never a silent hang** (see `ProcessError`):

 - the peer answered with an `error` object — `ProcessError.JsonRpc`, carrying its `code`, `message`,
   and the raw JSON of `data`;
 - the request timed out — `ProcessError.Timeout` (per-request overloads only; the budget covers the
   whole call, writing the frame included, so a peer that stopped reading its own stdin cannot hang
   one. Without a timeout a request waits until the peer answers, the peer's output ends, or the
   caller's token fires);
 - the caller's `CancellationToken` fired — `ProcessError.Cancelled`;
 - either of those two interrupted a send **that had begun writing its frame**: the frame may have
   reached the peer truncated, and no peer can resynchronize from that, so the failure also ends the
   conversation — pending requests fail with it and every later request/send reports it instead of
   writing into a stream the peer can no longer read. A torn *outgoing* frame does not corrupt what
   the peer says, so `MessagesAsync` keeps delivering incoming messages until the peer's output ends.
   An interruption that landed **before** the write began — a token that was already cancelled, a
   deadline that elapsed while the call was still queued behind another send, or one that elapsed
   while the first send was still waiting for a `Command.Stdin(source)` feeder to hand over the pipe —
   wrote nothing, so it fails only that one call and leaves the conversation usable;
 - the frame backlog underneath hit a `Command.StreamBuffer` cap configured with
   `StreamFullMode.Error` — `ProcessError.OutputTooLarge`, which ends the session exactly as a
   protocol failure does (see the backlog note below);
 - the peer's framed output ended (it exited, or closed stdout) while a request was pending —
   `ProcessError.Io`, and every later verb fails the same way instead of waiting forever;
 - the peer sent something that is not a JSON-RPC message — `ProcessError.Parse`, which ends the whole
   session: a peer that is not speaking the protocol cannot be understood message by message. Pending
   requests all fail with it, and `MessagesAsync` faults with `ProcessException` carrying it.

 An answer whose `id` matches no waiting request — typically a late reply to a request that already
 timed out — is discarded, since the caller has already been told the request failed.

 **Two backlogs, two knobs.** `messageBacklog` (the third constructor argument, 1024 by default)
 bounds the DECODED incoming messages waiting for `MessagesAsync`. Old notifications may be dropped
 and are counted in `DroppedMessages`; a peer request is never silently dropped — if it would be
 evicted, the conversation ends with `ProcessError.OutputTooLarge`. The decoded-message backlog does
 not count a total message or byte volume, so that error carries zero totals (the `ProcessError`
 convention for an unreported metric). `Command.StreamBuffer` bounds the
 raw frame backlog underneath it, through the `ContentLengthSession` this session owns, and only its
 two LOSSLESS full modes apply there: `Backpressure` paces the peer against the router, and `Error`
 ends the conversation with `ProcessError.OutputTooLarge` at the cap. The two DROP modes are refused
 when this session is constructed — the constructor raises `ProcessException` carrying
 `ProcessError.Unsupported`, because silently deleting a queued frame would delete a message the peer
 is correlating with a request, and no consumer could tell. Leaving `StreamBuffer` unset keeps the
 default unbounded frame backlog.

KillOnParentDeathScope

The *scope* of cleanup a platform actually guarantees when the parent process of a `Command.KillOnParentDeath` child dies **suddenly** (SIGKILL, a crash, `TerminateProcess`) — reported honestly, fixed per platform, and **independent of whether `KillOnParentDeath()` was called**. This is the same "report the real guarantee rather than paper over a downgrade" principle as `ProcessGroup.Mechanism`: a caller can reason about what actually happens on the current OS instead of assuming a uniform whole-tree kill everywhere.

LimitEvidence

Post-run evidence of whether the resource caps a `ProcessGroup` carried actually fired — one `LimitVerdict` per axis, read from the container the group itself owns. See `ProcessGroup.LimitEvidence()` for when it becomes available and what each axis means. Deliberately per-axis rather than one whole-group verdict: folding three honest three-valued verdicts into one would have to merge `NotTripped` and `Unknown` together, turning "we have no evidence" into "no". Sealed with an internal constructor — built only by the backend that reads it. Covers only `MemoryMax`, `MaxProcesses`, and `CpuQuota` (the `Cpu` axis is additionally guarded against `CpuTimeMax` — see `CappedAxes.GuardCpuVerdict`). `IoMax` and `CpuAffinity` have **no** corresponding axis at all: no containment mechanism here keeps a post-mortem "this whole-tree I/O or affinity cap engaged" counter, so there is nothing honest to report for them, ever — not even `Unknown`. `WindowsUiRestrictions` and `OomGroupKill` are policy toggles rather than caps that "fire", so they are likewise out of scope.

LimitVerdict

The post-run verdict for **one** resource-limit axis — did a cap this group carried actually **engage** while the tree ran? Read from `ProcessGroup.LimitEvidence()`; see that member for exactly when it becomes available. This answers a different question than `ProcessError.ResourceLimit`: that error is about **admission** — "could the cap you asked for even be applied?" This verdict is about what only the container itself can answer afterwards — "did a cap on this axis then actually fire?" A `Tripped` verdict is returned only on **authoritative kernel/OS evidence** recorded by the group's own container; exit codes and signals are never consulted, because a cap-driven kill and a self-inflicted crash can look identical from the outside, and inferring from them would manufacture exactly the false verdict this type exists to avoid.

LineTerminator

How the output pump decides where one captured or streamed line ends — set per stream on `Command` via `Command.LineTerminator` (both streams at once) or `Command.StdoutLineTerminator` / `Command.StderrLineTerminator`. The default is `Lf`, which reproduces ProcessKit's original line-splitting behaviour exactly. This is one shared definition of "a line" for the whole line-pumped path: what `RunningProcess.StdoutLinesAsync` / `OutputEventsAsync` yield, what a `WaitForLineAsync` predicate sees, what the per-line handlers (`Command.OnStdoutLine` / `OnStderrLine`) receive, and what `OutputStringAsync` joins. Choosing a mode moves all of them together — there is never a per-sink disagreement about what a line is. It is orthogonal to the raw byte path: `OutputBytesAsync` and the tees (`Command.StdoutTee` / `StderrTee`) stay byte-exact and are unaffected by the mode. A `\r\n` pair is always treated as a **single** terminator in every mode (it never emits a spurious empty line between the `\r` and the `\n`), so ordinary CRLF text reads identically across the modes; the modes differ only in whether a *lone* `\n` or a *lone* `\r` ends a line.

Mechanism

The OS primitive a `ProcessGroup` uses to contain a process tree. Reported honestly (never a silent downgrade) so callers can reason about the containment guarantee on the current platform.

MemberInfo

An enriched, point-in-time snapshot of one member of a `ProcessGroup` (see `ProcessGroup.MembersInfo`). `Pid` is always present — it is how the group enumerated the member; every other field is `option` and is `None` wherever the platform cannot honestly report it, never a fabricated value. The member's **command line and environment are deliberately absent on every platform** — argv routinely carries secrets and redaction/hashing is the consumer's policy, so this snapshot excludes them by construction, the same exclusion the logging / tracing / metrics paths enforce. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

MemberStats

A point-in-time resource snapshot of one live `ProcessGroup` member. `Pid` is the member identity from the group's native membership snapshot. CPU time and resident memory are optional because the operating system may not expose them for every process/platform; I/O counters are optional as a group because the per-process interface is not available everywhere. Missing values remain `None` rather than being represented by fabricated zeroes. A member that exits while it is being sampled is omitted from the returned list.

Outcome

How a process run concluded.

OutputBufferPolicy

Caps how many captured/streamed output lines are retained in memory. The pump always drains the OS pipe (the child never blocks on a full buffer); this only bounds the in-memory backlog. Line counters still count every line, so a count greater than the retained amount reveals that lines were dropped. Two independent ceilings — lines and bytes — either or both of which may be set.

OutputEvent

A single event in a merged stdout+stderr stream, tagged with its origin.

OutputLine

One line of captured output, with its terminating newline stripped, its UTC capture time, and a per-run sequence number shared by stdout and stderr.

OverflowMode

What to drop when a bounded output buffer is full.

Pipeline (Module)

Pipe-friendly functions over `Pipeline`, mirroring the instance methods.

Pipeline (Type)

An immutable left-to-right chain of commands wired stdout -> stdin, with **no shell** involved: each stage's standard output feeds the next stage's standard input directly. The whole chain runs inside one shared kill-on-dispose group, so cancelling, timing out, or disposing the run reaps every stage together. Build it by piping commands (`a.Pipe(b).Pipe(c)`), then run it to completion with the same run-and-capture verbs a single command exposes (`RunAsync`/`OutputStringAsync`/`OutputBytesAsync`/`ExitCodeAsync`/ `ProbeAsync`/`ParseAsync`/`TryParseAsync`). To stream the final stage's stdout as it arrives instead of buffering the whole chain, start a live session with `StartAsync` (→ `PipelineSession`), the pipeline analogue of `Command.StartAsync` → `RunningProcess`. The exit status follows shell **pipefail**: the rightmost stage that did not exit with an accepted code (its `Command.OkCodes`, `{0}` by default) determines the result, unless that stage opted out with `Command.UncheckedInPipe`. When no checked stage failed, the final stage's real outcome and diagnostics are returned; an unchecked voluntary exit remains successful because its actual code is included in the result's accepted codes, not because the outcome is rewritten. Per-stage I/O config that applies inside a pipeline: each stage's `OkCodes` (pipefail) and `UncheckedInPipe`, the last stage's `StdoutEncoding`, `StdoutTee`, and `OutputBuffer` **byte** cap (`MaxBytes` + `Overflow`, applied to the captured stdout — its `MaxLines` never applies to a raw byte capture), stage 0's `Stdin` source (feeding the whole chain), and the chain-level `Timeout` / `CancelOn`. Every stage's stderr is likewise drained under its OWN `OutputBuffer`'s **byte** cap (`MaxBytes` + `Overflow`; a stage without `MaxBytes` set keeps its stderr unbounded, as before), so a chatty stage can never exhaust memory regardless of its position in the chain. Per-stage *stdout/ stderr observation* hooks are still **not** applied — intermediate stages' `StdoutTee`, every stage's `StderrTee`, and `OnStdoutLine`/`OnStderrLine` — because the chain wires stdout into the next stage's stdin and captures only the final stage's output. Observe an individual command by running it on its own, not as a pipeline stage. A stage's `CapturePolicy` is bound by that same boundary — every capture a pipeline makes (the final stdout, and each stage's stderr) is a RAW BYTE capture with no line framing, which is also what leaves `OutputBuffer.MaxLines` inapplicable here — but it is **rejected** rather than left unapplied, because a redaction hook that quietly stops running hands back the secret it was installed to remove (see the rejected-config paragraph below). Per-stage config a pipeline cannot honour is **rejected when the stage is piped** (an `ArgumentException` from `Pipe`, naming the field and stage index), rather than silently dropped: a `Stdin` source on any stage *after the first* (its stdin is always rewired to the previous stage's stdout — only stage 0 may set a source), a per-stage `Timeout` on any stage (only the chain-level `Pipeline.Timeout` bounds a pipeline; `Command.Timeout` on a stage never fires), a per-stage `IdleTimeout` on any stage (a pipeline captures only the last stage's output and does not monitor per-stage output activity, so a stage's own idle deadline can never fire), a per-stage `Retry` on any stage (retry is a verb-layer mechanism, and stages spawn directly, bypassing it), and a per-stage `CancelOn` on any stage (a stage's own `Command.CancelOn` token is likewise a verb-layer mechanism the direct stage spawn bypasses; only the chain-level `Pipeline.CancelOn` cancels a pipeline), `KeepStdinOpen` on any stage (the pipeline exposes no per-stage `RunningProcess.TakeStdin` handle through which the kept-open pipe could be used), a per-stage `CapturePolicy` on any stage (every capture a pipeline makes is a raw byte capture, so the seam has no decoded line to shape — run such a command on its own to scrub what it captured), `StdoutToFile` on any stage (stdout is instead pipeline wiring or final captured output), and `Stdout(StdioMode.Null|Inherit)` on any stage (the pipeline must use a pipe rather than silently override that destination). `StderrToFile` remains supported: it directs that stage's diagnostics to the requested file while the chain continues to carry stdout. Set the deadline on the pipeline, cancel the whole chain with `Pipeline.CancelOn`, feed stage 0, or run the command on its own. `MergeStderr` (a shell `2>&1`) is allowed only on the **last** stage — its stdout is the pipeline's captured output, so merging captures the final stage's combined stdout+stderr. On any earlier stage it is rejected (`ArgumentException`) the moment the stage stops being last (another stage is appended after it): a pipeline wires each stage's stdout into the next stage's stdin, so an OS-level merge on an intermediate stage would inject its stderr into the downstream stage's input data. Observability is whole-pipeline, not per-stage: running the chain emits one `Log.spawn`/`Log.exit` pair (plus `Log.timeout` on a timeout) and one `Diag.runStarted`/`runCompleted`/`runEnded` triple, all sharing a single run id — never one set per stage. Stage 0's `Logger` becomes the pipeline's logger (a per-stage `Logger` on any *other* stage has no effect — set it on stage 0, or observe an individual command by running it on its own); the `program` tag/label is a composite of every stage's name, joined `"a | b | c"` (built only from `Command.Program`, never argv/env, so the argv/env-never-logged invariant holds for a multi-stage run too). Stage 0 likewise owns `StopSignal`, because graceful shutdown broadcasts one soft signal to the whole chain — and, for the same reason, `CancelGrace`/`CancelSignal`: a cancelled chain (`Pipeline.CancelOn` or the verb's own token) is torn down through ONE soft signal and ONE grace window over the shared group, so setting either on stage 0 makes the whole chain's cancellation graceful, and setting it on a later stage is rejected rather than ignored. The chain-level `Pipeline.Timeout` is untouched by that knob and keeps its immediate hard kill. `StreamBuffer` depends on how the pipeline is run. For buffered verbs (`RunAsync`, `OutputStringAsync`, `OutputBytesAsync`, `ExitCodeAsync`, and similar verbs used without `StartAsync`), it is inapplicable because there is no streaming consumer to receive the policy. `StartAsync`, however, returns a live `PipelineSession`: the last stage's `StreamBuffer` policy is applied to the session's stdout channel, including its bounded-buffer/backpressure behavior. `KeepStdinOpen` is rejected with `ArgumentException`, not accepted as a silent no-op. Although `PipelineSession` is a live handle, it does not expose interactive stdin: the pipeline wires each stage after the first from the previous stage's stdout itself, and exposes no user-reachable `RunningProcess.TakeStdin` handle.

PipelineExtensions

`Command.Pipe` builds a two-stage `Pipeline`; further `Pipeline.Pipe` calls extend it.

PipelineSession

A live streaming session over a whole pipeline — the multi-stage analogue of `RunningProcess`, returned by `Pipeline.StartAsync`. It gives a pipeline the streaming layer a single command has long had: stream the **final** stage's stdout line by line as it arrives (`StdoutLinesAsync` / `StdoutJsonLinesAsync` / `OutputEventsAsync`), wait on a readiness line (`WaitForLineAsync`), wait for the whole chain to finish with the SAME pipefail classification the buffering verbs use (`FinishAsync`), or stop / reap the entire chain (`StopAsync` / `Kill` / dispose). Disposing it reaps every stage's tree (kill-on-drop), just like disposing a `RunningProcess`. **Single consumption** (the `RunningProcess` rule, [[K-031]]): the final stage's stdout is pumped exactly once, so `StdoutLinesAsync` and `OutputEventsAsync` are mutually exclusive — a second, different consumer throws `"already consumed by another verb"`. `FinishAsync` rejoins the SAME stdout-streaming session `StdoutLinesAsync` started (so it is the natural "wait for the rest" after streaming lines); after `OutputEventsAsync`, use `StopAsync` (or dispose) to reap. These hold because the session delegates every streaming/consuming verb to one underlying `RunningProcess`. **Whole-chain semantics.** The stream is the final stage's stdout, but `FinishAsync`/`StopAsync` reap and classify the ENTIRE chain: the returned `Finished.Outcome` is the pipefail representative (the rightmost checked stage that did not exit with an accepted code, or a `TimedOut`/`Cancelled` for the whole chain); when no checked stage failed, it is the final stage's real outcome, including an accepted unchecked non-zero exit. `Finished.Stderr` is that representative stage's stderr — identical to what `Pipeline.RunAsync` would report. Stopping or disposing tears down EVERY stage (including a partially started chain), never just the last. A genuine read failure in an upstream inter-stage relay is returned by `FinishAsync` as `ProcessError.Io`, even if the downstream stage observed the resulting EOF and exited successfully; a downstream broken pipe remains routine. That relay failure also tears the whole chain down the moment it is seen — it never waits out a still-running upstream stage that has simply stopped writing — so the streamed final-stage output ends wherever that kill lands.

PlatformHelper

One external binary this platform's spawn paths must load, and whether this host actually holds it. ProcessKit shells out to a helper only where the OS offers no in-process equivalent — arming `PR_SET_PDEATHSIG` inside the child, establishing a controlling terminal, running a `.cmd` shim. A missing helper is never worked around silently: the affected verb fails with a typed `ProcessError` naming the helper. This entry is that same fact, available *before* the spawn. The POSIX security helpers are resolved only from a fixed list of trusted system directories and never from `PATH`, so a helper present on `PATH` alone is reported missing here — deliberately, because the spawn refuses it too (a `PATH`-hijackable binary would run with the caller's full privileges before any drop). See the hardening guide.

Priority

 A portable CPU-scheduling priority for a child process (see `Command.Priority`), mapped onto the
 native primitive at spawn time: a **Windows** process priority class OR'd into the `CreateProcess`
 creation flags (the same seam as `Command.CreateNoWindow`), or a **Unix** `nice` value applied via
 `setpriority` to the spawned process-group leader.

 Every variant is supported on **both** platform families — `setpriority` is plain POSIX (Linux,
 macOS, the BSDs alike) and every Windows edition has all five priority classes — so
 `Command.Priority` never yields `ProcessError.Unsupported`.

 **How far the priority reaches into the spawned tree depends on the platform and the level.** It
 always takes on the immediate child on both platforms; whether the child's own descendants
 (grandchildren) inherit it differs:

 - **Unix — whole tree, every level.** A `nice` value is inherited across `fork`, so every
   descendant the leader spawns runs at the requested priority. One honest divergence: the `nice`
   is applied to the group leader *immediately after* `posix_spawn` returns (there is no
   `posix_spawn` attribute for it), so a descendant the leader forks in the sub-millisecond window
   before that call lands keeps the inherited default — the same spawn→apply window the cgroup
   mechanism already documents.

 - **Windows — whole tree only for the lowered classes.** The priority class is set atomically at
   process creation (no spawn→apply window), but Windows only *inherits* a class to grandchildren
   when it is lowered. Per `CreateProcess`, a child spawned with no priority-class flag defaults to
   `NORMAL_PRIORITY_CLASS` *unless* its creator is `IDLE_PRIORITY_CLASS` or
   `BELOW_NORMAL_PRIORITY_CLASS`, in which case it inherits that class. So `Idle`/`BelowNormal`
   (and `Normal`) reach the whole tree, but for `AboveNormal`/`High` the grandchildren a child
   later spawns run at `Normal`, not the requested elevated class. The elevation is still honored
   on the immediate child for all five levels — only its inheritance by grandchildren is the
   platform limit, and it is never a silent downgrade of the child you launched.

 Only ordinary (non-real-time) CPU priorities are exposed here: `Priority` never raises a real-time
 scheduling class. And it is the CPU axis alone — `Priority` itself never touches **I/O** scheduling.
 How a child's block-device requests are ordered against everyone else's is the separate
 `Command.IoPriority` axis (Linux-only; see `IoPriority`). Neither axis implies the other, and
 background work usually wants both.

ProcessError (Module)

ProcessError (Type)

Structured failure type for ProcessKit operations. Named `ProcessError` (rather than just `Error`) to avoid colliding with the `Result.Error` constructor in F#. Honest-result verbs — `outputString`, `outputBytes`, `exitCode`, `probe` — return their value; only genuine failures surface as a `ProcessError` in the `Result` channel.

ProcessException

Carries a structured `ProcessError` when an error must surface through an exception. Raised by `Result.GetValueOrThrow()`, streaming infrastructure (JSON-line parsing, pump faults, and bounded streams in `StreamFullMode.Error` mode), and ProcessKit group initialization through DI. Streaming APIs may surface pipeline faults this way even when their run-result counterparts return `Result<_, ProcessError>`. `Message` is exactly `Error.Message`, so it inherits that render's guarantees: one line, with every caller-, child-, or peer-controlled fragment sanitized and bounded — safe to print into a log or a terminal even when a hostile child chose its own stderr. The full, unmodified payload is on `Error` (`Detail`, `Stdout`, `Stderr`, `Data`, `Original`), never truncated.

ProcessGroup

A kill-on-dispose container for a process *tree*. Every process started into the group — and everything those processes spawn — is reaped when the group is disposed (deterministic under `use`) or, failing that, when the GC finalizes it. The OS primitive is chosen at creation and reported honestly by `Mechanism` — a Windows Job Object (`KILL_ON_JOB_CLOSE`), a Linux cgroup v2 (when resource limits are requested), a FreeBSD `procctl(2)` process reaper (whole-tree, `setsid` escapees included), or a POSIX process group (`killpg` teardown). All of that lives behind an `IContainmentBackend`; this type only orchestrates once-only teardown, the stdin/stream wiring, and the runner/disposable seams.

ProcessGroupOptions

Options applied when creating a `ProcessGroup`: the graceful-shutdown window and whole-tree resource limits.

ProcessGroupStats

A snapshot of a process group's resource usage. Optional peak-process, CPU, memory, and I/O fields are `None` when the platform can't report them — the POSIX process-group mechanism (macOS and the Linux fallback) has no kernel accumulator, and neither does the FreeBSD process reaper, which contains a tree but accounts for nothing in it; the Linux cgroup v2 backend (the `limits` feature) supplies the controller metrics available to it. Sealed with an internal constructor so it can gain metrics without breaking the frozen API.

ProcessKitDiagnostics

The well-known names and identifiers of ProcessKit's `System.Diagnostics` / `Microsoft.Extensions.Logging` observability surface, so a consumer references them without a magic string or number — e.g. `builder.AddSource(ProcessKitDiagnostics.ActivitySourceName)` / `builder.AddMeter(ProcessKitDiagnostics.MeterName)`, or filter logs by `ProcessKitDiagnostics.Events.ProcessExited`. **Security:** neither a log message, a trace span tag, nor a metric tag ever carries argv or environment **values** — only the program *name* and non-secret facts (outcome, duration, exit code / signal, pid, run id).

ProcessLookup

Standalone, identity-safe process lookup and reuse-safe liveness for a pid the caller holds **outside** any `ProcessGroup` (T-385) — a pid saved to disk across runs, a launch registry, or an external probe watching a process this library never itself contained. `ProcessGroup.MembersInfo` answers the same questions for a group's own membership; this module is the companion for a bare pid nothing here has ever tracked, so it needs no group and creates nothing. Reuses exactly the same per-platform readers `ProcessGroup.MembersInfo` uses (`Native.Windows` / `Native.Posix`) — no second, parallel identity-reading mechanism for this entry point — and keeps every standing rule those readers already enforce: never reads a process's argv or environment, and every enriching `MemberInfo` field stays an honest `option`, `None` wherever the platform cannot report it, never fabricated.

ProcessResult

ProcessResult<'T>

The full outcome of a run: the exit code as data, captured stdout/stderr, and timing. A non-zero exit is **not** an error here — inspect `Code`/`IsSuccess`, or call `ProcessResult.ensureSuccess` to convert a failure into a `ProcessError`. `'T` is the captured-stdout type: `string` for the text verbs, `byte[]` for the bytes verbs.

ProcessRunnerExtensions

The full run-verb vocabulary on *any* `IProcessRunner`, layered over the three-method seam (`CaptureStringAsync`/`CaptureBytesAsync`/`SpawnAsync`). So a chosen or injected runner — a shared `ProcessGroup`, a `ScriptedRunner`, a `JobRunner` — gets `run`/`exitCode`/`probe`/`parse`/… uniformly (`group.RunAsync command`, `scripted.ProbeAsync command`), callable from F# and C#. The verb *logic* lives once in the `Runner` module; these are thin sugar over it. Retry contract: every capture verb here applies the command's `Retry` policy (it routes through the `Runner` module). The `cancellationToken` is optional and defaults to `CancellationToken.None`, so `runner.RunAsync command` and `runner.RunAsync(command, ct)` are the same method and retry identically. (Because the seam primitives are named `Capture*`/`Spawn`, the verb names never collide with them, so adding a token can't silently bypass retry.) For a raw, single, no-retry capture call the seam primitive directly: `runner.CaptureStringAsync(command, ct)`. Streaming verbs (`StartAsync`/`FirstLineAsync`) never retry.

ProcessStdin

A handle for writing to a running child's standard input interactively. Obtained from `RunningProcess.TakeStdin` when the command was built with `Command.Stdin` / `Command.KeepStdinOpen`. Call `FinishAsync` to close stdin (the child sees end-of-file). Each write accepts an optional `CancellationToken`: a child that stops reading fills the stdin pipe and blocks the write, so a token lets the caller bound how long it waits (a cancelled write throws `OperationCanceledException`, the .NET convention for a cancelled `Task`). As with any cancellable stream write, a cancelled write may already have delivered *some* of its bytes to the child, so the safe recovery from a timed-out interactive write is to abandon the session — not to retry the write, which would duplicate the delivered prefix.

PtyConfig

Initial terminal geometry and behaviour flags for an opt-in pseudo-terminal (PTY) run — see `Command.Pty`. A PTY gives the child a real controlling terminal (`isatty` true) on a single merged stdout+stderr stream, for tools that demand a tty (an interactive `ssh`/`sudo` prompt, a credential helper, a TUI, a progress bar that switches to "dumb" line-buffered output when it detects a pipe). The default (no PTY) is byte-identical to a plain pipe run. **Secret-safety (echo footgun).** A terminal echoes typed input back into its *output* by default (cooked-mode `ECHO`), so bytes written to the child's stdin through the PTY — including an interactively typed password — are echoed into the captured merged output. This is standard terminal behaviour, not a bug, but it means a credential can appear in captured output (or a recorded cassette). Set `Echo = false` to disable the terminal echo: on **POSIX** ProcessKit clears the pty slave's cooked-mode `ECHO` bit (`termios`) before the child adopts it, so a password typed to the child through the PTY is not echoed into the captured merged stream (proven by test). On **Windows** the echo of a ConPTY is governed by the child's own console mode (`ENABLE_ECHO_INPUT`/`ENABLE_LINE_INPUT` on `CONIN$`), which has no supported parent-side pre-spawn override; ProcessKit therefore does not force echo off there — a documented platform divergence, not a silent claim (an interactive prompt on Windows should suppress its own echo, as `ssh`/credential helpers do). As everywhere else in the library, argv and environment **values** — and any PTY credentials — are never logged or traced; the record/replay redaction hook still governs what a cassette persists.

PtyLineEnding

What `PtySession.SendLineAsync` appends to the text it sends — the "Enter key" of an interactive session. A real terminal sends a carriage return when Enter is pressed, which is why `Auto` picks it for a `Command.Pty` run; a plain pipe has no line discipline to translate it, so `Auto` picks a line feed there instead. Override it when a particular child disagrees.

PtySession

An expect-style conversation with a live child: wait for a pattern in its terminal output, send it input, repeat — the classic automation loop for an interactive program (`ssh`, a REPL, an installer, a credential prompt). Built over a started `RunningProcess`, and designed for one from a `Command.Pty` run, which is what makes an interactive child prompt at all. **Why not `WaitForLineAsync`.** A terminal prompt is not a line: `Password: `, `> `, `(y/N) ` carry no line terminator, so a line-framed wait cannot see them until a newline finally arrives — often never, because the child is waiting for the very input the prompt is asking for. A session therefore reads the child's merged terminal output as **raw text** and matches patterns against a sliding window of it, framing nothing. **One conversation, in order.** The verbs are meant to be called sequentially, the way the script they automate reads: expect, send, expect. Two `ExpectAsync` calls racing on one session are safe (the window is guarded, so exactly one of them consumes a given match) but arbitrary — which of the two sees the prompt is a coin toss, the same as concurrently driving any other single handle. **This session owns the output pipes.** Creating it claims the handle exactly like `OutputEventsAsync`/`StdoutLinesAsync` do, so a capturing or streaming verb on the same handle afterwards is refused ("already consumed by another verb"), and constructing a session over a handle another verb already claimed throws `InvalidOperationException`. Use `WaitForExitAsync` for the child's outcome, and dispose the `RunningProcess` (or its owning `ProcessGroup`) to reap the tree — do not layer a second consuming verb on top. **Line handlers do not fire here.** `Command.LineTerminator`, `OnStdoutLine`/`OnStderrLine` and the per-stream line counters describe a framed stream, and this session deliberately does not frame one; the byte-exact tees (`Command.StdoutTee`/`StderrTee`) are still fed, exactly as they are on the line paths. On a plain (non-PTY) run a session still works, but the child decides whether you ever see a prompt: without a terminal most programs switch their stdout to block buffering, so the prompt sits in the child's own buffer — that is the child's behaviour, and precisely what `Command.Pty` exists to change. **`StreamBuffer` is inapplicable here.** A session has no queued line/frame backlog: raw output feeds its sliding match window directly. `PtySessionOptions.WindowChars` and `TranscriptChars` are its explicit bounded-memory policy, so a `Command.StreamBuffer` setting has no effect. **Secret-safety.** Sent input is never logged, traced, or added to `Transcript` — but a terminal echoes input back into its OUTPUT by default, so with `PtyConfig.Echo = true` a sent password arrives in the child's output stream and therefore in the transcript. For a credential exchange use `Echo = false` (POSIX), or turn `CaptureTranscript` off, or both.

PtySessionOptions

Tuning for a `PtySession`: how much output a pattern may be matched against, whether the session keeps a transcript for diagnostics, and what `SendLineAsync` sends as the line ending. Both sizes are hard memory bounds in **characters**, so a long-lived session over a chatty child can never grow without limit; both bound by dropping the OLDEST text, since the tail is what the next pattern will match and what a failed session's diagnosis needs.

ReportJson

 AOT-safe `System.Text.Json` metadata for the opt-in JSONL report serializer: one self-describing JSON
 object per line for `Outcome`, `ProcessResult<string>`/`ProcessResult<byte[]>`,
 `ProcessGroupStats`, `RunProfile`, `MemberInfo`, and `LimitEvidence` — the shapes this port has today.
 Ports the **shape**, not the code, of ProcessKit-rs's `report-serde` feature; see
 `docs/jsonl-reports.md` for the full schema, the versioning promise, and C#/F# consumer examples
 reading a JSONL stream.

 **Opt-in.** Nothing on `ProcessResult`/`ProcessGroupStats`/`RunProfile`/`MemberInfo`/`LimitEvidence`
 themselves changed — this is a separate serializer you reach for explicitly, via
 `JsonSerializer.Serialize(value, ReportJson.OutcomeTypeInfo)` or the `ToReportJson()` extension methods
 on `ReportJsonExtensions`.

 **Four rules, matching the source feature:**
  1. **A tagged shape carries a `"kind"` identifier**, spelled as this schema's own stable, documented
     machine name — never a raw union-case ordinal or a BCL `.ToString()`. `Outcome` is `exited` /
     `signalled` / `timed_out` / `unobserved`; each report line's own envelope is `process_result` /
     `process_group_stats` / `run_profile` / `member_info` / `limit_evidence`.
  2. **`Serialize` only — deliberately no `Deserialize`.** These are values the library *reports*, never
     values a caller supplies back to it; every converter's `Read` throws `NotSupportedException`. Every
     `JsonTypeInfo<'T>` below is still safe to pass to a `JsonSerializer.Deserialize` call by a
     caller who ignores this and does so anyway — it simply throws instead of fabricating a value.
  3. **Reports *about* processes, never what a process produced.** No converter here ever reads captured
     stdout/stderr content, argv, or environment values — `ProcessResult.Stdout`/`Stderr`/`Combined` are
     never touched, whatever `'T` is.
  4. **Fields are additive; a field's spelling and unit are frozen.** Every one of these report types is
     `[<Sealed>]` with an internal constructor and grows fields across minor releases without
     breaking this schema's readers — a JSONL consumer must ignore keys it does not recognize, the same
     discipline any self-describing format needs. Time is always a number of fractional seconds
     (`duration_secs`, `total_cpu_time_secs`, `cpu_time_secs`, …); a measurement the platform cannot
     report is `null`, never a fabricated `0`.

ReportJsonExtensions

C#-friendly `ToReportJson()` overloads over `ReportJson`'s metadata — one compact JSON object per call, with no embedded newline, so appending `Environment.NewLine` (or `\n`) after it is a valid JSONL line. F# callers can use these too, or call `JsonSerializer.Serialize(value, ReportJson.OutcomeTypeInfo)` (etc.) directly.

ResourceLimitCapabilities

What this host can enforce, per `ResourceLimits` dimension. **These answer for the HOST, not for the mechanism the snapshot's options happen to select.** On Linux, asking for a whole-tree cap is itself what selects the cgroup v2 mechanism, so reporting "unsupported" for a limit-free options set would understate what the host can do — the honest question each member answers is "if this cap were added to the options, could this host enforce it?". `Creation` and `Mechanism` are the members that answer for the options as they stand.

ResourceLimits

Resource limits applied at group creation. Most are enforced on the group **as a whole** by its kernel container; `CpuTimeMax` is the deliberate POSIX exception and is applied per spawned process through `RLIMIT_CPU` before exec. Whole-tree enforcement needs a real container — a **Windows Job Object** or a **Linux cgroup v2**. On macOS and the Linux process-group fallback, requesting memory/process/quota/affinity limits fails fast with `ProcessError.ResourceLimit`; CPU-time alone remains available through the per-child rlimit. On Linux the cgroup v2 controllers can only be enabled when this process runs at the real cgroup-v2 hierarchy root (not under a systemd scope, nor in an ordinary container); when they cannot, group creation fails fast for the same reason.

RestartCause

Why the supervisor is restarting an incarnation — the `SupervisorRestartEvent.Cause` a live `OnRestart` handler can branch on to tell an ordinary exit/crash restart apart from one the liveness probe forced.

RestartPolicy

When the supervisor restarts an exited child. In every case `Supervisor.StopWhen` and `Supervisor.MaxRestarts` can end supervision first.

ResultExtensions

 C#-idiomatic consumption of the `Result<'T, ProcessError>` that every verb returns. F# matches
 `Ok`/`Error` directly; C# pattern-matches the result with no projection or helper call, using the
 result's own members:

 
 await cmd.OutputStringAsync() switch
 {
     { IsOk: true,  ResultValue: var run } => run.Stdout,
     { IsOk: false, ErrorValue: var err } => err.Message,
 }
 

 The match is exhaustive (the `IsOk` bool covers both arms — no discard needed); `ResultValue` is
 read only in the `IsOk: true` arm (the pattern short-circuits) and binds non-null, so no `!`. For
 non-`switch` styles these extensions give `Match` / `Switch`, `TryGetValue`, and `GetValueOrThrow`
 without touching the result's members. A `Result` is never `null`, and the Try out-parameters
 follow the standard .NET pattern.

Rlimit

One per-process rlimit as configured on a `Command`: the resource, and the soft and hard values to apply to it — the pair `setrlimit(2)` itself takes. Read back from `Command.Rlimits`; built by `Command.Rlimit`, which validates the pair at the builder boundary. The **soft** value is the limit actually in force (exceeding it raises this resource's signal or fails the operation); the **hard** value is the ceiling the child may raise its own soft value back up to. Both are in the resource's native unit (see `RlimitResource`), and `Soft` never exceeds `Hard`.

RlimitResource

A Unix **per-process** resource governed by `setrlimit(2)` and requested through `Command.Rlimit` — the per-child complement of the whole-tree `ResourceLimits` below. The two are different instruments and neither replaces the other. A `ResourceLimits` cap is enforced by the group's kernel container on every process in the tree AT ONCE (one memory budget shared by all of them); an rlimit is applied to the direct child before its program starts and is then INHERITED individually by each descendant, so ten descendants each get their own copy of the cap rather than a shared one. A descendant may lower its own limits further, and may raise its soft value back up as far as the hard value it inherited — an rlimit is a robustness bound, not a containment boundary (that is what the group is for). Every value is in the resource's own native unit, exactly as the syscall takes it: **bytes** for `Core`/`Data`/`FileSize`/`Stack`, **seconds** for `Cpu`, a **count** for `NoFile`. There is no "unlimited" value — this API exists to LOWER a limit the child inherited, and raising a hard limit needs privilege the kernel refuses to an ordinary caller (the refusal is honest: the child never runs). **Unix-only**, and honestly so: Windows has no `setrlimit` analogue, so a spawn carrying any rlimit fails there with `ProcessError.Unsupported` rather than running the child uncapped. On POSIX the limits are applied before the child's own program starts by the util-linux `prlimit` helper, loaded only from a trusted system directory; a host that holds it in none of them (macOS/BSD, a minimal image) fails with `ProcessError.ResourceLimit` — see `Command.Rlimit` for the full mechanism.

RotatingFileSink

A caller-owned write-only stream that rotates a file by size for use with `Command.StdoutTee` or `Command.StderrTee`. The active file is `path`; archives are `path.1` (newest) through `path.` (oldest). Writes are split at `maxBytes`, so no file created by the sink exceeds the configured size. Rotation and write failures propagate through the ordinary tee error path. Unlike `StdoutToFile`/`StderrToFile`, this sink is fed by ProcessKit's parent-side pump: it supports rotation, but stops receiving bytes when the parent exits. The caller owns and must dispose it.

Runner

 The run verbs, expressed over any `IProcessRunner`. One verb, one meaning:

 - `run` — require a zero/accepted exit; return stdout, trailing whitespace trimmed. Output a
   bounded `OutputBuffer` policy truncated is refused (`OutputTooLarge`), never passed off as whole.
 - `outputString` / `outputBytes` — the full `ProcessResult`; a non-zero exit is data, and so is a
   truncated capture (read `ProcessResult.Truncated`).
 - `exitCode` — the exit code; a signal kill or timeout errors instead of inventing one.
 - `probe` — read the exit code as a yes/no: 0 -> true, 1 -> false, anything else errors.

RunningProcess

A live handle to a started process: stream its output, feed its stdin, wait for it, or collect it to completion. Disposing it reaps the whole process tree (kill-on-drop).

RunProfile

Resource summary of one finished run — produced by `RunningProcess.ProfileAsync`. CPU and memory come from the started child process (the same source as `RunningProcess.CpuTime` / `PeakMemoryBytes`). I/O comes from the run's private containment tree and stays `None` for a shared group, where an aggregate would include sibling runs. Sealed with an internal constructor.

ShutdownReport

The observed facts of one graceful `ProcessGroup` teardown, returned by `ProcessGroup.ShutdownReportAsync`. Where the fire-and-forget `ShutdownAsync` reports only success or a thrown exception, this carries what the teardown **actually observed**: which soft signal was attempted and whether it landed, how many members were alive before and after, whether the tree drained within the grace or had to be hard-killed, and how long it really took. A consumer that owns its own end-of-run race (a deadline that is not a fixed timeout, but a timeout x Ctrl-C x control-socket race) can report the *observed* tier instead of re-deriving it from `ShutdownAsync`'s bare success. # Point-in-time member counts `MembersBefore`/`MembersAfter` count the same member set `ProcessGroup.Members` reports — the whole tree on the Windows Job Object and Linux cgroup v2 mechanisms and on the FreeBSD process reaper (which reads the live tree from `PROC_REAP_GETPIDS`, zombies excluded), the tracked group **leaders** on the POSIX process-group fallback (macOS / the other BSDs / Linux without cgroup v2). Each is `None` only if that membership read failed (an unreadable `cgroup.procs`, a failed Job Object query), never a fabricated `0`. # Unconditional teardown — same guarantee as `ShutdownAsync` This still tears the group down exactly like `ShutdownAsync`: the soft signal, then the grace, then an unconditional hard kill of any survivor, then release. There is no "spare the survivors, keep the group usable" mode here — see `ProcessGroup.ShutdownReportAsync`'s own doc comment for why this port does not offer one. `Escalated` reports whether that hard kill actually fired; it is never a choice the caller can suppress. # Sealed, accessor-only A read-only snapshot the library produces: sealed with an internal constructor so it can gain fields across minor releases without a breaking change, and each fact is exposed through a property (documenting its own platform caveats) rather than a public field.

Signal

A signal delivered to one run via `RunningProcess.Signal` or broadcast through `ProcessGroup.Signal`. The curated variants map to the POSIX signal of the same name on Unix. On **Windows** `Kill` maps to the Job Object terminate (the same hard kill as `ProcessGroup.KillAll`), and `Int`/`Term` are a best-effort soft stop: a console CTRL+BREAK to each child started with `Command.WindowsCtrlSignals()` and a `WM_CLOSE` posted to the top-level windows of every member that has one (a GUI child). They yield `ProcessError.Unsupported` only when the group has neither a CTRL-capable child nor a windowed member — nothing to soft-signal — never a silent downgrade. Every other variant yields `ProcessError.Unsupported` on Windows. `Other` is an escape hatch carrying a raw signal number on Unix (e.g. `SIGWINCH`); it is always unsupported on Windows. `SIGSTOP`/`SIGCONT` are deliberately absent from the curated set — pause and resume the whole tree with `ProcessGroup.Suspend` / `ProcessGroup.Resume`, which are portable (Windows included); `Signal.Other` with the raw `SIGSTOP` number remains available when the raw signal is specifically wanted on Unix.

SignalCapabilities

What `ProcessGroup.Signal` / `RunningProcess.Signal` can actually deliver through the mechanism the snapshot's options select. The three rows are the signal vocabulary's platform divergence: the unconditional hard kill, the soft stop, and everything else.

SoftSignalDelivery

The fate of a graceful teardown's best-effort **soft-signal** tier — what actually happened to the polite "please exit" request `ProcessGroup.ShutdownReportAsync` issues before the grace window, as opposed to what it *tried* to do. The soft signal is the group's configured `Options.StopSignal` (`Signal.Term` by default) on the Unix mechanisms, and a best-effort `WM_CLOSE` on the Windows soft tier. It is deliberately **not** the hard kill: escalation to `SIGKILL` / the atomic Job terminate is reported separately, by `ShutdownReport.Escalated`.

SoftStopScope

How far a **soft stop** — a `Signal.Term`/`Signal.Int`-class request that asks a tree to exit cleanly rather than hard-killing it — reaches on *this* group, right now: the honest answer to "if I call `group.Signal(Signal.Term)` this instant, which of its members have a live target for that request?", read with `ProcessGroup.SoftStopScope()` *before* the signal is attempted. This is a capability report, not a delivery guarantee — see below. This is a **capability report, not an action**: querying it delivers no signal, posts no `WM_CLOSE`, spawns nothing, and does not mutate the group — asking never changes the answer a later `Signal` call gets. A caller (a CLI wrapping this library, an orchestrator deciding whether a soft stop is worth attempting at all) can state the real reach to its own operator instead of firing `Signal(Term)`, parsing a `ProcessError.Unsupported` back, and guessing at the scope from a hard-coded platform assumption. # It reports the *soft* tier only This describes only the graceful `Signal.Int`/`Signal.Term` soft-stop tier. The unconditional **hard** kill — `Signal.Kill`, `ProcessGroup.KillAll`, and disposing the group — always tears the whole tree down on every platform regardless of this value; that guarantee is unchanged and never `Unsupported`. # Runtime, per-group — not a fixed platform constant Unlike `KillOnParentDeathScope` (fixed per platform at build time), this is read from the group's **live membership** on every call, so the *same* build reports different scopes for different groups — most visibly on Windows, where a group with a live console-CTRL leader (a child started with `Command.WindowsCtrlSignals()`) or a live windowed member reports `OptInMembers`, while a group with neither reports `Unsupported`. "Live" is checked, not assumed: a CTRL-capable leader whose handle is still open (and so still registered) but has already exited does NOT count, because `GenerateConsoleCtrlEvent` on its now-torn-down console process group would then fail. The read is side-effect-free — it posts no `WM_CLOSE`, sends no CTRL+BREAK, and mutates nothing — but it is a capability report, not a delivery guarantee: `OptInMembers` says a live target for the soft stop exists, not that a later `Signal(Int/Term)` is certain to reach it (`GenerateConsoleCtrlEvent` can still fail for reasons this read does not probe, such as the caller having no console to share). | Mechanism | Scope | Why | |---|---|---| | Linux cgroup v2 | `WholeTree` | `Signal(Int/Term)` writes to every process in the cgroup. | | FreeBSD process reaper | `WholeTree` | `PROC_REAP_KILL` reaches every process in every subtree the group owns, with no escapee at all — not even a child that `setsid`s away: this is the strongest form of the promise on any unix. | | POSIX process group (macOS / the other BSDs / Linux without cgroup v2) | `WholeTree` | `killpg` reaches every tracked group leader and its descendants — a child that `setsid`s away escapes, the same documented weakness the kill-on-drop guarantee already has, not new to the soft stop. | | Windows Job Object | `OptInMembers` or `Unsupported` | A Job Object has no POSIX signal; a soft stop reaches only members it can *trigger* — a console-CTRL leader (opted in via `Command.WindowsCtrlSignals()`) or any live windowed member (`WM_CLOSE`). `OptInMembers` when at least one such member is live, else `Unsupported`. |

Stdin

A source for a child process's standard input, attached with `Command.Stdin`. When set, the child's stdin is a pipe fed from this source; the pipe is closed (EOF) once the source is exhausted — unless `Command.KeepStdinOpen` is also set, in which case the pipe is left open after the source is drained so the caller can keep writing to it interactively via `RunningProcess.TakeStdin` (which becomes available once the source feed has finished, so the source and the interactive writer never write the pipe at the same time).

StdioMode

How a child's stdout or stderr stream is connected. Set per-stream on `Command` via `Stdout`/`Stderr`; the default is `Piped`.

StopReason

Why supervision ended.

StreamBufferPolicy

An opt-in bounded/backpressure policy for the streaming verbs (`StdoutLinesAsync` / `OutputEventsAsync` / `WaitForLineAsync`) and `ContentLengthSession.FramesAsync` (which honours the lossless full modes and refuses the two lossy ones), set via `Command.StreamBuffer`. It is inapplicable to `PtySession`, whose raw match window and transcript have their own character bounds. Unlike `OutputBufferPolicy` — which bounds an in-memory *buffer* a one-shot verb assembles — this bounds the *channel* between the background pump and your live consumer. Leaving it unset keeps today's unbounded channel: an unbounded, uncapped in-flight backlog, exactly as before this policy existed.

StreamFullMode

How a bounded *streaming* channel behaves once its capacity is reached — the streaming analogue of `OverflowMode`, but with a genuine backpressure option that only makes sense against a live consumer (a buffered one-shot verb has no such consumer to pace, which is why `OutputBufferPolicy` has no equivalent case).

SupervisionEvent

One typed transition of a live supervision, delivered by `SupervisionSession.EventsAsync` — the stream counterpart of the `OnRestart`/`OnStormPause` callbacks and the `Status` snapshot, which it adds to rather than replaces. Read `Kind` first: it says which transition this is, and therefore which of the payload properties below carry a value (each is `None` for every other kind). `Name` is the same fact as a stable lowercase machine identifier — `incarnation_started`, `restart_scheduled`, … — suitable as a log field or metric label, in the same `snake_case` style as the `"kind"` identifiers `ReportJson` writes. **Non-secret by construction.** An event carries lifecycle facts only: counters, a pid, an `Outcome`, durations, the program name, and coarse failure/stop classifications. It never carries argv, environment values, captured stdout/stderr, or a `ProcessError`'s message — the same taxonomy `MemberInfo` and the library's logging already follow, so a consumer can forward the whole stream to a log or metrics sink without auditing it for secrets. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

SupervisionEventKind

Which supervision transition a `SupervisionEvent` reports — the discriminator to branch on when consuming `SupervisionSession.EventsAsync`. A plain .NET enum on purpose: this taxonomy is meant to *grow*, and a new kind must not break a consumer that was written against an earlier version. Existing kinds are never renumbered, renamed, or repurposed; new ones are appended with the next free value. Both languages already force the habit that makes that safe — F# requires a wildcard branch when matching an enum, and a C# `switch` expression needs a discard arm — so treat an unrecognized kind as "something newer happened" (the event's `Name` still identifies it) rather than as an error.

SupervisionOutcome

What a finished supervision reports — the last run plus the keeper's telemetry. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

SupervisionSession

A live handle to a running supervision, returned by `Supervisor.StartAsync`. Unlike `RunAsync` — which only reports its `SupervisionOutcome` at the very end — a session lets a caller watch supervision *while it runs* (`Status`), ask it to stop *gracefully* (`StopAsync`), and await its eventual outcome (`Completion`). This is the primitive for building daemons / process managers on top of the runner layer without pulling in `Microsoft.Extensions.Hosting`. Thread-safe: `Status` is read under the same lock the supervision loop uses to publish each state change, so a concurrent read never races an update nor throws; `StopAsync` is idempotent and race-safe against the loop (and against a repeat call). Sealed with an internal constructor — build one via `Supervisor.StartAsync`.

SupervisionStatus

A consistent, point-in-time snapshot of a `SupervisionSession`'s live state — read atomically, so every field agrees with the others (no torn read across a concurrent update from the supervision loop). Only non-secret facts are exposed (activity, counts, the current child's pid/start time); argv and environment values never appear here, matching `ProcessKitDiagnostics`'s taxonomy. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

Supervisor (Module)

Pipe-friendly entry points for `Supervisor`.

Supervisor (Type)

Keeps a `Command` alive: runs it, classifies every exit against the `RestartPolicy` and the `StopWhen` predicate, and restarts it after an exponential-backoff delay until supervision ends. `Command.Retry` answers "run this once, replaying on failure"; a supervisor answers the different question **"keep this alive"** — a minimal `runit`/`systemd`-style keeper on top of the runner layer. The two are distinct layers: a supervised command's own `Retry` is **not** applied per incarnation (supervision runs the bare runner), so use the supervisor's own restart policy and backoff instead. Runs go through an `IProcessRunner` (the default `JobRunner`); override with `WithRunner` to share a `ProcessGroup` or inject a test double. Defaults: `OnCrash`, unlimited restarts, backoff `200ms × 2.0` capped at 30 s, jitter on, failure-storm guard off (enable with `StormPause`; failure half-life 30 s, threshold 5.0). **Observability while supervision runs.** `RunAsync` only reports its `SupervisionOutcome` at the very end, which is unusable for a long-lived (potentially never-ending) supervised service — so two callback seams, `OnRestart` and `OnStormPause`, report restarts and storm pauses *live*, as they happen (e.g. for a health check or crash-loop alerting). Both callbacks are invoked synchronously, from the supervision loop itself (the same async context driving `RunAsync`), right before the corresponding delay is slept out — so a slow or blocking handler delays every restart/pause; keep handlers quick and non-blocking. Neither callback changes `SupervisionOutcome`'s semantics — `Restarts`/`StormPauses`/`Stopped` are unaffected and remain the authoritative final tally; the callbacks are an additive, best-effort live view. **Interactive supervision.** For a poll-and-control view — a live `Status` snapshot (activity, restart count, storm-pause flag, the current child's pid/start time), a graceful `StopAsync`, and a `Completion` task — use `StartAsync`, which returns a live `SupervisionSession` handle. `RunAsync` is a thin wrapper over `StartAsync` + awaiting `Completion`; the `Status` snapshot *adds* to the `OnRestart`/`OnStormPause` callbacks without replacing them. **Event stream.** Where the callbacks push two specific transitions into your code, `Events` opts in to the whole lifecycle as a *pull*-based `IAsyncEnumerable` (`SupervisionSession.EventsAsync`): starts, outcomes, launch-failure classes, restarts, storm pauses, health-check verdicts, give-ups, and the terminal reason, each a non-secret typed value. It is a third additive view alongside the callbacks and `Status`, never a replacement — and because a supervisor must not be pacing itself against its observer, its buffer is bounded and drops the oldest unread events (counted, and marked in-band) instead of applying backpressure.

SupervisorRestartEvent

A single restart, reported live from the supervision loop (see `Supervisor.OnRestart`) — not to be confused with the final `SupervisionOutcome.Restarts` count. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

SupervisorStormPauseEvent

A single failure-storm pause, reported live from the supervision loop (see `Supervisor.OnStormPause`) — not to be confused with the final `SupervisionOutcome.StormPauses` count. Sealed with an internal constructor so it can gain fields without breaking the frozen API.

TryParser<'T>

A C#-friendly parser for the `TryParse` verbs: the standard .NET `bool TryX(string, out T)` shape, so C# can pass a BCL parser like `int.TryParse` / `DateTime.TryParse`. Give the verb an explicit type argument — `cmd.TryParseAsync<int>(int.TryParse)` — or assign the parser to a `TryParser<int>` first; the explicit `'T` is required because BCL `TryParse` methods are overloaded (and C# can't infer `'T` from a byref lambda parameter either). It returns `true` and sets `value` on success, `false` otherwise — a `false` result becomes `ProcessError.Parse`. For a custom error *message*, throw from a `Parse` parser, or use the `Result`-returning `Runner.tryParse` from F#.

WaitAnyResult

The result of `RunningProcess.WaitAnyAsync`: which started process finished first and how it concluded. A named type (rather than a tuple) so the fields read clearly from C#.

WindowsIntegrityLevel

The Windows *mandatory integrity level* a child's token is lowered to — see `Command.WindowsIntegrityLevel`. Windows labels every token (and every securable object) with an integrity level and enforces a no-write-up policy: a process may not modify an object labelled above its own level, whatever the DACL says. Lowering the level a child runs at therefore takes away write access to the user's own files, registry, and windows *without* changing who the child runs as — the closest Windows analogue to the Unix `Uid`/`Gid` drop, and the mechanism behind browser renderer sandboxes. Only levels at or below an ordinary process's own are offered: a token's integrity can be lowered but never raised (`SetTokenInformation` refuses), so an "elevate me" variant could only ever fail the spawn. Pair with `Command.WindowsRestrictedToken` for the full drop — the two are independent (integrity governs what may be *written to*, privileges govern what may be *done*).

WindowsUiRestrictions

The Windows Job Object **UI restrictions** a process group can impose on its whole tree (`JOBOBJECT_BASIC_UI_RESTRICTIONS`) — see `ProcessGroupOptions.WithUiRestrictions`. These are the desktop-side counterpart of the resource caps: where the rest of `ResourceLimits` bounds what a contained tree may *consume* (and which cores it may consume it on), these bound what it may *do to the interactive session it happens to share with you* — read or overwrite the clipboard, change display or system-wide parameters, create or switch desktops, or log the user off / shut the machine down. A build plugin or a downloaded tool has no business doing any of that, and unlike a resource cap there is no way to notice after the fact that it did. A `[]` set: combine the members with `|||` (F#) or `|` (C#), or take the whole set with `All`. `None` (the default) leaves the Job's UI restrictions untouched, byte-identical to a group created before this option existed. **Windows-only, and honestly so.** The Job Object is the only primitive with this concept; POSIX (and Linux cgroup v2) have no equivalent, so requesting any restriction there fails `ProcessGroup.Create`/`UpdateLimits` with `ProcessError.Unsupported` rather than silently dropping it — exactly as the Unix-only `Command.Uid`/`Umask` family fails on Windows. What the restrictions do *not* do is sandbox the child's filesystem, network, or registry access; they are one layer of a perimeter (see the hardening guide), not a sandbox.

Type something to start searching.