Project Assurance Primitives
Tracker: pm-2lex4r, pm-lyfu7b, pm-wn6wot, pm-91xeam, pm-uhv1m5, pm-m7bb7r, pm-py7qv2, pm-33mjrw, pm-q6n8sj, pm-h06944, pm-88mo8m, pm-atnfh4, pm-xmmafu, pm-4ok4ex, pm-dwj33e
Agent Quick Context
Assurance turns project policy into three reusable SDK-owned declarations:
- A measurement selects authoritative project data and produces a number or labelled set plus population, contributor, and compute-cost receipts.
- An assertion applies exactly one explicit bound, scope, lifetime, and enforcement level to a measurement. Required negative controls prove that the bound can both pass and fail.
- A gate evaluates named assertions at declared lifecycle triggers and returns one structured verdict shared by CLI, SDK, MCP, and CI callers.
Declarations live in .agents/pm/assurance.json. Every registry mutation and non-dry gate verdict is appended through the verified workspace history stream; never edit either file directly.
pm history _workspace --verify, pm validate, and pm health verify both
the hash chain and replay-to-disk agreement for every governed singleton. A
valid chain paired with a different, missing, or unreadable singleton is still
drift. SDK hosts can inspect that state with inspectWorkspaceHistoryState,
adopt a reviewed out-of-band value only through
reconcileWorkspaceJsonHistory with a terminal authorizing Decision, or
replace it from a verified version with restoreWorkspaceJsonFromHistory.
Both recovery paths append forward; neither rewrites the existing stream.
Why Assurance Exists
Project management is context management. A useful quality gate therefore needs more than a shell exit code: it must preserve what was measured, which population was judged, why a bound exists, who owns it, what changed the result, how expensive the evaluation was, and which immutable tree received the verdict.
The assurance SDK keeps those semantics independent from presentation. Commander and MCP only normalize inputs. Package authors and CI hosts can use the same public functions without reconstructing policy in scripts.
Declaration Vocabulary
Measurements support these built-in sources:
| Source | Purpose |
|---|---|
items |
Count authoritative full item records matching status, type, tags, an exact metadata field, or a field state of present/missing. |
dependency_kind |
Count typed relationship edges such as blocked_by or verifies; accepted aliases and canonical spellings measure the same population. |
graph |
Select a numeric or labelled-set field from a public graph SDK result. |
validate |
Select a validator check status or numeric detail. |
health |
Select a health check status, numeric detail, or labelled set. |
history |
Count immutable events by operation, author, harness, or model. |
links |
Count items with present or missing file, test, or documentation evidence. |
derived |
Combine numeric measurements with deterministic arithmetic and cycle detection. |
provider |
Delegate a measurement to an explicitly supplied host/package resolver. |
ASSURANCE_MEASUREMENT_SOURCE_KINDS and ASSURANCE_GATE_TRIGGERS are the
public SDK constants for these closed vocabularies. Runtime validation rejects
missing arrays and unknown discriminants with AssuranceMutationRefusalError;
it never persists an unrecognized source or leaks an incidental JavaScript
property-access error. Hosts that generate forms or package schemas can obtain
the same values from pm contracts --command assurance --json under
assurance_contracts.
Each measurement may declare max_cost. Evaluation fails closed when the total abstract compute units exceed that ceiling. Every result reports units, scanned items, scanned history rows, provider calls, duration, population size, and contributors.
One workspace context memoizes identical graph operations and the shared validate and health reports for its lifetime. A gate may therefore project many fields from one authoritative audit snapshot without multiplying full-workspace I/O or allowing concurrent assertions to observe different tracker states.
An items source with field must declare exactly one predicate: equals (including an explicit null) or state. state: missing treats an absent property, null, an empty string, or an empty array as missing; state: present selects the complement. Workspace evaluation loads full item metadata, so files, tests, and docs selectors measure stored evidence rather than a light projection that omitted those collections.
Assertions require exactly one polarity:
ceiling,floor,equals, orzeromonotone_nondecreasingormonotone_nonincreasingsubset_offor labelled sets
Scopes are all, active, or filter. A filter names another measurement whose contributors define the item population. lifetime: hold keeps the guarantee after its owner item becomes terminal. lifetime: retire retires it only after owner termination and requires retire_reason.
Enforcement is block, warn, or observe. Weakening a bound, scope, lifetime, owner, source measurement, or enforcement requires authorization_decision naming a terminal Decision item verified by the host. The transport verifies only that explicitly named item; it never treats unrelated workspace Decisions as authorization. Tightening does not require authorization.
CLI Workflow
Create a measurement:
pm assurance put measurement active-issues \
--definition '{"id":"active-issues","source":{"kind":"items","statuses":["open","in_progress"],"types":["Issue"]},"max_cost":5000}' \
--message "Track the active issue population"
Create an assertion with executable negative controls:
pm assurance put assertion active-issues-ceiling \
--definition '{"id":"active-issues-ceiling","measurement_id":"active-issues","owner_item_id":"pm-example","scope":{"kind":"active"},"ceiling":25,"lifetime":"hold","enforcement":"block","negative_control":{"cases":[{"observed":25,"expected":"pass"},{"observed":26,"expected":"fail"}]}}'
Create and evaluate a gate:
pm assurance put gate release-readiness \
--definition '{"id":"release-readiness","assertion_ids":["active-issues-ceiling"],"triggers":["ci","pre-release"]}'
pm assurance run release-readiness --trigger ci --dry-run --json
pm assurance run release-readiness --trigger pre-release --tree "$(git rev-parse HEAD)" --json
pm assurance verdicts release-readiness --limit 20 --json
Registry reads and removals use the same nouns:
pm assurance list measurement --json
pm assurance show assertion active-issues-ceiling --json
pm assurance remove gate release-readiness
Referenced measurements and assertions cannot be removed. Remove the consuming gate or assertion first.
Presets, derivation, and promotion
List the four built-in project shapes, preview one as ordinary declarations, or apply it atomically:
pm assurance presets
pm assurance presets software-delivery --owner pm-example
pm assurance apply software-delivery --owner pm-example \
--message "Adopt the initial delivery evidence contract"
The preset creates measurements, assertions, and a gate in one audited transaction. Reapplying the same bundle is idempotent; an existing divergent id is refused rather than overwritten. The available shapes are software-delivery, research, agent-evaluation, and operations.
Self-derivation observes active items without writing anything. Each proposal reports its active scope, population size, and observed missing-evidence ceiling. Persistence requires the explicit --apply flag:
pm assurance derive --owner pm-example
pm assurance derive --owner pm-example --apply \
--message "Accept the observed evidence baseline"
pm assurance promote derived-active-missing-tests-ceiling --enforcement warn
pm assurance promote derived-active-missing-tests-ceiling --enforcement block
Derived assertions start at observe. Promotion is exactly one step (observe to warn, then warn to block) and each transition is an ordinary audited declaration mutation. There is no automatic promotion and no privileged preset execution path.
Extension Measurement Providers
An extension opens the measurement vocabulary through api.registerAssuranceMeasurementProvider. The registration declares stable keys, parameter types, a coarse low/medium/high cost class, network use, a host timeout, and a resolver. It requires the services capability; a network provider must also declare permissions.network: true in manifest.json.
This code-quality provider measures a local report without changing assertion or gate semantics:
import { readFile } from "node:fs/promises";
import { defineExtension } from "@unbrained/pm-cli/sdk";
export default defineExtension({
activate(api) {
api.registerAssuranceMeasurementProvider({
id: "coverage",
keys: {
lines: {
value_type: "number",
parameters: { report: { type: "string", required: true } },
},
},
cost_class: "low",
network: false,
timeout_ms: 2_000,
async resolve({ parameters }) {
const report = JSON.parse(
await readFile(String(parameters.report), "utf8"),
) as {
total: { lines: { pct: number } };
};
return { value: report.total.lines.pct, population_size: 1, cost: 1 };
},
});
},
});
An evaluation package can expose episode reward on the same surface:
api.registerAssuranceMeasurementProvider({
id: "agent-eval",
keys: {
"mean-reward": {
value_type: "number",
parameters: { suite: { type: "string", required: true } },
},
},
cost_class: "high",
network: false,
timeout_ms: 120_000,
async resolve({ parameters }) {
const result = await runFrozenEvaluation(String(parameters.suite));
return {
value: result.meanReward,
population_size: result.episodes,
cost: result.steps,
contributors: result.regressedScenarioIds,
};
},
});
An external registry provider declares its network dependency explicitly:
api.registerAssuranceMeasurementProvider({
id: "npm-registry",
keys: {
"dist-tag-count": {
value_type: "number",
parameters: { package: { type: "string", required: true } },
},
},
cost_class: "medium",
network: true,
timeout_ms: 5_000,
async resolve({ parameters }) {
const name = encodeURIComponent(String(parameters.package));
const response = await fetch(
`https://registry.npmjs.org/-/package/${name}/dist-tags`,
);
if (!response.ok) throw new Error(`registry returned ${response.status}`);
const tags = (await response.json()) as Record<string, string>;
return { value: Object.keys(tags).length, population_size: 1, cost: 10 };
},
});
The corresponding measurement is ordinary registry data:
{
"id": "published-tag-count",
"source": {
"kind": "provider",
"provider": "npm-registry",
"key": "dist-tag-count",
"parameters": { "package": "@example/tool" }
}
}
A provider-backed gate must opt into every provider and each trigger's execution envelope. Omission refuses provider execution:
{
"id": "release-readiness",
"assertion_ids": ["published-tag-count-ceiling"],
"triggers": ["ci", "scheduled"],
"provider_policy": {
"allowed_providers": ["npm-registry"],
"triggers": {
"ci": { "max_cost_class": "low", "allow_network": false },
"scheduled": { "max_cost_class": "medium", "allow_network": true }
}
}
}
Before invocation the host verifies the provider allow-list, declared cost class, and network capability for the active trigger. It then validates key parameters and result shape, enforces the registered timeout, and charges the returned cost through the existing measurement ceiling. Extension tests must bind PM_PATH and PM_GLOBAL_PATH to temporary roots; never point provider fixtures at the repository tracker or a live service.
SDK and MCP
The reusable client exposes the same action grammar:
import { PmClient } from "@unbrained/pm-cli/sdk";
const pm = new PmClient({ pmRoot: ".agents/pm" });
await pm.assurance({
action: "run",
id: "release-readiness",
trigger: "ci",
dry_run: true,
});
For direct host composition, use evaluateMeasurement, evaluateAssuranceGate, createAssuranceWorkspaceContext, the preset/derivation helpers, and the audited declaration/verdict helpers exported from @unbrained/pm-cli/sdk and its focused @unbrained/pm-cli/sdk/governance entrypoint. Active extension registrations are discovered automatically. Embedding hosts may additionally pass stable resolver ids and matching provider_capabilities to createAssuranceWorkspaceContext; an absent resolver or capability fails loudly. The core evaluator bounds concurrent assertions and expression operands, and workspace history loading uses bounded concurrency; item-only callers can explicitly skip history and Git identity resolution.
Hosts that want the normal action transport and its audited registry lookup can
pass the same adapters as the third runAssuranceAction argument. The request
and project path remain transport data; resolver capabilities remain explicit
runtime authority:
await runAssuranceAction(
{
action: "run",
id: "repository-static-quality",
trigger: "ci",
dry_run: true,
},
{ path: ".agents/pm" },
{
workspace: {
include_history: false,
providers: {
"repository-quality": resolveRepositoryQualityMeasurement,
},
provider_capabilities: {
"repository-quality": { cost_class: "high", network: false },
},
},
},
);
This is the migration boundary for repository automation: scripts may remain
focused process adapters, while assertion composition, provider policy,
negative controls, lifecycle triggers, and verdicts move into ordinary
assurance declarations. scripts/release/gate-registry.json records every
gate script as migrated, provider-backed, or retained and fails unless the
migrated/provider-backed population is the majority.
AssuranceMutationRefusalError and AssuranceEvaluationRefusalError are
exported from both @unbrained/pm-cli/sdk and
@unbrained/pm-cli/sdk/governance, so hosts can classify expected declaration
or evaluation refusals without parsing messages. Source-resolution refusals
also carry the gate, assertion, measurement, source kind, field, and optional
health/validate check, so a zero value remains distinct from an absent or
mistyped field.
Generic SDK and MCP dispatch use action: "assurance" with subcommand set to list, show, put, remove, run, verdicts, presets, apply, derive, or promote. Discover the current machine contract instead of copying parameter lists:
pm contracts --action assurance --schema-only --json
pm contracts --command assurance --flags-only --json
pm contracts --command assurance --json
Command-scoped contract output omits unrelated extension, governance,
relationship, and Commander-alias catalogs unless --full is requested. This
keeps the selected action schema and its assurance vocabularies complete under
the default output budget instead of truncating the very enum a caller needs.
Verdict Contract
A gate emits one object containing:
- gate id, evaluated tree, trigger, timestamp, and dry-run status;
- overall
pass,warn, orblockplus stable exit code; - every assertion's measurement, scope, population, observed value, structured bound, signed distance, enforcement, negative-control proof, cost, and contributors;
- every assertion's
measurement_definition_fingerprint, a SHA-256 identity for the exact declaration that produced the observation; - an aggregate compute receipt.
Dry runs never write history. Non-dry verdicts are immutable workspace audit events and remain queryable after ordinary registry changes. Verdict reads return newest entries first and default to a bounded result; use --limit to select up to 1,000 matching records. A blocking verdict exits non-zero; warnings and observations remain successful while preserving their failed assertion rows.
Verdicts persisted before definition fingerprints were introduced remain readable and are identifiable by the absence of measurement_definition_fingerprint. Registry mutations and verdict writes use the same explicit-author, configured-author, and detected-harness precedence as other SDK mutations; they do not manufacture an unknown author when a harness identity is available.
Safety and Evolution
- Use stable lowercase ids; prose belongs in descriptions and mutation messages.
- Prefer saved measurements and derived arithmetic over duplicating queries in scripts.
- Give expensive graph, health, validate, or provider measurements explicit cost ceilings.
- Keep owner items and authorization Decisions linked into the project graph.
- Treat negative controls as part of the policy, not test decoration.
- Use
holdunless a time-bounded guarantee has an explicit retirement rationale. - Use
--dry-runwhile authoring or tightening a gate, then persist a verdict against an immutable tree.