You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Kernel: a change to the Factory's own rules or a Lens's inputs needs a person #113
Part of the epic "Epic: slice 0, foundations. The repo holds the line, its context is tested, and the platform facts are recorded".
Why. The security pass on "A Lens run by GitHub Actions is read only from its own workflow file" (#97) found that nothing the Kernel reads stops a pull request that changes the Factory's own rules, or the inputs of a Lens, from being approved and enqueued with no person:
The facts carry no list of the paths a pull request changes, so the Kernel cannot see that .github/, lifecycle/, config/, the hooks, kernel/gate.py, kernel/brakes.py or a Lens's inputs (Makefile, tools/, tests/, .gitleaks.toml) were touched.
Dependabot's security updates are enabled and edit workflow pins. Such a pull request is class dependency, skips the model review, and is judged by the deterministic Lenses alone, one of which is the file it changes.
Design §5.1 makes the Factory's own rules the Maintainer's. Today that is held only by the session hooks, locally.
Done when
The facts carry the paths the pull request changes (read in full, with a complete flag), and the configuration names the protected paths and each Lens's inputs, from one list shared with config/hooks.json.
A pull request that changes a protected path or a Lens's inputs converges only once every model Lens has reviewed its current head, whatever its class; the machine user then approves it as usual (amended by the Maintainer, 2026-10-16: no person; see the rulings below). A queued one whose head moves leaves the queue. A dependency update is no exception. An unreadable list of paths holds for the Maintainer (needs_you).
A boundary test and a mutant for each; the simulator makes such changes, by a Role, by Dependabot and by a person.
Decision 0018's row on a change to a Lens's own workflow file is given its check.
Solution Architect, delegated; rule 3 and the load decided by the Maintainer. Builds on #114 (decision 0018, merged) and #33's dequeue.
1. The paths are a fact. New in schemas/facts.schema.json: changed_paths, every path the pull request changes, each once, from the top of the repo. A rename or copy gives both its old and new path; a deletion gives its old one. New flag complete.changed_paths, added to READ_IN_FULL in kernel/terms.py. A flag that is not exactly true gives facts_incomplete, and a queued item leaves the queue (#33). A list read in full can still be unreadable: not a list of strings, or a path that is empty, has a leading or trailing /, or a . or .. name. That gives the new paths_unreadable, in NEEDS_YOU_REASONS, read where review_unreadable is. Why: the Kernel cannot hold what it cannot see, and a misread list must stop the item.
2. One list, in config/hooks.json.protected gains config/apps.json. A new key, lens_inputs, gives {lens, path, is}:
verify: Makefile, plus CHECKS from tools/check_context.py;
the secret scan: .gitleaks.toml.
The workflow files are already under .github/, and CodeQL has no input in the repo. The syntax is what schemas/hooks.schema.json already allows: a final / means everything below it, and * matches part of one name. Matching ignores case, so it can hold more, never less. One new function beside load() in tools/hooks/protected.py gives the Kernel config protected_paths, the patterns without is, which is prose. tests/support.py and sim/world.py both call it. The session hooks treat lens_inputs as reported. New tests in tests/test_tools.py hold this:
the Kernel's matcher and protected.entries() agree on one corpus;
the config carries every entry;
each lens is a required deterministic Lens, and verify's inputs cover CHECKS;
each 0018 workflow is protected.
Why: with one file, only the two matchers can drift, and a test pins them.
3. What lets such a change converge: pr-review's review of this head.Decided by the Maintainer, 2026-10-16. No person is needed. A change that touches the list converges only once every model Lens has reviewed its current head, as any change is reviewed, and the approval is then submitted by the machine user (donpetry-bot, the configured approver), as it is for every item. In required_model_lenses() in kernel/derive.py, a change that touches the list needs every model Lens whatever its class: model_not_required_for does not apply. A session pushing as the Maintainer can never approve its own change, since the approval comes from another account, and the machine user never pushes (§5.1, I-26). No new approval fact and no self-push exception. Why: the gap is a change judged by nothing but the Lens it edits. A review by another model, bound to the content, closes it without stopping each change for a person.
4. Without it. No new Hold: an item whose model review of this head is missing waits for it as any item does (waiting on the review), and a queued one whose head changes already leaves the queue (#33). Only an unreadable list stops the item for a person (rule 1, paths_unreadable, needs_you). The decision names the matched configured patterns, never a changed path. Why: the review is the gate, so the gate's own machinery is enough.
5. No class is excused. A Dependabot pin update to .github/workflows/verify.yml is reviewed by pr-review before it converges. model_not_required_for still excuses a dependency update that touches nothing on the list. Why: the Lens it edits is otherwise its only judge.
6. Records. The next free number is 0020 (0019 is the session hooks' bypass, decided the same day): docs/decisions/0020-a-change-to-the-factorys-own-rules-or-a-lenss-inputs-is-reviewed-by-a-model-lens.md, new, replacing nothing. In 0018's workflow-file row, only the Check cell changes, as tools/check_decisions.py allows. It names test_a_lens_input_counts_like_a_protected_path, and the refusal stays waiting on harness/. Design §4.4 gains I-45.
7. Simulator. Touched paths draw from random.Random("paths %d" % seed), so the fixed seeds stay as they were. Three actors make such changes: a Role's fix Run touches tests/, Dependabot edits .github/workflows/verify.yml, and the Maintainer pushes config/defaults.json. World.invariants() and approve() assert I-45: nothing touching the list is approved, enqueued or merged without every model Lens's review of that head, whatever its class. tests/test_simulator.py checks the seeds reach every case.
8. Boundary tests (tests/test_decide.py) and mutants.
Test
Mutant it kills
test_a_change_to_a_protected_path_needs_the_model_review (README.md on a dependency item converges without it)
"Harness: apply refuses a Role's push to a protected path or a Lens's input that no model Lens has reviewed".
"Run each deterministic Lens from the base's inputs, so a change cannot weaken the Lens that judges it".
"A model Lens's inputs: its manifest and prompt" (waits on roles/).
"Consumer entry: its protected_paths in this syntax".
Decided by the Maintainer, 2026-10-16.
Rule 3: no person and no self-push exception. pr-review's review, approved by donpetry-bot, is the gate; a session pushing as the Maintainer cannot approve itself.
The load: accepted. With rule 3 as decided, it is small: every change but a dependency update already needs the model review, so only those touching the list wait for it. "Run each deterministic Lens from the base's inputs" comes straight after this story.
Activity
don-petry commentedon Oct 16, 2026
Working rulings (overturnable in review)
Solution Architect, delegated; rule 3 and the load decided by the Maintainer. Builds on #114 (decision 0018, merged) and #33's dequeue.
1. The paths are a fact. New in
schemas/facts.schema.json:changed_paths, every path the pull request changes, each once, from the top of the repo. A rename or copy gives both its old and new path; a deletion gives its old one. New flagcomplete.changed_paths, added toREAD_IN_FULLinkernel/terms.py. A flag that is not exactly true givesfacts_incomplete, and a queued item leaves the queue (#33). A list read in full can still be unreadable: not a list of strings, or a path that is empty, has a leading or trailing/, or a.or..name. That gives the newpaths_unreadable, inNEEDS_YOU_REASONS, read wherereview_unreadableis. Why: the Kernel cannot hold what it cannot see, and a misread list must stop the item.2. One list, in
config/hooks.json.protectedgainsconfig/apps.json. A new key,lens_inputs, gives{lens, path, is}:verify:Makefile, plusCHECKSfromtools/check_context.py;.gitleaks.toml.The workflow files are already under
.github/, and CodeQL has no input in the repo. The syntax is whatschemas/hooks.schema.jsonalready allows: a final/means everything below it, and*matches part of one name. Matching ignores case, so it can hold more, never less. One new function besideload()intools/hooks/protected.pygives the Kernel configprotected_paths, the patterns withoutis, which is prose.tests/support.pyandsim/world.pyboth call it. The session hooks treatlens_inputsasreported. New tests intests/test_tools.pyhold this:protected.entries()agree on one corpus;lensis a required deterministic Lens, andverify's inputs coverCHECKS;workflowis protected.Why: with one file, only the two matchers can drift, and a test pins them.
3. What lets such a change converge: pr-review's review of this head. Decided by the Maintainer, 2026-10-16. No person is needed. A change that touches the list converges only once every model Lens has reviewed its current head, as any change is reviewed, and the approval is then submitted by the machine user (
donpetry-bot, the configuredapprover), as it is for every item. Inrequired_model_lenses()inkernel/derive.py, a change that touches the list needs every model Lens whatever its class:model_not_required_fordoes not apply. A session pushing as the Maintainer can never approve its own change, since the approval comes from another account, and the machine user never pushes (§5.1, I-26). No new approval fact and no self-push exception. Why: the gap is a change judged by nothing but the Lens it edits. A review by another model, bound to the content, closes it without stopping each change for a person.4. Without it. No new Hold: an item whose model review of this head is missing waits for it as any item does (
waitingon the review), and a queued one whose head changes already leaves the queue (#33). Only an unreadable list stops the item for a person (rule 1,paths_unreadable,needs_you). The decision names the matched configured patterns, never a changed path. Why: the review is the gate, so the gate's own machinery is enough.5. No class is excused. A Dependabot pin update to
.github/workflows/verify.ymlis reviewed by pr-review before it converges.model_not_required_forstill excuses a dependency update that touches nothing on the list. Why: the Lens it edits is otherwise its only judge.6. Records. The next free number is 0020 (0019 is the session hooks' bypass, decided the same day):
docs/decisions/0020-a-change-to-the-factorys-own-rules-or-a-lenss-inputs-is-reviewed-by-a-model-lens.md, new, replacing nothing. In 0018's workflow-file row, only the Check cell changes, astools/check_decisions.pyallows. It namestest_a_lens_input_counts_like_a_protected_path, and the refusal stays waiting onharness/. Design §4.4 gains I-45.7. Simulator. Touched paths draw from
random.Random("paths %d" % seed), so the fixed seeds stay as they were. Three actors make such changes: a Role's fix Run touchestests/, Dependabot edits.github/workflows/verify.yml, and the Maintainer pushesconfig/defaults.json.World.invariants()andapprove()assert I-45: nothing touching the list is approved, enqueued or merged without every model Lens's review of that head, whatever its class.tests/test_simulator.pychecks the seeds reach every case.8. Boundary tests (
tests/test_decide.py) and mutants.test_a_change_to_a_protected_path_needs_the_model_review(README.mdon adependencyitem converges without it)test_a_lens_input_counts_like_a_protected_path/matches only the directorytest_a_pattern_matches_one_name_per_star_in_any_case*crosses/; case comparedtest_either_name_of_a_rename_countstest_a_dependency_update_to_a_workflow_pin_needs_the_model_reviewdependencyexcused on the listtest_a_queued_change_to_the_list_whose_head_moves_leaves_the_queuetest_changed_paths_read_short_deferchanged_pathstaken out ofREAD_IN_FULLtest_an_unreadable_changed_path_defers_for_the_maintainer..readtest_a_decision_names_the_pattern_never_the_pathCounter-examples:
facts.no-completeness-for-changed-paths.json,hooks.a-lens-input-with-no-lens.json.9. Follow-ups.
adapters/; until then only fixtures produce the fact.applyrefuses a Role's push to a protected path or a Lens's input that no model Lens has reviewed".roles/).protected_pathsin this syntax".Decided by the Maintainer, 2026-10-16.
donpetry-bot, is the gate; a session pushing as the Maintainer cannot approve itself.