Warden tiers
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.
Seven labels, defined operationally — by what a plan does, not by a guess:
hermetic — reads only files already in the checkout. No DB, no server, no model, no network.needs-redis — talks to a live Redis instance.needs-postgres — talks to a live Postgres instance.needs-server — requires a live server and/or app reachable on $SERVER_URL/$APP_URL.needs-model — makes a real LLM or embedding call.destructive — mutates state another plan reads. Includes any DROP SCHEMA, pushTestSchema(), or database create/drop.volatile — can legitimately go red on unchanged inputs. Network- or time-sensitive.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].
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.
| Plan | tier: | requires: | Evidence |
|---|---|---|---|
| 00-warden-selftest | hermetic | [] | executes warden's own selftest scripts + the tier gate over files in the checkout; no external service |
| 01-greenlight-gates | volatile | [] | pnpm audit:gate is network/time-sensitive; regenerates server/openapi.json, judges its own diff, reverts it |
| 02-foundation-substrate | hermetic | [] | source grep only; no env file sourced |
| 03-connect | hermetic | [] | source grep only |
| 04-permissions | hermetic | [] | source grep/rg only |
| 05-collapse | destructive | [needs-postgres] | DROP SCHEMA CASCADE on shared robin_ci |
| 06-socrates | destructive | [needs-model, needs-server, needs-postgres] | DROP SCHEMA, npx agent-browser, OPENROUTER |
| 07-wiki-types | needs-postgres | [] | vitest against robin_ci |
| 08-workspace-reads | needs-postgres | [] | vitest against robin_ci |
| 09-d30-gate | needs-postgres | [] | vitest against robin_ci |
| 10-app-shell | needs-server | [needs-postgres] | curl + agent-browser against live app/server |
| 11-capture | needs-postgres | [] | vitest against robin_ci |
| 12-ask-edit | destructive | [needs-postgres] | pushTestSchema() |
| 13-bug-sweep | destructive | [needs-postgres] | DROP SCHEMA; resets because 06 leaves state behind |
| 14-member-authoring-governance | destructive | [needs-postgres] | DROP SCHEMA |
| 15-v1.2-payload | destructive | [needs-postgres] | provisions and drops throwaway mig_test |
| 16-corpus-evals | destructive | [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-isolation | needs-server | [needs-postgres] | agent-browser + vitest against the live app and robin_ci |
| 17-issue-187-mcp-connector-ux | needs-server | [needs-postgres] | agent-browser + curl against the live app/server |
| 17-issue-198-passcode-ui-retire | needs-server | [needs-postgres] | agent-browser flows against the live app |
| 17-issue-306-wiki-type-ghosts | needs-postgres | [] | source grep + vitest/psql against robin_ci |
| 17-v11-batch-issue-349-detach-guard | needs-server | [needs-postgres] | agent-browser + curl against the live app/server |
| 18-issue-337-entity-domain-inheritance | destructive | [needs-postgres] | DROP SCHEMA public CASCADE on robin_ci |
| 18-v11-batch-issue-214-admin-ia | needs-server | [needs-postgres] | agent-browser + curl against the live app |
| 18-v11-batch-issue-224-composite-reweave | destructive | [needs-postgres, needs-server] | seeds and mutates shared DB state through the live API |
| 19-issue-187-mcp-connector-ux | needs-server | [needs-postgres] | curl + vitest against the live server |
| 20-v11-batch-issue-328-first-author-grounding | volatile | [needs-model, needs-server, needs-postgres] | live OPENROUTER model call through the running pipeline |
| 20-v11-batch-issue-347-inbox-decisions | destructive | [needs-postgres, needs-server] | seeds and mutates shared DB state through the live API |
| 21-issue-366-provenance-repair | destructive | [needs-postgres] | DROP SCHEMA on robin_ci |
| 21-issue-367-custom-type-base | volatile | [needs-model, needs-server, needs-postgres] | live model call through the running pipeline |
| 22-issue-365-permissions-audit | destructive | [needs-server, needs-postgres, needs-model] | mutates live-DB membership rows through the server and psql |
| 23-issue-387-mcp-search-scope | destructive | [needs-server, needs-postgres] | seeds marker signals across workspaces in the shared DB |
| 24-issue-391-hide-root-workspace | destructive | [needs-server, needs-postgres] | seeds marker signals in the shared DB; agent-browser + curl |
| 25-cr-18-domain-classifier-quality | needs-postgres | [] | psql reads against robin_ci; never resets it |
| 26-cr-25-alce-citation-floor | needs-model | [needs-postgres] | live NLI judge via OpenRouter; psql reads against robin_ci |
| 27-cr-20-extraction-completeness | needs-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.
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.
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.