Policy Engine
The Policy Engine is the layer that decides what gates apply to a given task. It is declarative (YAML), testable (JSON Schema), and auditable (every decision is reproducible from the file set).
This page is the focused explainer. The full strategic context is in ai-engineering-strategy.md ยง9. The live policies are in policies/.
Why a Policy Engineโ
Without an explicit engine, "what gates apply when" lives implicitly in the AI's behavior. Different sessions interpret the same situation differently. A schema change in one session triggers a migration check; in another session, it doesn't. The result is non-determinism, even with detailed agent files.
With an explicit engine:
- The gates are visible. A developer reads the YAML and predicts the AI's behavior.
- A CI workflow can dry-run policies against a PR diff and show what would happen.
- Failed gates produce comprehensible errors. "Policy
api-contract-change.yamlrequirescontract_diffgate; it failed because X." - Risk lives in the policy, not in the agent. Each policy outputs a
risk.score; the score determines whether human approval is required regardless of the configurable autonomy setting.
This is principle 8 from deterministic-ai.md: The Policy Engine decides; the AI follows.
Anatomy of a policyโ
A policy is a YAML file in /policies. It is validated at PR time against policies/schema.json by scripts/validate-policies.mjs โ Ajv + js-yaml, wired into validate-pr.yml.
The required fields:
| Field | Type | Purpose |
|---|---|---|
id | kebab-case string | Must match the filename without .yaml. |
name | string | Human-readable label. |
description | multi-line string | When this policy fires and what it enforces. |
triggered_by | array (โฅ1 item) | One or more triggers; see below. |
risk | object | score (1โ10), blast_radius (low/medium/high), reason (โฅ5 chars). |
The optional fields:
| Field | Purpose |
|---|---|
required_agents | Agents the AI must load when this policy fires (kebab-case names matching .claude/agents/<name>.md). |
required_gates | Named gates with descriptions that must pass before merge. |
auto_workflows | Named actions triggered by conditions on gate output. |
human_approval_required | Conditions that force a human in the loop, beyond the configurable autonomy. |
Trigger typesโ
Three kinds of trigger are accepted by the schema:
triggered_by:
- file_change: "contracts/**" # glob on changed file paths
- event: "schema.modified" # named event from events/vocabulary.md
- diff_size_gt: { files: 20 } # diff exceeds N changed files
Multiple triggers on the same policy are OR-ed: any single match fires the policy.
human_approval_required formsโ
Three forms, picked per condition:
human_approval_required:
- always: true # this policy ALWAYS requires approval
- hard_floor: production_deploy # link to autonomy/defaults.yaml id
- condition: "risk.score >= 7" # predicate over gate outputs
The hard_floor form is the bridge between the Policy Engine and the Autonomy Model: if a policy declares a Hard Floor binding, the autonomy gate becomes unoverridable, even by a per-user ~/.claude/user-overrides.yaml.
How policies runโ
When the AI enters step 4 of /continue (Evaluate Policies), the engine:
- Identify files. Reads the proposed file set (from the plan, or from the actual diff if implementation has started).
- Match triggers. The policy definitions ship in the plugin โ in a consumer repo they are read from the installed plugin's
policies/directory (resolved via thebewith@bewith-deventry in~/.claude/plugins/installed_plugins.json; inside bewith-docs itself, from the working tree). For each policy file, check whether anytriggered_byclause matches the changed-file set of the consumer repo. (agent-guardstep 2 has the exact resolution snippet.) - Aggregate matches. For every matched policy:
- Add each
required_agentsentry to the load list (de-duped). - Add each
required_gatesentry to the gate list. - Take the maximum
risk.score. - Aggregate
auto_workflowsby phase. - Aggregate
human_approval_requiredclauses.
- Add each
- Return the decision. The AI executes: loads the agents, runs the gates, defers the auto-workflows to their phase, requests approval if required.
A "match nothing" outcome is valid โ the AI proceeds with the default agent set and the default gates.
The nine starter policiesโ
Each is a normative library entry consumer repos adopt and tune.
| Policy | Trigger | Risk | Primary gate |
|---|---|---|---|
api-contract-change | contracts/**, **/*.controller.ts|php, openapi.yaml | 8 (high) | contract-diff + breaking-check |
schema-migration | **/migrations/**, **/*.migration.{ts,sql} | 9 (high) | migration-dry-run + data-loss-check |
auth-touch | auth/Cognito modules, JWT, RBAC code | 9 (high) | auth-review + RBAC-impact |
legacy-php-change | legacy PHP repos / paths | 8 (high) | scoped-change + human-review |
frontend-i18n | i18n keys, locale files | 4 (medium) | translation-completeness |
production-deploy | tag push to *-prod-* | 10 (high) | Hard Floor: production_deploy |
secret-rotation | secret manager / Cognito secrets | 10 (high) | Hard Floor: secret_rotation |
large-refactor | diff_size_gt: { files: 20 } | 6 (medium) | configurable: large_refactor |
cross-repo-impact | cross-repo events (planned) | 7 (medium) | awareness comment in linked repos |
Catalog: policies/index.md.
The event vocabularyโ
Policies are triggered by file_change globs, diff_size_gt thresholds, or by named events. The events are defined in events/vocabulary.md โ a markdown reference, not an event bus.
Examples:
file.changed
schema.modified
endpoint.added
endpoint.signature_changed
migration.created
secret.referenced
auth.touched
i18n.string_added
deploy.requested
pr.cross_repo_overlap
The mapping from events to policies is the YAML in each policy file. We deliberately avoid building or adopting a real event bus at this scale; the team is one team and the events come from a small number of well-known sources (git, CI, ClickUp). When scale demands it (50+ services), a real event bus may be appropriate. Until then, the vocabulary document plus YAML policies handle the same need at one percent of the complexity.
The engine is here; the runtime is downstreamโ
A deliberate architectural deviation from v0.4 (see ai-engineering-strategy.md "Implementation notes"):
- The policies live here, as YAML + JSON Schema validator +
validate-policies.mjs. - The engine that evaluates triggers at step 4 of
/continuelives downstream โ it belongs in whatever Claude skill or CLI ends up consuming the policies (probably the/continuecommand's implementation).
Policies in this repo are a normative library, not a running service. A consumer repo adopts the policies that apply to its tech stack and overrides values where needed. The runtime that reads them is owned by a different layer.
Adding a new policyโ
Workflow:
- Identify the failure mode the policy guards against. Write one sentence.
- Identify the trigger โ what file change, event, or diff shape signals the failure mode is being entered. Write the glob/event.
- Identify the gate that catches the failure. Write the gate id + description.
- Identify the risk score (1โ10) and the blast radius. Write the reason.
- Scaffold the YAML against the schema. Validate with
pnpm validate:policies. - Add a row to
policies/index.md. - Open a PR. The
validate-pr.ymlworkflow re-runs the validator.
Naming: the filename must equal the id field (kebab-case, ends in alphanumeric). The validator enforces this.
See alsoโ
- ai-engineering-strategy.md ยง9 โ the strategic case + worked policy example.
- deterministic-ai.md principle 8 โ the philosophical framing.
- autonomy-model.md โ how
human_approval_required: { hard_floor: ... }ties policies to unoverridable Hard Floor items. policies/โ the live YAML library.policies/schema.jsonโ the validation schema.events/vocabulary.mdโ the event names policies trigger on.