daniel-ospina

proportional-gates

"Reference: replaces rigid/programmatic skill rules with judgment-based gating. Consumed by other skills — not invoked directly. Defines proportionality principle, risk-tiered verification, and the 'reviewer validates judgment' pattern."

daniel-ospina 0 2 Updated 2d ago
GitHub

Install

npx skillscat add daniel-ospina/agent-infra/proportional-gates

Install via the SkillsCat registry.

SKILL.md

⛔ This skill MUST be read in full — not skimmed. Formal review gates depend on its workflow.
Skipping steps silently bypasses quality checks. Missing gates = undetected breakages.

Proportional Gates

Ontology: tortoise/docs/ONTOLOGY.md (v3.1, canonical) — fetch: gh api repos/daniel-ospina/tortoise/contents/docs/ONTOLOGY.md --jq .content | base64 -d (§5 = controlled vocabulary).

Overview

Rigid rules ("always run 4 reviewers", "always typecheck", "always use a worktree") create bureaucracy without intelligence. They force the same overhead on a 3-line config change as on a 500-line DB migration. This wastes tokens, slows down agents, and produces non-sensical results (typecheck failures attributed to markdown changes).

Proportional gates replace mechanical rules with a single principle: match verification depth to change risk and novelty. An agent uses judgment to decide what gates to run. A reviewer validates those decisions.

This skill is the canonical reference. Consuming skills cite proportional-gates §Review Cycles by name and never copy its cells; the domain tables (workspace isolation, pre-flight verification, dependency verification, research depth) may be inlined — see §Consuming These Tables.


Core Principle

The cheapest verification that catches the most likely failure mode. Proportionate to the change. Validated by review.

Every gate decision follows the same logic:

  1. Classify the change — what type? what risk? what novelty?
  2. Select proportional verification — what's the cheapest check that catches the likely failure?
  3. Note the decision — document what was skipped and why
  4. Reviewer validates — a fresh sub-agent confirms the gating decisions were appropriate

Change Classification

Before deciding what gates to run, classify the change:

Dimension Low Medium High
Code impact Docs, config, CSS, strings only Single component/hook change Multi-file, shared types, DB, auth
Surface risk No runtime behavior change UI behavior change, existing pattern New integration, new data flow, auth change
Novelty Following existing pattern exactly Adapting existing pattern New pattern, unfamiliar library, first-of-kind
Reversibility Trivially revertible Requires migration rollback Destructive, data loss possible

Overall risk = highest dimension. A docs-only change with high novelty = Medium (docs are low-risk even if novel).


Proportionality Tables

Workspace Isolation

Risk Isolation
Low Plain branch inside a worktree; in a hub, create the worktree first (#626).
Medium Worktree recommended if 3+ files or shared infrastructure. Plain branch OK for single-file.
High Worktree required. Hub dirty → bash scripts/checkout-hygiene/hub-worktree.sh salvage <branch> (in-hub git stash push is M4-blocked); inside a worktree, do not stash — refs/stash is shared by every worktree of the repo, so a bare git stash pop can take a peer lane's work (#1264) — make a WIP commit on your own branch instead.

Never start on main/master regardless of risk.

⛔ Guard note (#626): "Plain branch" means a branch created from inside an existing
worktree — in-hub git checkout -b is BLOCKED in every repo (the shared main checkout
must never be flipped; agent-infra's #99 exemption was removed in #615 and the M3
create-new carve-out in #626). In a hub, create an isolated worktree first
(using-git-worktrees skill; agent-infra: bash scripts/checkout-hygiene/hub-worktree.sh <branch>).

Pre-flight Verification

Risk Typecheck Build Integration Tests pgTAP
Low Skip (no TS changes) Skip Skip Skip (migrations = always High)
Medium Run if .ts/.tsx changed Run if src/ changed Run if integration surfaces touched Run if migrations
High Always run Always run Always run Always run

Skip rule: If a check would not catch the change's failure mode, skip it. A markdown change cannot cause a type error. A CSS change cannot break an integration test.

Review Cycles

Risk Reviewers Max Cycles
Low 1 reviewer —
Low-Medium (small plan, existing patterns) 2 reviewers 3
Medium-High (large plan, some novelty) 3 reviewers 5
High (novel architecture, first-of-kind) 4 reviewers 10

— is the skip sentinel: one reviewer pass, no re-review (the enforcer reads it as 0 loop cycles). A numeric cell counts cycles, where cycle 1 is the first pass. Tier crosswalk: micro → Low, standard → Low-Medium, complex → High. Editing this table requires the matching change in extensions/loop-enforcer/termination.ts and tier-config-parity.test.ts in the same commit — the parity test parses it.

Proportional dispatch: The agent decides how many reviewers to launch based on plan size and novelty. A 20-line plan following existing patterns = 2 reviewers. A 200-line plan with new architecture = 4 reviewers.

The Low code-impact class is a property of the CHANGE, not of the linked issue (#1348). A
docs/stylesheet-only PR may be linked to a complexity:standard issue and still be Low. The merge gate
represents that with the clean-low verdict (scripts/record-review.sh), whose class is deliberately
NARROWER than the Low value of §Change Classification's Code impact column (the cell that reads
"Docs, config, CSS, strings only"): config and strings are excluded, because a config change is where a
runtime-behaviour change hides (agent-infra #1348's own motivating regression, #4708, was a config
change), and so are root instruction files and any enforcement input. Its guard is fail-closed — an
unreadable or truncated diff is never certified Low. See the OVERRIDES: line on agent-infra #1348.

The SURFACE CAP — a tier is capped by what the change can BREAK, never by how small it looks. The shallow verdicts (clean-micro, clean-low) are DEPTH exemptions, so the CEILING on them is keyed on the changed paths: while any changed path is in the ALWAYS-FULL class — the merge/land/repair rails (scripts/admin-merge.sh, scripts/ci-failure-set.sh, scripts/atomic-land.sh, scripts/check-pipeline-compliance.sh, the recorder scripts/record-review.sh), .github/workflows/**, .github/actions/**, .husky/**, extensions/**, skills/**, templates/**, enforcement/**, scripts/check-*, scripts/lib/diff-normalize.py (the ONE implementation of the diff digest, so a normalizer that prints nothing yields a constant DIFF_HASH and a false accept), and the modules the rail EXECS to decide whether a red check blocks (scripts/ci_exemption.py, scripts/ci-workflow-pr-evaluable.sh, scripts/checkout_guard.sh, scripts/ci_runner_gate.sh) — only clean is honest, and record-review.sh REFUSES (exit 4, no record) a clean-micro whose diff touches one, whenever the diff is readable. A transport failure (no gh, the pulls read failed) only WARNS and the record proceeds — this cap is a ceiling on a self-selected tier, not the attestation itself — but a payload the API ANSWERED with and this classifier cannot certify (a pulls body with no base/count, a compare non-2xx such as the fork-head 404, no merge base, a pathless row) REFUSES, because that is positive evidence rather than absent evidence. The boundary of the class is stated so it does not grow without one: the surfaces whose own correctness IS a gate, not every script. skills/** and templates/** are in the class because they are AGENT-EXECUTABLE INSTRUCTIONS: skills/code-review/SKILL.md is the script that decides what "reviewed" means (a micro edit there would rewrite the gate while dispatching no reviewer), and templates/ materializes into every consumer repo's AGENTS.md. That arm exists because the micro tier used to be selected from the linked ISSUE'S LABEL alone and never looked at the diff, so a complexity:micro issue could record clean-micro for a one-line merge-rail edit — and the micro flow dispatches no reviewers at all (measured 2026-10-23: the refusal had to be added; on the unchanged recorder that record was WRITTEN).
Ask BEFORE dispatching: record-review.sh --print-tier <pr> [owner/repo] prints <status>\t<full|low|micro> — the highest tier the diff's surfaces justify; every non-ok status (empty, too_large, unclassified, unavailable) carries an EMPTY tier. Never key on diff SIZE: review cost is flat in diff size (a 5-line shell fix reviewed at 1.73M tokens; a 36-line test fixture at 3.06M), so a "small diff, shallow review" tier buys nothing while inviting a genuinely dangerous diff to look cheap. The class is a hand-maintained denylist, and deliberately so — the SOTA (Meta's Predictive Test Selection, Microsoft TIA, Google presubmit) computes a dependency graph instead, and its named failure mode is unsoundness when that graph is incomplete; a hand-kept list of the surfaces whose correctness IS the product's gates is the conservative form of the same idea (no named practice for it — it is local conservatism, stated as such). The selector resolves unclassifiable paths (a ../absolute/control-character row) to full, which is the standard "fall back to full when the change cannot be classified" mitigation, and it distinguishes a TRANSPORT failure (unverified — warned, not blocking) from a PAYLOAD it cannot certify (refused), so an unusual diff row can never read as "the network was down".

Adversarial domain (bound: 2 cycles) — orthogonal to the rows above. For gate/enforcement code whose correctness is "an attacker cannot make it fail open" (argv/path/symlink resolution, working-tree discard, merge and verification gates), the budget is bounded by the declared threat surface, not by reviewer exhaustion: cap 2 cycles, acceptance = every declared threat class covered by a test + green CI, residuals filed from cycle 1, not chased. A fresh reviewer returning THREAT SURFACE COVERED (all declared classes covered, no in-scope bypass reproduced) is this domain's clean exit — a literal NO ISSUES FOUND is not required; when the merge rests on threat-list coverage, disclose it in the PR body as [ADVERSARIAL-BOUND] cycles=<N> threats=<K> covered=<K> residuals=<#N,…|none>. The declaration is mandatory at scoping (issue-scoping §Adversarial Threat Surface). Statement of record: AGENTS.md §Hard Cap.

Dependency Verification

Situation Action
Dep already used elsewhere in codebase Skip verification — pattern is known
Dep is well-known stdlib-adjacent (lodash, date-fns) Skip — common knowledge
Dep is new to codebase AND not in plan's Pattern Research Verify: 1-2 Perplexity calls
Dep is novel, unfamiliar, AND plan has no Pattern Research Verify: 2-3 Perplexity calls. Pause if unavailable.

Perplexity unavailable: If dep is well-known (used in 2+ other repos, documented extensively), proceed with note. Only pause for genuinely novel deps.

Research Depth

Topic novelty Research calls
Well-known pattern (codebase has 2+ examples) 0-1 Perplexity calls
Pattern exists but choice has trade-offs 2-3 Perplexity calls (canonical + comparative)
Novel pattern, no codebase precedent 3+ Perplexity calls (canonical + comparative + pitfalls + recency)
First-of-kind, unfamiliar domain 5+ calls (all framings + scale + adversarial)

The Reviewer-Validates-Judgment Pattern

Instead of rigid rules, skills use this pattern:

Agent: "Classified as Low risk — skipping typecheck (no TS changes), 
        using plain branch (single markdown file). 
        Reviewer will validate."
        
Reviewer sub-agent: reads the change, confirms:
  - Classification is correct (are there hidden .ts changes?)
  - Skipped gates would not have caught anything
  - → "Classifications validated: Low risk confirmed. All skips appropriate."
  OR
  - → "Found .ts file in diff — reclassify as Medium. Run typecheck."

This is the same generate-review loop applied to gate selection itself. The agent uses judgment; the reviewer catches mistakes; the loop converges.


Consuming These Tables

Consuming skills cite proportional-gates §Review Cycles by name and never copy its cells — a copy is a second source of truth, and it is not the table the parity test parses, so it drifts silently.

The judgment principle and the domain tables (e.g. workspace isolation, pre-flight verification, dependency verification, research depth) may be restated or inlined in a consuming skill's own words — "replace rigid rules with judgment-based gating", and the Reviewer-Validates-Judgment pattern above. Reviewer counts and cycle caps may not.


When NOT to Apply Proportionality

Some gates should remain absolute — they catch catastrophic failures that judgment cannot reliably predict:

Gate Why Absolute
pgTAP on migrations Data loss from bad migrations is irreversible. Always run.
P0 human gates Data loss, security, legal/compliance, cost >$10/mo. Always pause.
Commit to main/master Never. Always use a branch.
Force push to main Never.

Proportionality applies to verification depth, not to safety invariants.

Continue following the workflow as mandated by this skill. Do not skip steps.

Review Gate Routing by Level

For review gates that dispatch sub-agent reviewers, route by the issue's Level field:

Level Dispatch Rationale
Epic Sub-agent reviewer (fresh context) Full adversarial check needed; scope justifies dispatch cost

Fallback: If Level has no row above (missing or unrecognized), default to sub-agent review (safe default = full review, never skip).

Skills affected: plan-review, code-review, test-review, and any skill that dispatches reviewer sub-agents.

Enforcement: The skill-enforcer extension already requires the relevant skill to be read before the operation. This routing table is an agent instruction — the agent reads this skill and applies the rule.