Skip to main content

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.yaml requires contract_diff gate; 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:

FieldTypePurpose
idkebab-case stringMust match the filename without .yaml.
namestringHuman-readable label.
descriptionmulti-line stringWhen this policy fires and what it enforces.
triggered_byarray (โ‰ฅ1 item)One or more triggers; see below.
riskobjectscore (1โ€“10), blast_radius (low/medium/high), reason (โ‰ฅ5 chars).

The optional fields:

FieldPurpose
required_agentsAgents the AI must load when this policy fires (kebab-case names matching .claude/agents/<name>.md).
required_gatesNamed gates with descriptions that must pass before merge.
auto_workflowsNamed actions triggered by conditions on gate output.
human_approval_requiredConditions 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:

  1. Identify files. Reads the proposed file set (from the plan, or from the actual diff if implementation has started).
  2. 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 the bewith@bewith-dev entry in ~/.claude/plugins/installed_plugins.json; inside bewith-docs itself, from the working tree). For each policy file, check whether any triggered_by clause matches the changed-file set of the consumer repo. (agent-guard step 2 has the exact resolution snippet.)
  3. Aggregate matches. For every matched policy:
    • Add each required_agents entry to the load list (de-duped).
    • Add each required_gates entry to the gate list.
    • Take the maximum risk.score.
    • Aggregate auto_workflows by phase.
    • Aggregate human_approval_required clauses.
  4. 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.

PolicyTriggerRiskPrimary gate
api-contract-changecontracts/**, **/*.controller.ts|php, openapi.yaml8 (high)contract-diff + breaking-check
schema-migration**/migrations/**, **/*.migration.{ts,sql}9 (high)migration-dry-run + data-loss-check
auth-touchauth/Cognito modules, JWT, RBAC code9 (high)auth-review + RBAC-impact
legacy-php-changelegacy PHP repos / paths8 (high)scoped-change + human-review
frontend-i18ni18n keys, locale files4 (medium)translation-completeness
production-deploytag push to *-prod-*10 (high)Hard Floor: production_deploy
secret-rotationsecret manager / Cognito secrets10 (high)Hard Floor: secret_rotation
large-refactordiff_size_gt: { files: 20 }6 (medium)configurable: large_refactor
cross-repo-impactcross-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 /continue lives downstream โ€” it belongs in whatever Claude skill or CLI ends up consuming the policies (probably the /continue command'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:

  1. Identify the failure mode the policy guards against. Write one sentence.
  2. Identify the trigger โ€” what file change, event, or diff shape signals the failure mode is being entered. Write the glob/event.
  3. Identify the gate that catches the failure. Write the gate id + description.
  4. Identify the risk score (1โ€“10) and the blast radius. Write the reason.
  5. Scaffold the YAML against the schema. Validate with pnpm validate:policies.
  6. Add a row to policies/index.md.
  7. Open a PR. The validate-pr.yml workflow re-runs the validator.

Naming: the filename must equal the id field (kebab-case, ends in alphanumeric). The validator enforces this.

See alsoโ€‹