Policy v2 and gate decisions#
Maida policy v2 makes the source of every acceptance criterion explicit. The gate is a pure function of the candidate code, policy, immutable baseline, and environment. It never accumulates baseline observations across CI runs.
version: 2
trials: 3
fail_fast: true
metrics:
stop_condition_reached: {kind: invariant, require: true}
forbidden_tools: {kind: invariant, none_of: [admin_delete]}
step_count:
kind: measured
direction: upper
tolerance: {relative: 0.5}
cost_tokens:
kind: measured
direction: upper
tolerance: {relative: 0.25}
task_pass_rate:
kind: statistical
direction: lower
threshold: 0.90
confidence: 0.95
success_predicate: all_invariants_passed
mode: report_only
Policy is hand-authored input and fails closed. Unknown fields are errors.
The version uses major[.minor]: version: 2 normalizes to (2, 0);
version: 2.1 enables the plan metrics below; version: 2.0.0 is not valid.
Metric kinds#
Kind |
Acceptance criterion |
Sufficiency |
|---|---|---|
|
Author-declared semantic contract |
No sample requirement |
|
Author-declared numeric tolerance or limit |
Valid at N=1 |
|
One-sided prediction bound inferred from a baseline sample |
Checked when the baseline is bound |
|
Policy threshold θ and candidate Bernoulli outcomes |
Checked when policy loads |
The band is either declared or purchased. A measured tolerance is cheap and works with one candidate observation. A distributional bound requires enough baseline trials to earn its requested coverage.
Invariant#
An invariant is exact. Any observed violation fails, including at N=1. Across
multiple trials, trials are a counterexample search rather than a rate sample:
one violation is enough. Reports show violated in 1/25 trials so intermittent
contracts are visible. Reclassify an intermittent property instead of weakening
the invariant rule.
Supported invariants are stop_condition_reached, forbidden_tools,
required_tools, no_loops, no_guardrails, plan_effectful_modules, and
plan_grants.
Measured#
A measured metric compares the candidate aggregate with a user tolerance or
hard limit. The default aggregate is median. Every report includes
min/median/max. aggregate: max and aggregate: p90 are explicit upper-tail
opt-ins.
A one-in-25 tail event is not a measured question. Express “the rate of
step_count > X increased” as a Bernoulli statistical metric.
Relative and absolute tolerances are additive around the baseline aggregate. When a hard limit is also present, the stricter boundary applies.
Pre-execution plan rules#
Policy 2.1 adds the deliberately small vocabulary used to judge a generated plan before it executes:
version: 2.1
metrics:
plan_depth: {kind: measured, direction: upper, limit: 4}
plan_fanout: {kind: measured, direction: upper, limit: 2}
plan_budget_cost_usd: {kind: measured, direction: upper, limit: 1.5}
plan_budget_model_tokens: {kind: measured, direction: upper, limit: 2000}
plan_budget_tool_calls: {kind: measured, direction: upper, limit: 10}
plan_budget_wall_time_ms: {kind: measured, direction: upper, limit: 10000}
plan_effectful_modules:
kind: invariant
allowed: [demo.deliver]
none_of: [untrusted.shell]
plan_grants:
kind: invariant
allowed: [records.context.read, messages.deliver]
approval_required_for: [messages.deliver]
allowed requires the observed set to be a subset of the declared values, so
an explicit allowed: [] permits no values. none_of forbids any overlap;
all_of requires named values to be present. An empty none_of, all_of, or
approval_required_for clause is rejected when it would make the whole rule a
no-op. For plan_grants, approval_required_for requires the resolved artifact
to carry an approval requirement for each named effect that the plan actually
requests; unrelated policy-listed effects do not reject a harmless plan. Plan
metrics require complete pre-execution evidence from a plan backend for every
trial; a trace-only or partially evidenced gate fails closed rather than
pretending to evaluate them.
Distributional#
A distributional metric infers a one-sided order-statistic prediction bound
from the immutable baseline trial vector. For exchangeable observations, a new
draw exceeds the maximum of n baseline observations with probability
1/(n+1). Therefore:
n_min = ceil(1 / (1 - coverage)) - 1
This is 19 baseline trials for 95% coverage and 9 for 90%. Ties are
conservative. direction: both is rejected; declare a measured two-sided
tolerance if both sides are harmful.
At candidate N=1 the gate applies the prediction bound directly. At N>1 it
counts harmful exceedances and applies a one-sided Wilson bound to the
exceedance rate. Applying any-trial failure here would produce
1 - coverage^N false failures (72% at 95% coverage and N=25), so it is
intentionally not used.
If the baseline sample is too small, omitted mode resolves to
report_only. Explicit mode: gating is rejected when the baseline is bound,
with the required recapture size.
Statistical#
A statistical metric evaluates candidate Bernoulli outcomes against policy θ.
The baseline does not contribute. task_pass_rate defaults to the explicit
named predicate all_invariants_passed: one trial succeeds when every
invariant-tier metric and the agent process succeeded on that trial.
confidence is the one-sided coverage of the Wilson bound. At 0.95,
z = 1.645; it is not the coverage of a two-sided interval. Direction chooses
the harmful side:
lower: pass rate and stop-condition-reached rateupper: error, retry, or harmful-event ratesboth: only when explicitly declared
PASS and FAIL are both earned from the relevant one-sided bound. Otherwise the metric is INCONCLUSIVE. For unanimous lower-direction outcomes:
Wilson lower = n / (n + z²)
n_min = ceil(θ z² / (1 - θ))
At 95% confidence:
θ |
Minimum candidate trials |
|---|---|
0.70 |
7 |
0.80 |
11 |
0.90 |
25 |
An explicit gating metric below n_min is rejected while loading policy. The
error includes θ, confidence, computed n_min, configured trials, and both
remedies: raise trials or set mode: report_only. If mode is omitted, the
metric is report-only below the boundary and promotes to gating at the
boundary.
Direction and reporting#
direction is required on measured, distributional, and statistical metrics.
upper means only an increase can block; lower means only a decrease can
block; both must be explicit and is never inferred by v2.
Direction controls blocking, not reporting. A step count falling from 12 to 5
passes an upper-direction gate but the -7 delta remains in the PR comment.
Large improvements can reveal skipped work and deserve review without a red
check.
Composition and stopping#
FAIL dominates. PASS requires every blocking tier to pass and no gating metric
to be inconclusive. A gating INCONCLUSIVE produces the overall neutral
INCONCLUSIVE result. report_only metrics never block and have no verdict.
fail_fast: true is the default. The fixed budget stops when a blocking
failure is irreversible, especially an invariant violation, and reports
trials_used, trials_budgeted, stopping_rule, and abort_reason.
Use --no-fail-fast for baseline capture and calibration.
Exit codes are stable across report versions:
Exit |
Meaning |
|---|---|
0 |
PASS or INCONCLUSIVE |
1 |
Gate FAIL |
2 |
Input, policy, baseline, or run not found/invalid |
10 |
Internal execution error |
Immutable baseline capture#
Run the full candidate budget and bind it explicitly:
maida run agent.py --trials 25 --no-fail-fast --json-out report.json
maida baseline --from-report report.json --out .maida/baselines/agent.json
The checked-in baseline stores each per-trial numeric and invariant outcome
vector, trace structural signatures deduplicated by hash with counts, and an
environment fingerprint. When the report includes plan evidence, the additive
plan_sample stores its numeric vectors, set values, and canonical plan
artifacts deduplicated by artifact ID. It is never mutated or accumulated by
gate runs. Recapture explicitly when more evidence is desired.
Version streams#
The five formats are intentionally not harmonized:
Stream |
Current |
Form |
Compatibility |
|---|---|---|---|
Policy |
|
|
Hand-authored; unknown keys and unsupported minor versions reject; 2.0 remains valid for trace-only rules |
Trace |
|
full semver |
Generated; legacy |
Baseline |
|
full semver |
Generated; 0.3 patch drift and legacy |
Report |
|
full semver |
Generated external contract; consumers must ignore unknown fields within a major |
Plan |
|
full semver |
Generated canonical artifact; exact version and content digest are validated |
Report fields are never removed or repurposed within a major. Report consumers must ignore unknown fields. Policy loaders do the opposite because silently ignoring a requested policy key would make the gate claim enforcement it did not perform.
v1 migration#
An unversioned or version: 1 policy loads through a visible deprecation
warning. Known legacy metric names infer their established directions:
step/tool/cost/latency are upper; pass rate is lower. Unknown v1 keys are named
in the warning.
N=1 users retain binary pass/fail through invariant and measured tiers. Default N=3 users retain their verdict unless the old gate passed solely because a statistical metric used the removed 3/3 unanimous compatibility rule. That is the only branch-protection-visible migration change.
The former single_trial_binary and small_n_unanimous decision rules no
longer exist.