Universal Read Output Contracts
Tracker references: pm-15yuly, pm-g8oh0f, pm-hb7ug8, pm-cxr0jb, pm-hid9g1, pm-h8tpeh, pm-5t33or, pm-wv47pf, pm-srns, pm-sb0tns, pm-gjjurs, pm-eugaqy, pm-jt8aa2, pm-kyjdne, pm-8nev0o, pm-oahhyc, pm-q4isdq, pm-e5gl05, pm-cha95z, pm-2qvq7a, and pm-xam9bt.
Agent Quick Context
Every built-in read surface uses four output dimensions: what to include, how much to return, how much the result may cost, and how to encode it. The same canonical controls work through the CLI, SDK, MCP, generated schemas, runtime contracts, and shell completions.
| Dimension | CLI | SDK and MCP | Meaning |
|---|---|---|---|
| Include | --output-include <csv> |
outputInclude |
Retain named fields or sections, or select a declared projection mode. |
| Amount | --output-limit <n|unbounded> |
outputLimit |
Bound shared row collections. |
| Cost | --output-budget <n|unbounded> |
outputBudget |
Fail closed when even the compact result cannot fit. |
| Encoding | --output-format <toon|json> |
outputFormat |
Select the CLI renderer and record the requested encoding. |
The contract covers list, context, search, get, next, health, deps, graph, history, activity, validate, events, contracts, comments, notes, files, docs, stats, and aggregate, including list aliases and ctx.
Primary row shaping follows each envelope's row_contract.row_keys, including
dot-delimited arrays and object maps such as graph.nodes. Include, amount, and
repeat suppression therefore remain bound to the rows the command says it
returns. A result may additionally declare continuation_row_keys when a
nested evidence collection must resume independently without redefining the
primary rows that --output-limit bounds. Cost compaction may inspect both
primary and nested collections; it does not rely on command-specific keys.
The runtime uses that declaration internally on every read but omits the
repeated metadata from results by default. Request
--output-row-contract / outputRowContract: true when a consumer needs the
row paths, jq selector, and active TOON encoding contract.
Include Modes
--output-include accepts two kinds of token. A field selector names a row
field or section and narrows the computed result. A projection mode names a
whole declared projection and is the canonical spelling of a command-local mode
flag: brief, compact, full, summary, and counts, depending on the
surface. Controls that change execution rather than projection remain separate:
for example, deps --collapse <none|repeated> retains dependency-grouping
semantics, and health --check-only retains refresh-suppression semantics.
Mode tokens are resolved before the command computes its rows, because a mode
selects which fields exist rather than which of the computed fields survive.
pm list --output-include brief is therefore exactly pm list --brief, and the
two are byte-identical apart from the read_output receipt that records which
spelling was used. Modes and field selectors compose: the mode selects the
projection, the remaining selectors narrow it.
pm list --status open --output-include brief # same result as --brief
pm contracts --output-include full # same result as --full
pm list --status open --output-include brief,id # brief projection, id only
Read readOutputIncludeModeOptions(command) from the SDK for the exact
replacement modes a surface declares. Every compatibility alias also declares
semantics: "replacement" | "behavior_preserving"; generators therefore do
not have to infer obligation strength from prose. The executable migration test
derives all 22 projection-mode replacements from this table, invokes both
spellings in a temporary tracker, and compares their useful result after
removing spelling receipts and volatile run metadata. A selector that matches
neither a declared mode nor any field on any returned row is refused with the
legal domain, rather than returning rows with every field removed.
Runtime discovery publishes the same exact vocabulary before invocation.
pm contracts --command <command> --summary returns
output_projection_contracts.commands, while
output_projection_contracts.global_ladder_scope: "union_not_per_command"
explicitly marks output_policy.degradation_ladder as a cross-command union,
not a promise that every member is accepted by the selected command. For
example, list declares brief, compact, and full; health declares
brief, full, and summary. The full read_output_dimensions matrix also
includes projection_modes for every canonical read surface, including an
explicit empty array when a surface has no whole-result modes.
Single-item detail fallback
Tracked by pm-8eclqf.
When a standard get exceeds its budget, the SDK first tries the declared
brief projection. It retains complete item metadata and dependencies while
omitting body, linked artifacts, claim history, and schedule details. The
receipt records applied_depth: brief and
degradation_reason: output_budget_reached; the omission receipt names the
removed sections and how to restore them. Explicit field selections, full/deep
reads, trees, and continuation requests keep their selected semantics.
If brief still exceeds the ceiling, normal compaction and eventual refusal
apply. estimated_tokens measures the emitted envelope;
omitted_result_estimated_tokens measures the useful result that could not fit.
Cross-Call Context Sessions
--output-session <json> / outputSession composes the four per-call
dimensions across a request group. The caller supplies versioned state and
passes the returned read_session.next_state to the next read:
{
"version": 1,
"id": "orientation",
"token_budget": 4000,
"spent_tokens": 0,
"seen_item_ids": []
}
The session ceiling and an explicit --output-budget both bind; the smaller
remaining allowance wins. Rows for item facts already present in the caller's
context become { "id": "pm-a1b2", "context_ref": "session:orientation:pm-a1b2" } instead of repeating prose. References retain
stable item identity and can be restored with pm get <item-id> --brief when
the prior context is unavailable. The receipt reports estimated and charged
tokens separately when the remaining group allowance is smaller than the
minimum control envelope, plus the accumulated spend, remaining capacity,
newly served items, and suppressed repeats.
The carried served-item set accepts at most 10,000 identities. A receipt always
preserves identities already present in the supplied state and retains newly
served identities in deterministic order until that capacity is full. When a
single read crosses the boundary, seen_item_overflow_count reports how many
new identities were not carried forward; those facts remain in the current
envelope and may be served in full again on a later read. The emitted
next_state therefore always remains valid input to the next call without
silently widening the safety bound.
Session state is deliberately caller-carried: CLI processes, SDK clients, MCP hosts, and packages share the same deterministic primitive without a hidden daemon or mutable cache. Validation rejects unknown fields, invalid identifiers, unsupported schema versions, unsafe integers, and spend beyond the declared ceiling before a read executes.
The mandatory orientation calibration runs context, list, search, get,
and next against both a two-item tracker and a 2,243-item tracker. Its
cross-call ceilings are strict: complete serialized bytes and cumulative spend
may only shrink, while repeat suppression may only hold or improve. The gate
also fixes the expected unique-fact shape:
| Tracker tier | Group spend / budget | Seen items | Suppressed repeats | Delivered bytes |
|---|---|---|---|---|
| 2 items | 3,820 / 20,000 | 2 | 3 | 15,274 |
| 2,243 items | 10,156 / 20,000 | 106 | 7 | 40,614 |
These are deterministic synthetic-corpus measurements from
scripts/release/context-intent-calibration.json; they contain no hosted
tracker content.
Precedence and Compatibility
Resolution is deterministic: canonical controls win over command-local compatibility options, which win over intent defaults, which win over command defaults. Existing options such as --fields, --limit, --token-budget, --format, --brief, and --full remain accepted. Contract output marks them as hidden compatibility aliases and supplies a migration hint; traversal, cursor, side-effect, and streaming controls instead receive an explicit behavior-preservation hint because a static output control cannot replace their semantics. Callers that omit the four shaping dimensions retain the established data projection; the one intentional envelope correction is that repeated row_contract metadata is now opt-in.
Completeness spellings preserve their established promise. pm list-all and
list-family --no-truncate imply an unbounded cost dimension when the caller
does not supply --output-budget; they cannot silently return a budget-trimmed
subset while claiming to return every matched row. An explicit canonical
budget still has precedence and may request a bounded result deliberately.
The same SDK-native rule applies to every declared complete-result spelling,
including contracts --full: explicit completeness defeats only the implicit
default ceiling. If an explicit caller budget cannot retain any useful domain
result, the CLI still prints the parseable omission receipt but exits 2; a
consumer therefore cannot interpret the missing command or action arrays as a
successful empty contract.
pm list --status open --output-include id,title,status --output-limit 10
pm context --for orient --output-budget 900 --output-format toon
pm search "runtime contracts" --output-limit 5 --output-format json
pm stats --output-row-contract
pm contracts --full --json
Canonical amount, encoding, and cost controls that leave an already-fitting result unchanged do not add a read_output receipt. Explicit include projections, sessions, continuations, and --output-row-contract retain the detailed receipt. Every shaped or budget-compacted result carries a read_output receipt with the requested dimensions, precedence, observed option provenance, deterministic estimated token count, string/row compaction signals, and budget outcome. When canonical controls are present, canonical_options_used records spellings such as --output-include; the optional field is omitted when no canonical control was supplied so minimum-budget session receipts remain compact. legacy_aliases_used and migration_hints record only compatibility flags the caller actually supplied. A command-local flag synthesized internally to execute a canonical projection mode is therefore never misreported as deprecated syntax. When both spellings are supplied, both provenance arrays report that mixed invocation. Budget degradation discovers nested arrays as well as declared result rows, so validation diagnostics and other governance payloads compact their inner findings before the useful result is omitted. compacted_row_paths names every reduced collection without redefining those nested arrays as ordinary pagination rows.
The estimate is ceil(utf8_bytes / 4) for the complete envelope. The CLI uses
its selected built-in JSON or TOON representation, including indentation and
the final newline. SDK calls that select outputFormat use that representation;
structured SDK calls without a renderer retain compact JSON measurement.
Those unformatted calls keep the conservative workload-class ceiling
(default_max_estimated_tokens, also used as the generated TOON ceiling).
Selecting JSON explicitly opts into its larger declared rendering allowance;
serializing an SDK object for measurement does not implicitly select that policy.
Per-call and session receipts include their own cost and continuation disclosure.
Compaction reserves room for that disclosure before returning a useful page, so
an envelope cannot claim to fit merely because its rows fit before receipts were
added. Each selected collection uses a measured prefix search instead of irreversible
halving, retaining the largest prefix that fits with the other collections held
fixed. applied_limit and both continuation forms follow the retained item count.
A complete page and a truncated page have different mandatory metadata costs;
row counts across that boundary are not an unconditional monotonicity guarantee. An infeasible ceiling returns the explicit omission result.
For context and next, that final projection is also the usage-feedback
delivery boundary. The SDK carries a JSON-invisible serve receipt through
intent and universal output projection, then appends the final omitted decision
and exact emitted item ids. An infeasible output budget records zero delivered
rows; it can never leave the packer's earlier inclusions marked as served.
PmClient finalizes the receipt before resolving, while the CLI finalizes it
from the exact active result passed to telemetry and after-command hooks.
When rows are dropped to satisfy a ceiling, the result also carries output_budget_truncation, naming the binding budget and its source, any explicitly requested dimension the budget overrode, every compacted collection path, and executable CLI/SDK/MCP recovery options — a default ceiling can override an explicit --output-limit unbounded, and that override is reported rather than silent. If a producer supplied an opaque item-page cursor, compaction rebases it to the last row actually returned and reports continuation_cursor_rebased: true; following the cursor therefore cannot skip rows removed from the middle of a producer page. If no useful content can fit, PmReadOutputBudgetExceeded provides a discriminated omission result, a compact { outputBudget: "unbounded" } recovery object, and omitted_result_estimated_tokens, the last useful-result estimate before omission; use isReadOutputBudgetExceeded before accessing result-specific fields. The encoding control --output-format <toon|json> also applies to mutation envelopes, including the mutation modes of comments, notes, files, and docs. Other universal read controls remain rejected before mutation execution. --json remains supported.
Cursor recovery carries the opaque value once in recovery.cursor and declares
the accepting cli, sdk, and mcp binding beside it. This avoids
serializing the same cursor once per transport while retaining an executable,
machine-readable binding for each surface.
When restore_with is "recovery", consumers must resolve the adjacent
recovery object: pass its cursor value through cli (--output-cursor)
or the sdk/mcp field (outputCursor) while repeating the original query.
The string is a reference, not a shell command. Other restore_with values
provide retry guidance when no declared row collection can continue.
Budget-compacted declared row paths are independently resumable. The
disclosure's continuations entries name the row path, retained/remaining/total
counts, and an opaque cursor. continuation_kind distinguishes a rebased
producer cursor, a universal output cursor, and a terminal page;
next_cursor mirrors the first universal entry for ordinary one-path
consumers. Replay the same query and budget
with --output-cursor <cursor> / outputCursor. The cursor validates the
command, declared continuation path, total, and canonical content fingerprint
before slicing, so same-cardinality content changes and other stale replays
fail closed instead of skipping evidence. A
bounded recovery therefore does not require replacing a 600-token request with
an unbounded multi-megabyte response;
recovery_budget_multiplier: 1 declares that each next page retains the same
useful-result ceiling.
Health continuation fingerprints use a versioned nonvolatile-snapshot policy
for the checks row. Command-lifecycle telemetry may refresh timestamps, queue
counters, and pending spans between pages, so the policy excludes only those
explicit direct fields from the telemetry row's details. The row identity,
verdict, enabled state, capture level, endpoint, environment overrides, probe
result, and failure errors remain fingerprinted. Every other health check also
remains fully fingerprinted; changing storage, integrity, history, or another
stable check fails closed. PM_READ_OUTPUT_CONTINUATION_FINGERPRINT_POLICIES
publishes the exact ignored detail fields per dynamic row. Other commands still
fingerprint every field, including fields with the same spelling, and any
policy-version change fails closed against an older cursor.
When compaction affects a nested collection that is not declared resumable,
the recovery budget is derived from the larger of the binding ceiling plus one
and the measured pre-compaction result. The SDK adds a 25% envelope margin and
rounds up to 100 tokens. The CLI, SDK, and MCP bindings therefore receive the
same finite value, which is strictly larger than the request that already
truncated. If safe-integer arithmetic cannot represent that value, recovery
uses unbounded explicitly. resolveReadOutputRecoveryBudget() exposes the
versioned pure calculation to package authors; there is no hard-coded smaller
retry and no recovery cycle.
Assurance declares budget_retention_policy: verdict_priority: failing block,
warn, and observe rows precede retired and passing rows while preserving order
within each class. assertions_total remains the pre-projection denominator,
and the assertions row path uses the same continuation primitive. A blocking
verdict consequently keeps its causal evidence on the first bounded page even
when the failing assertion was declared last.
Diagnostic gates retain bounded, actionable predicates. Every metadata field
required by the active validation profile emits a numeric count, including
zero, so assurance expressions can distinguish a clean check from a missing
key. Health retains at most 100 warning rows and reports warning_count,
warning_limit, and warnings_truncated; --strict-exit defaults to summary
projection unless --full is explicit, keeping the failing check identities
and warning codes inside the ordinary budget.
Failure transports use the companion Diagnostic Output Contracts. Those contracts bind errors, warnings, validation summaries, and recovery bundles independently from successful command results. Corrective actions precede explanation and survive every degradation stage, while untruncated JSON avoids a per-call receipt tax.
SDK and Package Usage
Typed PmClient read methods accept PmReadOutputOptions directly:
import { PmClient } from "@unbrained/pm-cli/sdk";
const pm = new PmClient({ pmRoot: ".agents/pm" });
const result = await pm.list({
status: "open,in_progress",
outputInclude: "id,title,status",
outputLimit: 10,
outputBudget: 800,
outputRowContract: true,
outputSession: {
version: 1,
id: "orientation",
token_budget: 4000,
spent_tokens: 0,
seen_item_ids: [],
},
// Supply outputCursor from output_budget_truncation to fetch the next row page.
});
Package authors should use the exported read-output contracts instead of inventing package-local limit or projection vocabularies. PM_READ_OUTPUT_SURFACE_CONTRACTS is the machine-readable matrix, PM_READ_OUTPUT_CONTINUATION_FINGERPRINT_POLICIES declares stable-snapshot exceptions, and resolveReadOutputDimensions plus applyReadOutputDimensions are the shared execution primitives.
Drift Gates
The full runtime contract reports every surface and all four dimensions. Strict SDK/MCP schemas expose the canonical camelCase keys, and the generated contract fixture catches surface or schema drift:
pnpm contracts:check
node scripts/run-tests.mjs test -- tests/unit/sdk/read-output-contracts.spec.ts
node scripts/run-tests.mjs test -- tests/unit/sdk/read-output-migration-hints.spec.ts
node scripts/run-tests.mjs test -- tests/unit/commands/workspace/completion-command.spec.ts