Write a principle
A principle is one enforceable rule: when it applies, what it requires, by when, and who may attest it. This guide encodes a real rule end to end:
A change that rewrites persisted data needs an approved migration plan before the code is written, and something other than the agent has to have run the recovery path before it is finished.
For the full syntax, see the document reference.
The commands run aep against a copy of this repository's protocols/, principles/,
workflows/, profiles/, artifacts/ and drivers/ with the two documents below added to it,
which is what every count in the output is a count of. The task is the one from
Govern a task, under the new profile:
# task.yaml
id: BILL-88
kind: feature
objective: move-invoices-to-partitioned-table
protocol: adp/1
profile: acme.service
derived_from:
- story:BILL-87
manifest: artifacts.yaml
constraints:
facts:
change.database_schema: true
change.public_contract: false
task-rename-a-button.yaml is the same task as BILL-89 with change.database_schema: false.
artifacts.yaml declares the story, an approved specification that specifies it, and an approved
migration-plan. artifacts-without-the-plan.yaml leaves the plan out. red-test.yaml is the
worked example's failing-test record.
Four decisions, in order
| Decision | Question | In the document |
|---|---|---|
| Applicability | when is this rule even about this task? | applies_when: |
| Timing | at which point does it bite? | before_<phase>: keys under requires: |
| Obligation | what must be true or exist? | predicates:, artifacts: |
| Evidence | who is allowed to say so? | evidence: with independent: true |
More columns: swipe horizontally, or focus the table and use the arrow keys.
The document
principles/migration-has-a-way-back.yaml:
id: migration-has-a-way-back
version: 1
title: A schema migration has a way back
summary: >-
A change that rewrites persisted data is not finished until something other
than the agent has run the down path. Without it the recovery plan gets
written during the incident, by whoever is awake.
applies_when:
# A fact nothing can observe, so the task declares it. The alternative —
# applying the rule to every task — gets it removed within a month.
change.database_schema: true
requires:
before_implementation:
artifacts:
- kind: migration-plan
status: approved
before_completion:
predicates:
- verification.recovery.passed
evidence:
# The agent's own report of a successful rollback rehearsal does not
# satisfy this.
- kind: verification
independent: true
And a profile that puts it in force:
id: acme.service
version: 1
title: Acme service development
summary: Standard development, plus Acme's migration rule.
protocol: adp/1
extends: development.standard
principles:
- migration-has-a-way-back
Verify it does what you meant
Applicability. Two tasks under the same profile — one declares change.database_schema: true,
the other declares it false:
$ aep govern resolve --root . --task task.yaml | grep -E '^(task|principles|obligations)'
task BILL-88 (feature)
principles spec-driven, test-driven, static-analysis, least-privilege, provenance-tracking, contract-testing, property-based-testing, approval-gates, reversible-changes, migration-has-a-way-back
obligations 13
$ aep govern resolve --root . --task task-rename-a-button.yaml | grep -E '^(task|principles|obligations)'
task BILL-89 (feature)
principles spec-driven, test-driven, static-analysis, least-privilege, provenance-tracking, contract-testing, property-based-testing, approval-gates, reversible-changes
obligations 11
The rule is absent from the second task, not present and vacuously satisfied — so nobody reads a green report and wonders which ticks meant anything.
Saying nothing is not the same as saying no. Leave change.database_schema out of the second
task entirely and the rule stays in force, all thirteen obligations with it. An applicability condition
the engine cannot evaluate resolves to applies
(crates/govern/aep-domain/src/principle.rs:688), because a rule that can rule itself out by silence is a
rule nobody can rely on. Opting out is a declaration a reviewer can see.
Timing. With the migration plan in the manifest, a failing test carries the task into implementation. Remove the plan from the manifest and the same evidence stops one state short:
$ aep govern evaluate --root . --task task.yaml --artifacts artifacts-without-the-plan.yaml \
--evidence red-test.yaml --advance | grep -E '^(state|transitions)| -> implement|migration-plan'
state establish_verifiers (Establish verifiers)
transitions
establish_verifiers -> implement [blocked]
? artifact migration-plan (approved) — no migration-plan artifact is declared [principle migration-has-a-way-back]
The block names the principle. Somebody can read it and either write the plan or argue with the rule — both better outcomes than an agent quietly writing the migration.
The two authoring failures worth meeting early
1. A fact the protocol does not declare. Suppose the rule had reached for
migration.rollback_tested, which reads perfectly well in English:
$ aep govern validate --root .
62 file(s): 5 protocol(s), 25 principle(s), 6 workflow(s), 11 profile(s), 13 lifecycle(s), 2 step map(s)
1 problem(s):
- [unobservable_fact] principle migration-has-a-way-back.obligations.migration-has-a-way-back/before-completion: `migration.rollback_tested` is not declared observable by protocol adp/1 (hint: declared families: ess_conformance.**, trace_conformance.**, mutation.**, differential.**, invariant.**, clean_room.**, build.**, types.**, task.**, change.**, risk, severity, state.**, workflow.**, principle.**, evidence.**, required_evidence.**, tests.**, test.**, unit_tests.**, contract_tests.**, regression_suite.**, static_analysis.**, contracts.**, property_test.**, coverage.**, specification.**, diff.**, source_diff.**, artifact.**, review.**, verification.**, approval.**, approvals.**, deployment.**, metric.**, service.**)
$ echo $?
1
migration.** does exist — but only under aop/1 (operations), and this principle runs under
adp/1. The error lists every family that is declared, which is usually enough to find the right
spelling — here, verification.recovery.passed.
2. A declared family, but a spelling nothing projects. This one passes validation and then never becomes true:
$ aep govern validate --root .
62 file(s): 5 protocol(s), 25 principle(s), 6 workflow(s), 11 profile(s), 13 lifecycle(s), 2 step map(s)
valid
$ aep govern evaluate --root . --task task.yaml | grep passsed
? verification.recovery.passsed [principle migration-has-a-way-back]
unobserved: verification.recovery.passsed
A ? that never becomes ✓ is a task nobody can finish, and it looks like a stuck agent rather
than a typo. Check every new predicate against the
projected-facts table before writing the
rest of the rule.
Rules you are held to, and where each one bites
before_<phase>keys use_for-in phase names:before_verification_setup→ phaseverification-setup. A phase no state declares is caught at resolve, not at validate — no workflow is known until a profile picks one — as[unknown_phase] workflow adp/default: an obligation is timed against phase `nonsense-phase`, which no state declares, with the declared phases listed beside it.- Requirements with no stated timing default to before completion.
- A principle must enforce something. A document with only a title is refused at validate as
[empty_declaration] principle empty-rule: declares no obligations, evidence, verification or capability policy, so it cannot change any outcome. - A principle's
capabilities:may only take away.denyandrequire_approvalare applied; anallow:parses, validates and is then ignored — a principle restricts, and granting is a profile's or a protocol's job (crates/govern/aep-domain/src/capability.rs:630). Addingallow: [secret.read]to the rule above leavesdenied secret.readin the resolved plan. - Extending a profile can only make completion harder: conditions are conjoined —
acme.serviceinheritsdevelopment.fast's completion condition throughdevelopment.standardand reports both, separately, inevaluate— and a denial cannot be granted back.