Skip to content

feat: give a rule a lifecycle, an owner and a denial that says what would pass - #78

Merged
sstriker merged 5 commits into
mainfrom
feat/rule-lifecycle
Nov 14, 2026
Merged

sstriker merged 5 commits into
mainfrom
feat/rule-lifecycle

Conversation

@sstriker

@sstriker sstriker commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

This is package 6 of PLAN.md: the rule lifecycle of section 7 of the paper.

The paper gives each rule an owner, a review date and a status. It gives a ceiling on enforced rules, a reset, a denial that says what would pass, and three tools. The code had none of these. A rule was enforced when a person verified it, and that was all.

What changes

  • Status. The policy block has status, owner, review_by, core and signed_off_by. A rule is proposed, advisory, enforced or retired. A rule is in a decision only when a person verified the version that says so. amem loads no module from an entry below verified.
  • Advisory. Each advisory module runs in an engine of its own, apart from the modules of enforced rules. The decision and the decision log have advisories: what those modules would refuse. An advisory module refuses nothing, whatever rule its denial names.
  • Denial. A denial can give code and remedy. The refusal shows the rule, the reason, the code and what would pass.
  • Ceiling. [gates] max_enforced_rules is a ceiling on the enforced rules of one domain, counted from the oldest entry. A rule over the ceiling is advisory until a person signs it off. The ceiling never holds back a rule of the L0 core.
  • Core. Only an L0 rule in the marked core takes no exception.
  • Tools. check_policy, file_exception and propose_amendment are memory tools and commands (amem policy check, except, amend). A session reads a rule as it reads an entry.
  • Reset. amem policy reset retires each rule of a domain that no person wrote, curated or approved. Only the owner runs it.
  • Lint. The lint reports a rule with no owner, a rule after its review date, a rule over the ceiling and a rule that refused nothing. The lint, the rates and the filing of findings are now in the library.
  • Demo. Act 3 shows the steps of a rule, a refusal, a check, an amendment, an exception and a reset. docs/demo-narrative.md, local/README.md, docs/terms.md, README.md and PLAN.md say the same.

A decision for the reviewer

A step from advisory to enforced is a new version of the entry. The new version supersedes the old version. Until a person verifies the new version, the rule is in no decision. The alternative is that a verified rule stays in force until a person verifies its next version. That alternative changes what "supersedes" means for a policy entry, so it is not in this change. README.md and PLAN.md list it as open.

Not in this change

README.md ("The rule lifecycle, in part") and row 6 of PLAN.md list what is open:

  • check_policy does not select rules by the action.
  • The lint looks at the full decision log, not at one review period.
  • amem does not check that the person who approves an exception is the approver that the answer names.
  • A new version of a rule is the newest rule under the ceiling. Thus, when the oldest rule under a full ceiling gets a new version, a different rule becomes enforced.
  • When the module of an enforced rule loads and then fails in a decision, that decision fails. main has the same behaviour.
  • An imported rule keeps the author and the date of its entry, and the reset and the ceiling use them.
  • A session loads the policy when it starts. The branch for package 10 changes this.

Checks

make check passed with 990 tests on 9d0d6d6. make corpus-check passed. Act 3 ran on a scratch record with the debug binary and printed each step. I did not run the act with a gateway key, so its auditor step and its spend step did not run.

Each new assertion for a fix failed when I removed that fix: the code fence check, the withheld text, the entry in force for a rule, the curation and approval rule of the reset, and the exception that the reset must not touch.

Adversarial pass

A first pass ran on bfae3c4. It confirmed 13 findings, and 30d4780 answers them:

  1. The words of file_exception could carry a policy block and a module into the entry that a person verifies. Now a request with a code fence is refused, and the entry must read back as the exception that was asked for.
  2. An advisory module could refuse, because the decision trusted the id in its denial. A denial with a field that was not a string made each decision fail. Now advisory modules run apart, and a denial reads such a field as its JSON.
  3. A second entry with the id of an enforced rule and status = "advisory" switched that rule off. Now the id of a denial does not select a status.
  4. The ceiling counted by id, so a new rule could push out an older rule or a rule of the L0 core. Now it counts from the oldest entry and never holds back the core.
  5. signed_off_by accepted any text. Now it must name a person.
  6. check_policy and propose_amendment gave the text of a rule above the handling of the session. Now the text is withheld, and the amendment is not filed.
  7. An entry that nobody reviewed could speak for a rule with the same id. Now the tools use the entry in force.
  8. The reset kept a rule when a person proposed it and nobody approved. Now it counts a curation or an approval, and its answer names entries.
  9. T0 showed an L0 rule that nobody verified as core. Now it does not.
  10. The owner cannot approve an exception that the owner filed. The text now says that a different person approves.
  11. lint --file filed a task for a rule that nobody verified. The held-back finding is gone.
  12. Five sentences did not agree with the code. The text or the code changed. An exception with status = "retired" is no longer in force.
  13. Ten mutations left all tests green. New tests hold the lint findings, the reset with an exception, the ceiling, the withheld text and the duplicate id. No test runs the four new commands of amem policy through the binary.

The pass named these as plausible, and they are open: the reset stops at the first write that fails and does not give what it retired, and the reset raises the label of the session when it reads a restricted rule.

A second pass ran on 30d4780. It attacked each fix. It found that five of nine fixes did not hold in full, and 9d0d6d6 answers what it confirmed:

  1. A denial that was not an object, or whose rule was not a string, still made each decision fail. The first case was new on this branch. Now the decision reads each denial and each cited id as text.
  2. T0 gave the text of a restricted rule to a session with a public cap. Now the digest gives only the id of such a rule.
  3. One advisory module that failed took the report of each other advisory rule with it. Now each advisory module has its own engine.
  4. An entry that nobody verified spoke for a rule when no entry was in force. Now such a rule is not found.
  5. The bound of one amendment for each rule and filer did not hold while a person worked on the first task. Now it holds for each task that is not done.
  6. check_policy and propose_amendment raised the label of the session from entries and tasks that they did not give. Now the label rises only to the rules given.
  7. A scope with a new line, or a justification with an HTML comment, could mislead the person who verifies an exception. Now the words are one line with no <.
  8. with_status changed the first copy of the block text in the body, not the block. Now it changes the block and checks the result.
  9. The version of the policy did not change when a rule went from advisory to enforced. Now it does.
  10. A person who completed a proposal task and did not approve counted as the approver for the reset. Now the entry must be verified.
  11. Six sentences of local/README.md did not agree with the code. The text changed.

Open from the second pass:

  • No test holds these: the read-back check of an exception, the domain of the entry of an exception, the Enforced filter on the policies that an exception can lift, and the date of an entry as the order under the ceiling.
  • lint --file gives each finding the text of a conflict between layers.
  • The ceiling is for one domain, but a rule of layers 0 to 2 applies in each domain.
  • The three items that this pull request lists above as not in this change.

A third pass ran on ba8eeb8 (9d0d6d6 with main merged in). It confirmed these defects, and a25307a fixes them. Each fix has a test that fails without it.

  • An agent that wrote 1000 raw policy entries put a verified rule out of every decision, because the policy was compiled from the newest 1000 policy entries. The policy is now compiled from every policy entry.
  • A refusal gave the reason, the code and the remedy of a rule to a session that could not read the rule's entry, and the gate task held the same text. Such a session now gets the rule's id only.
  • A denial that gave no policy was filed as a blast-radius gate and named nothing. It is now a policy gate, and the refusal says that the rule names no policy.
  • The same exception asked twice wrote two entries with one policy id. The second request is now refused.
  • A rule of the L0 core at advisory was not in T0. T0 now lists it.
  • No test held that an entry nobody reviewed cannot mark a rule as core. One does now.

The third pass also found assertions that pass when the code is wrong. These are still open, with no test: the label rises only for the rules that check_policy gives; a core rule above the handling is withheld in T0; an amendment on a blocked task counts as not done; a curator below the tier of a promotion does not count as a person; one row for two live entries with one id; the read-back checks of file_exception and with_status; the module's id in the policy version.

A denial that names no policy is not counted for its module in the rates and the lint. PLAN.md lists that.

No pass ran on a25307a. Three passes ran, and each found defects in the revision before it.

make check failed once on scripts/conformance.test.sh (case missing-tool, "list is not newest first"), then the script passed six times. The fake server orders its list by wall-clock time. That flaky test is not from this change, and a separate branch fixes it.

Posted by Claude on behalf of @sstriker

🤖 Generated with Claude Code

sstriker and others added 4 commits November 12, 2026 13:58
…ould pass

This is package 6 of PLAN.md, section 7 of the paper.

- The policy block has `status`, `review_by`, `core` and `signed_off_by`.
  A rule is proposed, advisory, enforced or retired. A rule is in a
  decision only when a person verified the version that says so. The
  policy loads no module from an entry below verified.
- An advisory rule reports what it would refuse and refuses nothing. The
  decision and the decision log have `advisories` and `denials`.
- A denial can give a reason code and a remedy. The refusal shows them.
- `[gates] max_enforced_rules` is a ceiling on the enforced rules of one
  domain. A rule over the ceiling is advisory until a person signs it off.
- Only an L0 rule in the marked core takes no exception.
- The memory tools `check_policy`, `file_exception` and
  `propose_amendment`, and the commands `amem policy check`, `except`,
  `amend` and `reset`. The reset retires each rule of a domain that no
  person wrote, promoted or approved.
- The lint reports a rule with no owner, a rule after its review date, a
  rule over the ceiling and an unused rule.
- Act 3 shows the steps, a refusal, an amendment, an exception and a
  reset.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- An advisory module could refuse. Its denial could name a different
  rule, and the decision used that name. The modules of advisory rules
  now run in an engine of their own, and their denials are only reported.
- A second entry with the id of an enforced rule and `status =
  "advisory"` switched that rule off. The id of a denial no longer
  selects a status.
- A denial with a field that is not a string made each decision fail.
  A denial now reads such a field as its JSON.
- The words of `file_exception` could carry a policy block and a module
  into the entry that a person verifies. A request with a code fence is
  refused, the entry must read back as the exception that was asked for,
  and the entry goes in the domain of the rule.
- The ceiling counted by id, so a new rule could push out an older rule
  or a rule of the L0 core. It now counts from the oldest entry, never
  holds back the core, and accepts a sign-off only from a person.
- `check_policy` and `propose_amendment` gave the text of a rule above
  the handling of the session. A rule is now read as an entry is.
- An entry that nobody reviewed could speak for a rule with the same id.
  The tools now use the entry in force.
- The reset kept a rule when a person proposed it and nobody approved.
  It now counts a curation or an approval. Its answer names entries.
- T0 showed an L0 rule that nobody verified as core.
- An exception with `status = "retired"` stayed in force.
- One filer has one open amendment for one rule.
- The lint, the rates and the filing of findings are in the library. The
  `held-back` finding is gone, because `inert` says the same.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- A denial that is not an object, or whose rule is not a string, made
  each decision fail. The decision now reads each denial and each cited
  id as text.
- All advisory modules ran in one engine, so one module that failed took
  each other report with it. Each advisory module now has its own engine.
- The version of the policy now says which modules refuse and which
  report.
- T0 gave the text of a rule above the handling of the session. The
  digest now gives only the id of such a rule.
- `check_policy` and `propose_amendment` raised the label of the session
  from entries and tasks that they did not give. The label now rises only
  to the rules given.
- An entry that nobody verified could speak for a rule when no entry was
  in force. Such a rule is now not found.
- The bound of one amendment for each rule and filer now holds while a
  person works on the first task.
- The words of an exception are one line with no markup.
- `with_status` now changes the policy block where it is, and checks that
  the result reads back.
- The reset counts an approver only when the entry is verified.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Main has the attempt keys of #77. The plan keeps the rows of both, and
the MCP tests of both stand.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…s text in a refusal

A third adversarial pass on ba8eeb8 found these defects. Each fix has a test
that fails without it.

- The policy was compiled from the newest 1000 policy entries. An agent that
  wrote 1000 raw policy entries put a verified rule out of every decision.
  The policy is now compiled from every policy entry. `check_policy` and the
  T0 digest also read every policy entry.
- A refusal gave the reason, the code and the remedy of a rule to a session
  that could not read the rule's entry. The task that the refusal filed held
  the same text below the rule's label. For such a session, the refusal now
  gives the rule's id only. The decision log keeps the full denial.
- A denial that gave no `policy` was filed as a blast-radius gate and named
  nothing. It is now filed as a policy gate, and the refusal says that the
  rule names no policy.
- The same exception asked twice wrote two entries with one policy id and two
  approval tasks. The second request is now refused and names the first entry.
- A rule of the L0 core at advisory was in `check_policy` and not in T0. T0
  now lists it.
- A test now holds that an entry nobody reviewed cannot mark a rule as core.
- The comment on `max_enforced_rules` said that rules are counted by id. They
  are counted oldest entry first.

Not fixed, and listed in PLAN.md: a denial that names no policy is not
counted for its module in the rates and the lint.

`make check` failed once on `scripts/conformance.test.sh` (missing-tool:
"list is not newest first"). The script then passed six times. The fake
server orders its list by wall-clock time; that is a separate defect.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5DyJRpu5ned5y3d92U7Ts
@sstriker
sstriker marked this pull request as ready for review November 14, 2026 16:41
Copilot AI balanced review requested due to automatic review settings November 14, 2026 16:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@sstriker
sstriker merged commit 498dacb into main Nov 14, 2026
10 checks passed
@sstriker
sstriker deleted the feat/rule-lifecycle branch November 14, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants