Warden tiers

Warden tier vocabulary

← eval suite index

Warden tier vocabulary

Every plan under .warden/plans/ declares a tier: and a requires: in its YAML front matter. This file is the single normative definition of what the seven labels mean, which label wins when a plan carries several, and the rule that keeps the two front-matter fields from drifting apart. check-plan-tiers.mjs enforces exactly what is written here — if the two disagree, this file is wrong and should be fixed, not the checker.

The point of the vocabulary is selection, not skipping: an operator runs bash .warden/run.sh tier:<label> to pick a cheap subset by declared requirement instead of naming plans by hand. See "Why tiers and not caching" below for why that is the whole ambition.

Labels

Seven labels, defined operationally — by what a plan does, not by a guess:

Primary-tier rule

A plan commonly carries several of the labels above. tier: takes the most restrictive one, in this total order — written as one line so it can be grepped:

destructive > volatile > needs-model > needs-server > needs-postgres > needs-redis > hermetic

Every remaining label goes into requires:, using the same seven-label vocabulary — not a stripped or renamed form of it.

Worked example: 06-socrates carries destructive + needs-model + needs-server + needs-postgres. destructive outranks the other three, so tier: destructive and requires: [needs-model, needs-server, needs-postgres].

Anti-drift rule

requires: may not contain the plan's own tier: value, and may not contain any label that outranks tier: in the order above. Either case would mean tier: is not actually the most restrictive label carried by the plan — the two records have drifted apart, and the primary-tier rule stopped holding.

The consequence is what makes the two fields useful together: tier: alone answers "how expensive or dangerous is this plan at worst", and tier: plus requires: together answer "what must be up for it to run". A label can appear in exactly one of the two fields on a given plan, and the more restrictive placement always wins.

Per-plan declarations

Plantier:requires:Evidence
00-warden-selftesthermetic[]executes warden's own selftest scripts + the tier gate over files in the checkout; no external service
01-greenlight-gatesvolatile[]pnpm audit:gate is network/time-sensitive; regenerates server/openapi.json, judges its own diff, reverts it
02-foundation-substratehermetic[]source grep only; no env file sourced
03-connecthermetic[]source grep only
04-permissionshermetic[]source grep/rg only
05-collapsedestructive[needs-postgres]DROP SCHEMA CASCADE on shared robin_ci
06-socratesdestructive[needs-model, needs-server, needs-postgres]DROP SCHEMA, npx agent-browser, OPENROUTER
07-wiki-typesneeds-postgres[]vitest against robin_ci
08-workspace-readsneeds-postgres[]vitest against robin_ci
09-d30-gateneeds-postgres[]vitest against robin_ci
10-app-shellneeds-server[needs-postgres]curl + agent-browser against live app/server
11-captureneeds-postgres[]vitest against robin_ci
12-ask-editdestructive[needs-postgres]pushTestSchema()
13-bug-sweepdestructive[needs-postgres]DROP SCHEMA; resets because 06 leaves state behind
14-member-authoring-governancedestructive[needs-postgres]DROP SCHEMA
15-v1.2-payloaddestructive[needs-postgres]provisions and drops throwaway mig_test
16-corpus-evalsdestructive[needs-postgres]restores the seed dump via restore-seed.sh (DROP SCHEMA public CASCADE + pg_restore) before every eval — same class of reset as 05/12/13/14/15
17-issue-158-workspace-isolationneeds-server[needs-postgres]agent-browser + vitest against the live app and robin_ci
17-issue-187-mcp-connector-uxneeds-server[needs-postgres]agent-browser + curl against the live app/server
17-issue-198-passcode-ui-retireneeds-server[needs-postgres]agent-browser flows against the live app
17-issue-306-wiki-type-ghostsneeds-postgres[]source grep + vitest/psql against robin_ci
17-v11-batch-issue-349-detach-guardneeds-server[needs-postgres]agent-browser + curl against the live app/server
18-issue-337-entity-domain-inheritancedestructive[needs-postgres]DROP SCHEMA public CASCADE on robin_ci
18-v11-batch-issue-214-admin-ianeeds-server[needs-postgres]agent-browser + curl against the live app
18-v11-batch-issue-224-composite-reweavedestructive[needs-postgres, needs-server]seeds and mutates shared DB state through the live API
19-issue-187-mcp-connector-uxneeds-server[needs-postgres]curl + vitest against the live server
20-v11-batch-issue-328-first-author-groundingvolatile[needs-model, needs-server, needs-postgres]live OPENROUTER model call through the running pipeline
20-v11-batch-issue-347-inbox-decisionsdestructive[needs-postgres, needs-server]seeds and mutates shared DB state through the live API
21-issue-366-provenance-repairdestructive[needs-postgres]DROP SCHEMA on robin_ci
21-issue-367-custom-type-basevolatile[needs-model, needs-server, needs-postgres]live model call through the running pipeline
22-issue-365-permissions-auditdestructive[needs-server, needs-postgres, needs-model]mutates live-DB membership rows through the server and psql
23-issue-387-mcp-search-scopedestructive[needs-server, needs-postgres]seeds marker signals across workspaces in the shared DB
24-issue-391-hide-root-workspacedestructive[needs-server, needs-postgres]seeds marker signals in the shared DB; agent-browser + curl
25-cr-18-domain-classifier-qualityneeds-postgres[]psql reads against robin_ci; never resets it
26-cr-25-alce-citation-floorneeds-model[needs-postgres]live NLI judge via OpenRouter; psql reads against robin_ci
27-cr-20-extraction-completenessneeds-model[needs-postgres]live NLI judge via OpenRouter; psql reads against robin_ci
28-cr-* (25 pending stubs, one per gated criterion)hermetic[]skip-guard stubs: source grep probes only; always skip, never fail

Derived counts over the 61-plan corpus (recount when plans are added — check-plan-tiers.mjs prints the hermetic/destructive totals on every run, and 00-warden-selftest asserts the actual file count; neither gate pins the numbers written here): tier: hermetic → 29 plans (00, 02, 03, 04, plus the 25 28-cr-* pending stubs). tier: destructive → 14 plans (05, 06, 12, 13, 14, 15, 16, 18-issue-337, 18-v11-batch-issue-224, 20-v11-batch-issue-347, 21-issue-366, 22-issue-365, 23-issue-387, 24-issue-391). tier: volatile → 3, tier: needs-model → 2, tier: needs-server → 7, tier: needs-postgres → 6, tier: needs-redis → 0. Selection by tier:needs-postgres matches 31 plans and tier:needs-model matches 6, because a selector matches a plan whose tier: or requires: names the label.

Why tiers and not caching

When this cut was made the corpus held 15 plans, only 3 of them hermetic — 78 of 397 assertion call sites, about 20% — and those three were pure-grep plans already the fastest in the suite, so a fingerprint-and-replay layer was cut by decision rather than by measurement. The 28-cr-* pending stubs have since tilted the corpus mostly-hermetic without changing the decision: tiers exist so an operator can choose a cheap subset by declared requirement; nothing here lets warden skip work it has already done.

Maintenance

A new plan must arrive with tier: and requires: declared, or check-plan-tiers.mjs fails the run — and with its row added to the per-plan table above (the derived-counts paragraph updated with it), because no gate asserts this file's totals. Changing a plan's blast radius — adding a database call, a live-server dependency, a model call — means updating its tier: (and requires:) to match, not leaving the declaration stale.

The front matter on every plan is exactly these two keys. Adding any other key is a scope decision, not a drive-by edit, and needs a decision recorded in the sprint's context before a plan or the checker references it.